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:
+80
-19
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user