diff --git a/cmd/admin/main.go b/cmd/admin/main.go index b025d76..449e4a8 100644 --- a/cmd/admin/main.go +++ b/cmd/admin/main.go @@ -35,6 +35,7 @@ func main() { invalid("failed to generate a random password") } + // Create the admin user. err = c.ORM.User. Create(). SetEmail(email). diff --git a/ent/admin/extension.go b/ent/admin/extension.go index e857450..308e2eb 100644 --- a/ent/admin/extension.go +++ b/ent/admin/extension.go @@ -16,6 +16,7 @@ var ( templateDir embed.FS ) +// Extension is the Ent extension that generates code to support the entity admin panel. type Extension struct { entc.DefaultExtension } @@ -34,6 +35,7 @@ func (*Extension) Templates() []*gen.Template { } } +// fieldName provides a struct field name from an entity field name (ie, user_id -> UserID). func fieldName(name string) string { if len(name) == 0 { return name @@ -51,6 +53,7 @@ func fieldName(name string) string { return strings.Join(parts, "") } +// FieldLabel provides a label for an entity field name (ie, user_id -> User ID). func FieldLabel(name string) string { if len(name) == 0 { return name @@ -69,6 +72,7 @@ func FieldLabel(name string) string { return strings.Join(parts, " ") } +// fieldIsPointer determines if a given entity field should be a pointer on the struct. func fieldIsPointer(f *gen.Field) bool { switch { case f.Type.Type == field.TypeBool: @@ -82,6 +86,7 @@ func fieldIsPointer(f *gen.Field) bool { return false } +// upperFirst uppercases the first character of a given string. func upperFirst(s string) string { if len(s) == 0 { return s diff --git a/pkg/context/context.go b/pkg/context/context.go index d433949..05ad549 100644 --- a/pkg/context/context.go +++ b/pkg/context/context.go @@ -34,6 +34,12 @@ const ( // ConfigKey is the key used to store the configuration in context. ConfigKey = "config" + + // AdminEntityKey is the key used to store the entity being operated on in the admin panel. + AdminEntityKey = "admin:entity" + + // AdminEntityIDKey is the key used to store the ID of the entity being operated on in the admin panel. + AdminEntityIDKey = "admin:entity_id" ) // IsCanceledError determines if an error is due to a context cancellation. diff --git a/pkg/handlers/admin.go b/pkg/handlers/admin.go index a828f27..b61bbc5 100644 --- a/pkg/handlers/admin.go +++ b/pkg/handlers/admin.go @@ -12,6 +12,7 @@ import ( "github.com/labstack/echo/v4" "github.com/mikestefanello/pagoda/ent" "github.com/mikestefanello/pagoda/ent/admin" + "github.com/mikestefanello/pagoda/pkg/context" "github.com/mikestefanello/pagoda/pkg/middleware" "github.com/mikestefanello/pagoda/pkg/msg" "github.com/mikestefanello/pagoda/pkg/pager" @@ -21,10 +22,6 @@ import ( "github.com/mikestefanello/pagoda/pkg/ui/pages" ) -// TODO plugins should create keys dynamically -const entityContextKey = "admin:entity" -const entityIDContextKey = "admin:entity_id" - type Admin struct { orm *ent.Client graph *gen.Graph @@ -53,8 +50,6 @@ func (h *Admin) Routes(g *echo.Group) { ng := entities.Group(fmt.Sprintf("/%s", strings.ToLower(n.Name))) ng.GET("", h.EntityList(n)). Name = routenames.AdminEntityList(n.Name) - ng.POST("", h.EntityList(n)). - Name = routenames.AdminEntityListSubmit(n.Name) ng.GET("/add", h.EntityAdd(n)). Name = routenames.AdminEntityAdd(n.Name) ng.POST("/add", h.EntityAddSubmit(n)). @@ -70,6 +65,7 @@ func (h *Admin) Routes(g *echo.Group) { } } +// middlewareEntityLoad is middleware to extract the entity ID and attempt to load the given entity. func (h *Admin) middlewareEntityLoad(n *gen.Type) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(ctx echo.Context) error { @@ -81,8 +77,8 @@ func (h *Admin) middlewareEntityLoad(n *gen.Type) echo.MiddlewareFunc { entity, err := h.admin.Get(ctx, n.Name, id) switch { case err == nil: - ctx.Set(entityIDContextKey, id) - ctx.Set(entityContextKey, map[string][]string(entity)) + ctx.Set(context.AdminEntityIDKey, id) + ctx.Set(context.AdminEntityKey, map[string][]string(entity)) return next(ctx) case ent.IsNotFound(err): return echo.NewHTTPError(http.StatusNotFound, "entity not found") @@ -134,14 +130,14 @@ func (h *Admin) EntityAddSubmit(n *gen.Type) echo.HandlerFunc { func (h *Admin) EntityEdit(n *gen.Type) echo.HandlerFunc { return func(ctx echo.Context) error { - v := ctx.Get(entityContextKey).(map[string][]string) + v := ctx.Get(context.AdminEntityKey).(map[string][]string) return pages.AdminEntityForm(ctx, false, h.getEntitySchema(n), v) } } func (h *Admin) EntityEditSubmit(n *gen.Type) echo.HandlerFunc { return func(ctx echo.Context) error { - id := ctx.Get(entityIDContextKey).(int) + id := ctx.Get(context.AdminEntityIDKey).(int) err := h.admin.Update(ctx, n.Name, id) if err != nil { msg.Danger(ctx, err.Error()) @@ -166,7 +162,7 @@ func (h *Admin) EntityDelete(n *gen.Type) echo.HandlerFunc { func (h *Admin) EntityDeleteSubmit(n *gen.Type) echo.HandlerFunc { return func(ctx echo.Context) error { - id := ctx.Get(entityIDContextKey).(int) + id := ctx.Get(context.AdminEntityIDKey).(int) if err := h.admin.Delete(ctx, n.Name, id); err != nil { msg.Danger(ctx, err.Error()) return h.EntityDelete(n)(ctx) diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index d06391f..bd58912 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -15,7 +15,7 @@ import ( "github.com/labstack/echo/v4" ) -// LoadAuthenticatedUser loads the authenticated user, if one, and stores in context +// LoadAuthenticatedUser loads the authenticated user, if one, and stores in context. func LoadAuthenticatedUser(authClient *services.AuthClient) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { @@ -41,7 +41,7 @@ func LoadAuthenticatedUser(authClient *services.AuthClient) echo.MiddlewareFunc // LoadValidPasswordToken loads a valid password token entity that matches the user and token // provided in path parameters // If the token is invalid, the user will be redirected to the forgot password route -// This requires that the user owning the token is loaded in to context +// This requires that the user owning the token is loaded in to context. func LoadValidPasswordToken(authClient *services.AuthClient) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { @@ -51,13 +51,13 @@ func LoadValidPasswordToken(authClient *services.AuthClient) echo.MiddlewareFunc } usr := c.Get(context.UserKey).(*ent.User) - // Extract the token ID + // Extract the token ID. tokenID, err := strconv.Atoi(c.Param("password_token")) if err != nil { return echo.NewHTTPError(http.StatusNotFound) } - // Attempt to load a valid password token + // Attempt to load a valid password token. token, err := authClient.GetValidPasswordToken( c, usr.ID, @@ -82,7 +82,7 @@ func LoadValidPasswordToken(authClient *services.AuthClient) echo.MiddlewareFunc } } -// RequireAuthentication requires that the user be authenticated in order to proceed +// RequireAuthentication requires that the user be authenticated in order to proceed. func RequireAuthentication(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if u := c.Get(context.AuthenticatedUserKey); u == nil { @@ -93,7 +93,7 @@ func RequireAuthentication(next echo.HandlerFunc) echo.HandlerFunc { } } -// RequireNoAuthentication requires that the user not be authenticated in order to proceed +// RequireNoAuthentication requires that the user not be authenticated in order to proceed. func RequireNoAuthentication(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if u := c.Get(context.AuthenticatedUserKey); u != nil { @@ -104,13 +104,12 @@ func RequireNoAuthentication(next echo.HandlerFunc) echo.HandlerFunc { } } -// RequireAdmin requires that the user be an admin in order to proceed. +// RequireAdmin requires that the authenticated user be an admin in order to proceed. func RequireAdmin(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if u := c.Get(context.AuthenticatedUserKey); u != nil { if user, ok := u.(*ent.User); ok { if user.Admin { - // TODO tests return next(c) } } diff --git a/pkg/middleware/auth_test.go b/pkg/middleware/auth_test.go index eebd910..01e9161 100644 --- a/pkg/middleware/auth_test.go +++ b/pkg/middleware/auth_test.go @@ -1,6 +1,7 @@ package middleware import ( + goctx "context" "fmt" "net/http" "testing" @@ -40,7 +41,7 @@ func TestRequireAuthentication(t *testing.T) { tests.InitSession(ctx) // Not logged in - err := tests.ExecuteMiddleware(ctx, RequireAuthentication()) + err := tests.ExecuteMiddleware(ctx, RequireAuthentication) tests.AssertHTTPErrorCode(t, err, http.StatusUnauthorized) // Login @@ -49,7 +50,7 @@ func TestRequireAuthentication(t *testing.T) { _ = tests.ExecuteMiddleware(ctx, LoadAuthenticatedUser(c.Auth)) // Logged in - err = tests.ExecuteMiddleware(ctx, RequireAuthentication()) + err = tests.ExecuteMiddleware(ctx, RequireAuthentication) assert.Nil(t, err) } @@ -58,7 +59,7 @@ func TestRequireNoAuthentication(t *testing.T) { tests.InitSession(ctx) // Not logged in - err := tests.ExecuteMiddleware(ctx, RequireNoAuthentication()) + err := tests.ExecuteMiddleware(ctx, RequireNoAuthentication) assert.Nil(t, err) // Login @@ -67,7 +68,7 @@ func TestRequireNoAuthentication(t *testing.T) { _ = tests.ExecuteMiddleware(ctx, LoadAuthenticatedUser(c.Auth)) // Logged in - err = tests.ExecuteMiddleware(ctx, RequireNoAuthentication()) + err = tests.ExecuteMiddleware(ctx, RequireNoAuthentication) tests.AssertHTTPErrorCode(t, err, http.StatusForbidden) } @@ -109,3 +110,36 @@ func TestLoadValidPasswordToken(t *testing.T) { require.True(t, ok) assert.Equal(t, pt.ID, ctxPt.ID) } + +func TestRequireAdmin(t *testing.T) { + ctx, _ := tests.NewContext(c.Web, "/") + tests.InitSession(ctx) + + // Not logged in + err := tests.ExecuteMiddleware(ctx, RequireAdmin) + tests.AssertHTTPErrorCode(t, err, http.StatusUnauthorized) + + // Login as a non-admin + err = c.Auth.Login(ctx, usr.ID) + require.NoError(t, err) + _ = tests.ExecuteMiddleware(ctx, LoadAuthenticatedUser(c.Auth)) + + // Logged in as a non-admin + err = tests.ExecuteMiddleware(ctx, RequireAdmin) + tests.AssertHTTPErrorCode(t, err, http.StatusUnauthorized) + + // Create an admin and login + adm, err := tests.CreateUser(c.ORM) + require.NoError(t, err) + err = c.ORM.User.Update(). + SetAdmin(true). + Exec(goctx.Background()) + require.NoError(t, err) + err = c.Auth.Login(ctx, adm.ID) + require.NoError(t, err) + _ = tests.ExecuteMiddleware(ctx, LoadAuthenticatedUser(c.Auth)) + + // Logged in as an admin + err = tests.ExecuteMiddleware(ctx, RequireAdmin) + assert.Nil(t, err) +} diff --git a/pkg/routenames/names.go b/pkg/routenames/names.go index c7cc90a..007c6e2 100644 --- a/pkg/routenames/names.go +++ b/pkg/routenames/names.go @@ -32,10 +32,6 @@ func AdminEntityList(entityTypeName string) string { return fmt.Sprintf("admin:%s_list", entityTypeName) } -func AdminEntityListSubmit(entityTypeName string) string { - return fmt.Sprintf("admin:%s_list.submit", entityTypeName) -} - func AdminEntityAdd(entityTypeName string) string { return fmt.Sprintf("admin:%s_add", entityTypeName) } diff --git a/pkg/services/auth_test.go b/pkg/services/auth_test.go index b654296..459446e 100644 --- a/pkg/services/auth_test.go +++ b/pkg/services/auth_test.go @@ -55,7 +55,7 @@ func TestAuthClient_GeneratePasswordResetToken(t *testing.T) { token, pt, err := c.Auth.GeneratePasswordResetToken(ctx, usr.ID) require.NoError(t, err) assert.Len(t, token, c.Config.App.PasswordToken.Length) - assert.NoError(t, c.Auth.CheckPassword(token, pt.Hash)) + assert.NoError(t, c.Auth.CheckPassword(token, pt.Token)) } func TestAuthClient_GetValidPasswordToken(t *testing.T) { diff --git a/pkg/services/container_test.go b/pkg/services/container_test.go index 1e0bd24..7f4a5d4 100644 --- a/pkg/services/container_test.go +++ b/pkg/services/container_test.go @@ -17,8 +17,4 @@ func TestNewContainer(t *testing.T) { assert.NotNil(t, c.Mail) assert.NotNil(t, c.Auth) assert.NotNil(t, c.Tasks) - g := c.Graph - if g == nil { - - } }