feat: Enhance admin user management system

- Add admin override capability to DELETE /api/auth/account endpoint
- Move /api/auth/users to admin-only with complete user fields (first_name, last_name, role, theme)
- Consolidate Bruno requests: remove duplicate List Users (Admin), merge Delete Account functionality
- Update all documentation to reflect enhanced capabilities
- Implement pgx 5 standards compliance with proper error handling

BREAKING CHANGES:
- /api/auth/users endpoint now requires admin role (was previously accessible)
- DELETE /api/auth/account accepts optional user_id parameter for admin deletion
This commit is contained in:
2026-01-27 13:36:11 -05:00
parent 9262a35f68
commit 71584c1b55
11 changed files with 345 additions and 52 deletions
+80 -19
View File
@@ -198,13 +198,21 @@ window.location.href = '/api/dashboard';
return c.HTML(http.StatusCreated, html)
}
if user.FirstName.Valid {
firstName = user.FirstName.String
}
if user.LastName.Valid {
lastName = user.LastName.String
}
return c.JSON(http.StatusCreated, AuthResponse{
Token: token,
User: UserProfile{
ID: uuid.UUID(user.ID.Bytes).String(),
Email: user.Email,
Username: user.Username,
Role: user.Role,
ID: uuid.UUID(user.ID.Bytes).String(),
Email: user.Email,
Username: user.Username,
FirstName: firstName,
LastName: lastName,
Role: user.Role,
},
})
}
@@ -375,7 +383,7 @@ func (h *AuthHandler) UpdateProfile(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"message": "profile updated"})
}
// ListUsers handles GET /api/users
// ListUsers handles GET /api/auth/users
func (h *AuthHandler) ListUsers(c echo.Context) error {
users, err := h.db.ListUsers(c.Request().Context())
if err != nil {
@@ -386,7 +394,10 @@ func (h *AuthHandler) ListUsers(c echo.Context) error {
ID string `json:"id"`
Email string `json:"email"`
Username string `json:"username"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Theme string `json:"theme"`
Role string `json:"role"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
@@ -397,6 +408,14 @@ func (h *AuthHandler) ListUsers(c echo.Context) error {
if u.Theme.Valid {
theme = u.Theme.String
}
firstName := ""
if u.FirstName.Valid {
firstName = u.FirstName.String
}
lastName := ""
if u.LastName.Valid {
lastName = u.LastName.String
}
createdAt := ""
if u.CreatedAt.Valid {
createdAt = u.CreatedAt.Time.Format("2006-01-02T15:04:05Z07:00")
@@ -409,7 +428,10 @@ func (h *AuthHandler) ListUsers(c echo.Context) error {
ID: uuid.UUID(u.ID.Bytes).String(),
Email: u.Email,
Username: u.Username,
FirstName: firstName,
LastName: lastName,
Theme: theme,
Role: u.Role,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
})
@@ -599,7 +621,7 @@ type UpdateUsernameRequest struct {
Username string `json:"username" validate:"required,min=3,max=50"`
}
// UpdateUsername handles PUT /api/user/username
// UpdateUsername handles PUT /api/auth/username
func (h *AuthHandler) UpdateUsername(c echo.Context) error {
userID := c.Get("user_id").(string)
userUUID, err := uuid.Parse(userID)
@@ -637,7 +659,7 @@ type UpdateEmailRequest struct {
Email string `json:"email" validate:"required,email"`
}
// UpdateEmail handles PUT /api/user/email
// UpdateEmail handles PUT /api/auth/email
func (h *AuthHandler) UpdateEmail(c echo.Context) error {
userID := c.Get("user_id").(string)
userUUID, err := uuid.Parse(userID)
@@ -677,7 +699,7 @@ type UpdatePasswordRequest struct {
ConfirmPassword string `json:"confirm_password" validate:"required"`
}
// UpdatePassword handles PUT /api/user/password
// UpdatePassword handles PUT /api/auth/password
func (h *AuthHandler) UpdatePassword(c echo.Context) error {
userID := c.Get("user_id").(string)
userUUID, err := uuid.Parse(userID)
@@ -730,41 +752,80 @@ func (h *AuthHandler) UpdatePassword(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"message": "password updated successfully"})
}
// DeleteAccount handles DELETE /api/user/account
// DeleteAccount handles DELETE /api/auth/account
// Supports self-deletion or admin deletion of other users
func (h *AuthHandler) DeleteAccount(c echo.Context) error {
// Get target user ID from query parameter (for admin override) or use current user
targetUserID := c.QueryParam("user_id")
userID := c.Get("user_id").(string)
// If admin override is used, validate admin and use target
if targetUserID != "" {
// Admin override mode - check if current user is admin
userRole := c.Get("user_role").(string)
if userRole != "admin" {
return c.JSON(http.StatusForbidden, map[string]string{"error": "admin access required"})
}
userID = targetUserID
}
userUUID, err := uuid.Parse(userID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
}
// Check if this is the last user - prevent deletion
// Check if this is the last admin user - prevent deletion
users, err := h.db.ListUsers(c.Request().Context())
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to check existing users"})
}
if len(users) == 1 {
// Convert user UUIDs to string for comparison
lastUserID := uuid.UUID(users[0].ID.Bytes).String()
if lastUserID == userID {
if c.Request().Header.Get("HX-Request") == "true" {
return c.HTML(http.StatusBadRequest, `<div class="text-red-500">Cannot delete the last user account</div>`)
}
return c.JSON(http.StatusBadRequest, map[string]string{"error": "cannot delete the last user account"})
// Count admin users and identify the user to be deleted
adminCount := 0
targetUserRole := ""
for _, user := range users {
if user.Role == "admin" {
adminCount++
}
// Find target user details
userUUIDStr := uuid.UUID(user.ID.Bytes).String()
if userUUIDStr == userID {
targetUserRole = user.Role
}
}
// Prevent deletion if target user is admin and this is the last admin
if targetUserRole == "admin" && adminCount == 1 {
if c.Request().Header.Get("HX-Request") == "true" {
return c.HTML(http.StatusBadRequest, `<div class="text-red-500">Cannot delete the last admin account</div>`)
}
return c.JSON(http.StatusBadRequest, map[string]string{"error": "cannot delete the last admin account"})
}
// Delete user (this will cascade to delete all related data)
err = h.db.DeleteUser(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true})
if err != nil {
if err == pgx.ErrNoRows {
if c.Request().Header.Get("HX-Request") == "true" {
return c.HTML(http.StatusNotFound, `<div class="text-red-500">User not found</div>`)
}
return c.JSON(http.StatusNotFound, map[string]string{"error": "user not found"})
}
if c.Request().Header.Get("HX-Request") == "true" {
return c.HTML(http.StatusInternalServerError, `<div class="text-red-500">Failed to delete account</div>`)
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, map[string]string{"message": "account deleted successfully"})
// Create success message based on context
var message string
if targetUserID != "" && userID != c.Get("user_id").(string) {
message = "user account deleted successfully"
} else {
message = "account deleted successfully"
}
return c.JSON(http.StatusOK, map[string]string{"message": message})
}
type UpdateScanSettingsRequest struct {