From 930d020ec1af1357d36d5f9115f8afe22c905989 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sun, 22 Feb 2026 01:57:56 -0500 Subject: [PATCH] feat(router): add routes for consolidated user management Add DELETE /api/auth/users/:id, PUT /api/auth/users/:id/password, and PUT /api/auth/users/:id routes. Remove individual profile update routes in favor of consolidated endpoints. --- internal/router/auth.go | 12 ++-- internal/router/frontend.go | 111 +++++++++++++++++++++++++++++++----- 2 files changed, 105 insertions(+), 18 deletions(-) diff --git a/internal/router/auth.go b/internal/router/auth.go index 3fe1a9d..129ead2 100644 --- a/internal/router/auth.go +++ b/internal/router/auth.go @@ -21,7 +21,6 @@ func registerAuthRoutes(cfg *Config, rateLimitMiddleware echo.MiddlewareFunc) { // Protected auth routes protected.GET("/auth/profile", cfg.AuthHandler.GetProfile) - protected.PUT("/auth/profile", cfg.AuthHandler.UpdateProfile) // Refresh token endpoint (no authentication required - uses refresh token from body) e.POST("/api/auth/refresh", cfg.AuthHandler.RefreshAccessToken) @@ -31,13 +30,18 @@ func registerAuthRoutes(cfg *Config, rateLimitMiddleware echo.MiddlewareFunc) { // Auth update routes authGroup := e.Group("/api/auth", createJWTMiddleware(cfg)) - authGroup.PUT("/email", cfg.AuthHandler.UpdateEmail) - authGroup.PUT("/username", cfg.AuthHandler.UpdateUsername) authGroup.PUT("/password", cfg.AuthHandler.UpdatePassword) authGroup.PUT("/theme", cfg.AuthHandler.UpdateTheme) - // Admin-only routes for user management + // Profile management (combined handlers - self-edit) + authGroup.PUT("/profile", cfg.AuthHandler.UpdateProfile) + authGroup.DELETE("/profile", cfg.AuthHandler.DeleteUser) + + // Admin-only routes (same handlers with URL param) admin := protected.Group("/auth", handlers.AdminMiddleware) admin.GET("/users", cfg.AuthHandler.ListUsers) admin.PUT("/users/:id/max-devices", cfg.AuthHandler.UpdateUserMaxDevices) + admin.PUT("/profile/:id", cfg.AuthHandler.UpdateProfile) + admin.PUT("/password/:id", cfg.AuthHandler.UpdatePassword) + admin.DELETE("/profile/:id", cfg.AuthHandler.DeleteUser) } diff --git a/internal/router/frontend.go b/internal/router/frontend.go index f3f2239..0ea05bd 100644 --- a/internal/router/frontend.go +++ b/internal/router/frontend.go @@ -53,7 +53,8 @@ func registerFrontendRoutes(cfg *Config) { e.GET("/login", func(c echo.Context) error { var buf bytes.Buffer sessionExpired := c.QueryParam("session") == "expired" - err := templates.Login(sessionExpired).Render(c.Request().Context(), &buf) + deleted := c.QueryParam("deleted") == "true" + err := templates.Login(sessionExpired, deleted).Render(c.Request().Context(), &buf) if err != nil { return err } @@ -361,6 +362,21 @@ func registerFrontendRoutes(cfg *Config) { return c.HTML(http.StatusOK, buf.String()) }) + // Profile page (all users) + frontendProtected.GET("/profile", func(c echo.Context) error { + user, err := getTemplateUserWithTheme(c, cfg) + if err != nil { + return renderErrorPage(c, "Error loading user", "user_load_error") + } + + var buf bytes.Buffer + err = templates.Profile(user).Render(c.Request().Context(), &buf) + if err != nil { + return err + } + return c.HTML(http.StatusOK, buf.String()) + }) + // ============================================================================ // ADMIN FRONTEND ROUTES // ============================================================================ @@ -392,19 +408,6 @@ func registerFrontendRoutes(cfg *Config) { return c.HTML(http.StatusOK, buf.String()) })) - frontendProtected.GET("/admin/profile", handlers.AdminMiddleware(func(c echo.Context) error { - user, err := getTemplateUserWithTheme(c, cfg) - if err != nil { - return renderErrorPage(c, "Error loading user", "user_load_error") - } - var buf bytes.Buffer - err = templates.AdminProfile(user).Render(c.Request().Context(), &buf) - if err != nil { - return err - } - return c.HTML(http.StatusOK, buf.String()) - })) - frontendProtected.GET("/admin/library", handlers.AdminMiddleware(func(c echo.Context) error { user, err := getTemplateUserWithTheme(c, cfg) if err != nil { @@ -418,6 +421,86 @@ func registerFrontendRoutes(cfg *Config) { return c.HTML(http.StatusOK, buf.String()) })) + // Admin users page + frontendProtected.GET("/admin/users", handlers.AdminMiddleware(func(c echo.Context) error { + user, err := getTemplateUserWithTheme(c, cfg) + if err != nil { + return renderErrorPage(c, "Error loading user", "user_load_error") + } + + // Fetch all users + users, err := cfg.Queries.ListUsers(c.Request().Context()) + if err != nil { + return renderErrorPage(c, "Error loading users", "users_load_error") + } + + // Count admins for UI protection + adminCount := 0 + for _, u := range users { + if u.Role == "admin" { + adminCount++ + } + } + + // Convert to template users + templateUsers := make([]templates.User, len(users)) + for i, u := range users { + templateUsers[i] = templates.User{ + ID: uuid.UUID(u.ID.Bytes).String(), + Username: u.Username, + Email: u.Email, + Role: u.Role, + Theme: getText(u.Theme), + FirstName: getText(u.FirstName), + LastName: getText(u.LastName), + CreatedAt: u.CreatedAt.Time, + } + } + + var buf bytes.Buffer + err = templates.AdminUsers(templateUsers, adminCount, user.ID).Render(c.Request().Context(), &buf) + if err != nil { + return err + } + return c.HTML(http.StatusOK, buf.String()) + })) + + // Admin: Get profile modal for editing user + frontendProtected.GET("/admin/users/:id/profile-modal", handlers.AdminMiddleware(func(c echo.Context) error { + // Get target user ID from URL + targetUserID := c.Param("id") + parsedUUID, err := uuid.Parse(targetUserID) + if err != nil { + return c.HTML(http.StatusBadRequest, "
Invalid user ID
") + } + + // Fetch target user + targetUser, err := cfg.Queries.GetUser(c.Request().Context(), uuidToPGType(parsedUUID)) + if err != nil { + return c.HTML(http.StatusNotFound, "
User not found
") + } + + // Convert to template user + templateUser := templates.User{ + ID: uuid.UUID(targetUser.ID.Bytes).String(), + Username: targetUser.Username, + Email: targetUser.Email, + Role: targetUser.Role, + Theme: getText(targetUser.Theme), + FirstName: getText(targetUser.FirstName), + LastName: getText(targetUser.LastName), + CreatedAt: targetUser.CreatedAt.Time, + } + + // Render modal + var buf bytes.Buffer + err = templates.ProfileModal(templateUser).Render(c.Request().Context(), &buf) + if err != nil { + return err + } + return c.HTML(http.StatusOK, buf.String()) + })) + // ============================================================================ // LEGACY API ROUTES (for backward compatibility) // ============================================================================