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:
@@ -0,0 +1,104 @@
|
|||||||
|
# Documentation Updates Summary
|
||||||
|
|
||||||
|
This document summarizes all documentation changes made to keep the codebase consistent with API improvements.
|
||||||
|
|
||||||
|
## 🔄 Recent API Consistency Changes
|
||||||
|
|
||||||
|
### 1. **Unified API Structure**
|
||||||
|
- **User Management**: All endpoints now use `/api/auth/*` prefix
|
||||||
|
- **Library Management**: All endpoints use `/api/library/*` prefix
|
||||||
|
- **Ebook Folders**: All endpoints use `/api/auth/ebook-folders` (admin-only)
|
||||||
|
|
||||||
|
### 2. **Updated Endpoint Mappings**
|
||||||
|
|
||||||
|
| Previous | Current | Function |
|
||||||
|
|----------|---------|----------|
|
||||||
|
| `/api/user/username` | `/api/auth/username` | Update username |
|
||||||
|
| `/api/user/email` | `/api/auth/email` | Update email |
|
||||||
|
| `/api/user/password` | `/api/auth/password` | Update password |
|
||||||
|
| `/api/user/account` | `/api/auth/account` | Delete account |
|
||||||
|
|
||||||
|
### 3. **Enhanced Security Logic**
|
||||||
|
- **Admin Protection**: Updated deletion logic to prevent deleting last **admin user** (not just last user)
|
||||||
|
- **More Secure**: Ensures system always has administrative access
|
||||||
|
|
||||||
|
## 📝 Updated Documentation Files
|
||||||
|
|
||||||
|
### 1. **README.md**
|
||||||
|
✅ Updated all API endpoint references to use consistent `/api/auth/*` structure
|
||||||
|
✅ Fixed deletion protection description from "last user" to "last admin"
|
||||||
|
✅ All endpoint documentation now matches actual implementation
|
||||||
|
|
||||||
|
### 2. **bruno/README.md**
|
||||||
|
✅ Already correctly documented with consistent API structure
|
||||||
|
✅ Comprehensive API testing documentation maintained
|
||||||
|
|
||||||
|
### 3. **bruno/user/Delete Account.bru**
|
||||||
|
✅ Updated endpoint documentation from `/api/user/account` to `/api/auth/account`
|
||||||
|
|
||||||
|
### 4. **Internal Handler Comments** (`internal/handlers/auth.go`)
|
||||||
|
✅ Updated all handler comment blocks to reference correct endpoints:
|
||||||
|
- `ListUsers` → `/api/auth/users`
|
||||||
|
- `UpdateUsername` → `/api/auth/username`
|
||||||
|
- `UpdateEmail` → `/api/auth/email`
|
||||||
|
- `UpdatePassword` → `/api/auth/password`
|
||||||
|
- `DeleteAccount` → `/api/auth/account`
|
||||||
|
|
||||||
|
### 5. **Template Files** (`templates/admin_profile.templ`)
|
||||||
|
✅ Updated HTMX form endpoints:
|
||||||
|
- Username form: `/api/user/username` → `/api/auth/username`
|
||||||
|
- Email form: `/api/user/email` → `/api/auth/email`
|
||||||
|
- Password form: `/api/user/password` → `/api/auth/password`
|
||||||
|
|
||||||
|
## 🎯 Complete API Documentation
|
||||||
|
|
||||||
|
### Authentication (Public)
|
||||||
|
- `POST /api/auth/register` - Register new user
|
||||||
|
- `POST /api/auth/login` - Login user
|
||||||
|
|
||||||
|
### User Management (Protected)
|
||||||
|
- `GET /api/auth/profile` - Get user profile
|
||||||
|
- `PUT /api/auth/profile` - Update profile
|
||||||
|
- `PUT /api/auth/username` - Update username
|
||||||
|
- `PUT /api/auth/email` - Update email
|
||||||
|
- `PUT /api/auth/password` - Update password
|
||||||
|
- `PUT /api/auth/theme` - Update theme
|
||||||
|
- `DELETE /api/auth/account` - Delete account ⚠️ *Cannot delete last admin*
|
||||||
|
|
||||||
|
### Admin Operations (Admin Only)
|
||||||
|
- `GET /api/auth/users` - List all users
|
||||||
|
- `POST /api/auth/ebook-folders` - Add folder
|
||||||
|
- `GET /api/auth/ebook-folders` - List folders
|
||||||
|
- `DELETE /api/auth/ebook-folders` - Remove folder
|
||||||
|
|
||||||
|
### Library Management (Admin Only)
|
||||||
|
- `PUT /api/library/scan-settings` - Update scan settings
|
||||||
|
- `GET /api/library/scan-settings` - Get scan settings
|
||||||
|
|
||||||
|
### Ebooks & Progress (All Authenticated Users)
|
||||||
|
- `GET /api/ebooks` - List ebooks
|
||||||
|
- `GET /api/ebooks/:id` - Get ebook details
|
||||||
|
- `GET /api/ebooks/:id/progress` - Get reading progress
|
||||||
|
- `PUT /api/ebooks/:id/progress` - Update progress
|
||||||
|
- `GET /api/ebooks/:id/rating` - Get rating
|
||||||
|
- `POST /api/ebooks/:id/rating` - Create/update rating
|
||||||
|
- `DELETE /api/ebooks/:id/rating` - Delete rating
|
||||||
|
- `GET /api/ebooks/:id/ratings` - Get all ratings
|
||||||
|
|
||||||
|
## 🔄 Build & Deployment
|
||||||
|
|
||||||
|
All changes have been incorporated into the Docker build process:
|
||||||
|
- Templates are regenerated with `templ generate`
|
||||||
|
- API consistency is enforced through build process
|
||||||
|
- Documentation stays in sync with implementation
|
||||||
|
|
||||||
|
## 📋 Testing
|
||||||
|
|
||||||
|
**Bruno Collection**: All API tests updated to match consistent endpoint structure
|
||||||
|
**Authentication Flow**: Register → Login → Use Bearer token for protected routes
|
||||||
|
**Role Testing**: Admin protection verified for critical operations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last Updated: January 27, 2026*
|
||||||
|
*API Version: v1 - Consistent Endpoint Structure*
|
||||||
@@ -181,11 +181,11 @@ The application uses Go HTML templates for server-side rendering with HTMX for d
|
|||||||
- `GET /api/auth/profile` - Get user profile (requires JWT)
|
- `GET /api/auth/profile` - Get user profile (requires JWT)
|
||||||
- `PUT /api/auth/profile` - Update user profile (first_name, last_name) (requires JWT)
|
- `PUT /api/auth/profile` - Update user profile (first_name, last_name) (requires JWT)
|
||||||
- `PUT /api/auth/theme` - Update user theme preference (requires JWT)
|
- `PUT /api/auth/theme` - Update user theme preference (requires JWT)
|
||||||
- `PUT /api/user/username` - Update username (requires JWT)
|
- `PUT /api/auth/username` - Update username (requires JWT)
|
||||||
- `PUT /api/user/email` - Update email (requires JWT)
|
- `PUT /api/auth/email` - Update email (requires JWT)
|
||||||
- `PUT /api/user/password` - Update password (requires JWT)
|
- `PUT /api/auth/password` - Update password (requires JWT)
|
||||||
- `DELETE /api/user/account` - Delete user account (requires JWT)
|
- `DELETE /api/auth/account` - Delete user account (requires JWT)
|
||||||
- **Protection**: Cannot delete the last user account
|
- **Protection**: Cannot delete the last admin account
|
||||||
|
|
||||||
### Ebook Folders (Admin Only)
|
### Ebook Folders (Admin Only)
|
||||||
- `POST /api/auth/ebook-folders` - Add an ebook folder for scanning
|
- `POST /api/auth/ebook-folders` - Add an ebook folder for scanning
|
||||||
|
|||||||
@@ -17,6 +17,13 @@ This directory contains Bruno collection for testing the Bookmann API with compr
|
|||||||
- **Login User**: POST /api/auth/login - Authenticate (email or username)
|
- **Login User**: POST /api/auth/login - Authenticate (email or username)
|
||||||
- **Get Profile**: GET /api/auth/profile - Get user info (requires token)
|
- **Get Profile**: GET /api/auth/profile - Get user info (requires token)
|
||||||
|
|
||||||
|
### User Management (Admin Only)
|
||||||
|
- **List Users**: GET /api/auth/users - Get all users with complete profile info (admin only)
|
||||||
|
- **Delete Account**: DELETE /api/auth/account - Delete own account or admin deletes other accounts with `user_id` parameter (admin only)
|
||||||
|
|
||||||
|
### User Management (Protected)
|
||||||
|
- **Delete Account**: DELETE /api/auth/account - Delete own account (self) or admin deletes other accounts with `user_id` parameter (admin only)
|
||||||
|
|
||||||
### Folders (Admin Only)
|
### Folders (Admin Only)
|
||||||
- **Add Ebook Folder**: POST /api/auth/ebook-folders - Add folder for scanning (admin only)
|
- **Add Ebook Folder**: POST /api/auth/ebook-folders - Add folder for scanning (admin only)
|
||||||
- **Get Ebook Folders**: GET /api/auth/ebook-folders - List configured folders (admin only)
|
- **Get Ebook Folders**: GET /api/auth/ebook-folders - List configured folders (admin only)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ vars {
|
|||||||
base_url: http://localhost:8765
|
base_url: http://localhost:8765
|
||||||
ebookid: 02a535a4-19f8-43fa-b81b-89a226d19dd9
|
ebookid: 02a535a4-19f8-43fa-b81b-89a226d19dd9
|
||||||
fakebookid: 123e4567-e89b-12d3-a456-426614174000
|
fakebookid: 123e4567-e89b-12d3-a456-426614174000
|
||||||
|
user_id: c51118f0-31fc-4c32-827d-517d6599bf21
|
||||||
}
|
}
|
||||||
vars:secret [
|
vars:secret [
|
||||||
token
|
token
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ meta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
delete {
|
delete {
|
||||||
url: {{base_url}}/api/user/account
|
url: {{base_url}}/api/auth/account
|
||||||
body: none
|
body: none
|
||||||
auth: inherit
|
auth: inherit
|
||||||
}
|
}
|
||||||
@@ -17,23 +17,37 @@ settings {
|
|||||||
|
|
||||||
docs {
|
docs {
|
||||||
## Delete Account
|
## Delete Account
|
||||||
|
|
||||||
Permanently deletes the authenticated user's account and all associated data.
|
Permanently deletes user account and all associated data.
|
||||||
|
|
||||||
**Method:** DELETE
|
**Method:** DELETE
|
||||||
|
|
||||||
**Endpoint:** /api/user/account
|
**Endpoint:** /api/auth/account
|
||||||
|
|
||||||
**Authentication:** Required
|
**Authentication:** Required
|
||||||
|
|
||||||
**Request Body:** None
|
**Usage:**
|
||||||
|
- **Self-deletion**: DELETE /api/auth/account (no parameters)
|
||||||
|
- **Admin deletion**: DELETE /api/auth/account?user_id={uuid} (admin only)
|
||||||
|
|
||||||
|
**Query Parameters (Admin only):**
|
||||||
|
- `user_id` (string): UUID of user account to delete
|
||||||
|
|
||||||
**Response:**
|
**Response:**
|
||||||
- `message` (string): Success message
|
- `message` (string): Success message
|
||||||
|
|
||||||
**Status Codes:**
|
**Status Codes:**
|
||||||
- 200: Success
|
- 200: Success
|
||||||
|
- 400: Bad Request (invalid user_id or attempting to delete last admin)
|
||||||
- 401: Unauthorized
|
- 401: Unauthorized
|
||||||
|
- 403: Forbidden (admin access required for user_id parameter)
|
||||||
|
- 404: Not Found (user does not exist)
|
||||||
|
|
||||||
|
**Protection Rules:**
|
||||||
|
- Regular users can only delete their own account
|
||||||
|
- Admins can delete any account including other users
|
||||||
|
- Cannot delete the last admin account in the system
|
||||||
|
- Admin role required to use user_id parameter
|
||||||
|
|
||||||
**Warning:** This action cannot be undone and will permanently delete all user data including ebooks, ratings, and progress.
|
**Warning:** This action cannot be undone and will permanently delete all user data including ebooks, ratings, and progress.
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
meta {
|
||||||
|
name: Delete User Account (Admin)
|
||||||
|
type: http
|
||||||
|
seq: 6
|
||||||
|
}
|
||||||
|
|
||||||
|
delete {
|
||||||
|
url: {{base_url}}/api/auth/account?user_id={{user_id}}
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
docs {
|
||||||
|
## Delete User Account (Admin)
|
||||||
|
|
||||||
|
Allows administrators to delete any user account by specifying user_id parameter.
|
||||||
|
|
||||||
|
**Method:** DELETE
|
||||||
|
|
||||||
|
**Endpoint:** /api/auth/account?user_id={user_id}
|
||||||
|
|
||||||
|
**Authentication:** Required (Admin only)
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
- `user_id` (string, required for admin): UUID of the user account to delete
|
||||||
|
|
||||||
|
**Usage Examples:**
|
||||||
|
- **Self-deletion**: DELETE /api/auth/account (no user_id parameter)
|
||||||
|
- **Admin deletion**: DELETE /api/auth/account?user_id=550e8400-e29b-41d4-a716-446655440000
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
- `message` (string): Success message indicating which account was deleted
|
||||||
|
|
||||||
|
**Status Codes:**
|
||||||
|
- 200: Success
|
||||||
|
- 400: Bad Request (invalid user_id or attempting to delete last admin)
|
||||||
|
- 401: Unauthorized
|
||||||
|
- 403: Forbidden (admin access required for user_id parameter)
|
||||||
|
- 404: Not Found (user does not exist)
|
||||||
|
|
||||||
|
**Admin Protection Rules:**
|
||||||
|
- Regular users can only delete their own account (no user_id parameter allowed)
|
||||||
|
- Admins can delete any account including their own
|
||||||
|
- Cannot delete the last admin account in the system
|
||||||
|
- Admin role required to use user_id parameter
|
||||||
|
|
||||||
|
**Variables:**
|
||||||
|
- `user_id`: Set this to the UUID of the user you want to delete
|
||||||
|
}
|
||||||
@@ -18,23 +18,76 @@ settings {
|
|||||||
docs {
|
docs {
|
||||||
## List Users
|
## List Users
|
||||||
|
|
||||||
Retrieves a list of all users.
|
Retrieves a list of all users with complete user information.
|
||||||
|
|
||||||
|
**Method:** GET
|
||||||
|
|
||||||
|
**Endpoint:** /api/auth/users
|
||||||
|
|
||||||
|
**Authentication:** Required (Admin only)
|
||||||
|
|
||||||
|
**Response:** Array of user objects with complete information:
|
||||||
|
- `id` (string): User ID (UUID)
|
||||||
|
- `email` (string): Email address
|
||||||
|
- `username` (string): Username
|
||||||
|
- `first_name` (string): First name (empty if not set)
|
||||||
|
- `last_name` (string): Last name (empty if not set)
|
||||||
|
- `role` (string): User role ("user" or "admin")
|
||||||
|
- `theme` (string): Theme preference (empty if default)
|
||||||
|
- `created_at` (string): Creation timestamp (ISO 8601)
|
||||||
|
- `updated_at` (string): Last update timestamp (ISO 8601)
|
||||||
|
|
||||||
|
**Status Codes:**
|
||||||
|
- 200: Success
|
||||||
|
- 401: Unauthorized
|
||||||
|
- 403: Forbidden (admin access required)
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Admin-only endpoint with complete user information
|
||||||
|
- Returns first_name, last_name, role, theme fields
|
||||||
|
- Useful for user management interfaces
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{base_url}}/api/auth/users
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
docs {
|
||||||
|
## List Users (Admin)
|
||||||
|
|
||||||
|
Retrieves a list of all users with complete user information.
|
||||||
|
|
||||||
**Method:** GET
|
**Method:** GET
|
||||||
|
|
||||||
**Endpoint:** /api/auth/users
|
**Endpoint:** /api/auth/users
|
||||||
|
|
||||||
**Authentication:** Required
|
**Authentication:** Required (Admin only)
|
||||||
|
|
||||||
**Response:** Array of user objects
|
**Response:** Array of user objects with complete information:
|
||||||
- `id` (string): User ID
|
- `id` (string): User ID (UUID)
|
||||||
- `email` (string): Email
|
- `email` (string): Email address
|
||||||
- `username` (string): Username
|
- `username` (string): Username
|
||||||
- `theme` (string): User theme preference
|
- `first_name` (string): First name (empty if not set)
|
||||||
- `created_at` (string): Creation timestamp
|
- `last_name` (string): Last name (empty if not set)
|
||||||
- `updated_at` (string): Last update timestamp
|
- `role` (string): User role ("user" or "admin")
|
||||||
|
- `theme` (string): Theme preference (empty if default)
|
||||||
|
- `created_at` (string): Creation timestamp (ISO 8601)
|
||||||
|
- `updated_at` (string): Last update timestamp (ISO 8601)
|
||||||
|
|
||||||
**Status Codes:**
|
**Status Codes:**
|
||||||
- 200: Success
|
- 200: Success
|
||||||
- 401: Unauthorized
|
- 401: Unauthorized
|
||||||
|
- 403: Forbidden (admin access required)
|
||||||
|
|
||||||
|
**Enhanced Features:**
|
||||||
|
- Now admin-only endpoint (moved from public to protected admin group)
|
||||||
|
- Returns complete user profile information including names and role
|
||||||
|
- Useful for comprehensive admin user management
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ body:json {
|
|||||||
"username": "testuser",
|
"username": "testuser",
|
||||||
"password": "password123",
|
"password": "password123",
|
||||||
"first_name": "Test",
|
"first_name": "Test",
|
||||||
"last_name": "User",
|
"last_name": "User"
|
||||||
"role": "user"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +56,7 @@ docs {
|
|||||||
- `id` (string): User ID
|
- `id` (string): User ID
|
||||||
- `email` (string): Email
|
- `email` (string): Email
|
||||||
- `username` (string): Username
|
- `username` (string): Username
|
||||||
- `theme` (string): User theme preference
|
- `theme` (string): User theme preference
|
||||||
- `first_name` (string, optional): First name
|
- `first_name` (string, optional): First name
|
||||||
- `last_name` (string, optional): Last name
|
- `last_name` (string, optional): Last name
|
||||||
- `role` (string): User role ("user" or "admin")
|
- `role` (string): User role ("user" or "admin")
|
||||||
|
|||||||
+2
-2
@@ -70,10 +70,10 @@ func main() {
|
|||||||
protected := e.Group("/api", jwtMiddleware)
|
protected := e.Group("/api", jwtMiddleware)
|
||||||
protected.GET("/auth/profile", authHandler.GetProfile)
|
protected.GET("/auth/profile", authHandler.GetProfile)
|
||||||
protected.PUT("/auth/profile", authHandler.UpdateProfile)
|
protected.PUT("/auth/profile", authHandler.UpdateProfile)
|
||||||
protected.GET("/auth/users", authHandler.ListUsers)
|
|
||||||
|
|
||||||
// Admin-only routes for folder management
|
// Admin-only routes for user and folder management
|
||||||
admin := protected.Group("/auth", handlers.AdminMiddleware)
|
admin := protected.Group("/auth", handlers.AdminMiddleware)
|
||||||
|
admin.GET("/users", authHandler.ListUsers)
|
||||||
admin.POST("/ebook-folders", authHandler.AddEbookFolder)
|
admin.POST("/ebook-folders", authHandler.AddEbookFolder)
|
||||||
admin.GET("/ebook-folders", authHandler.GetEbookFolders)
|
admin.GET("/ebook-folders", authHandler.GetEbookFolders)
|
||||||
admin.DELETE("/ebook-folders", authHandler.DeleteEbookFolder)
|
admin.DELETE("/ebook-folders", authHandler.DeleteEbookFolder)
|
||||||
|
|||||||
+80
-19
@@ -198,13 +198,21 @@ window.location.href = '/api/dashboard';
|
|||||||
return c.HTML(http.StatusCreated, html)
|
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{
|
return c.JSON(http.StatusCreated, AuthResponse{
|
||||||
Token: token,
|
Token: token,
|
||||||
User: UserProfile{
|
User: UserProfile{
|
||||||
ID: uuid.UUID(user.ID.Bytes).String(),
|
ID: uuid.UUID(user.ID.Bytes).String(),
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Role: user.Role,
|
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"})
|
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 {
|
func (h *AuthHandler) ListUsers(c echo.Context) error {
|
||||||
users, err := h.db.ListUsers(c.Request().Context())
|
users, err := h.db.ListUsers(c.Request().Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -386,7 +394,10 @@ func (h *AuthHandler) ListUsers(c echo.Context) error {
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
|
FirstName string `json:"first_name"`
|
||||||
|
LastName string `json:"last_name"`
|
||||||
Theme string `json:"theme"`
|
Theme string `json:"theme"`
|
||||||
|
Role string `json:"role"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
UpdatedAt string `json:"updated_at"`
|
UpdatedAt string `json:"updated_at"`
|
||||||
}
|
}
|
||||||
@@ -397,6 +408,14 @@ func (h *AuthHandler) ListUsers(c echo.Context) error {
|
|||||||
if u.Theme.Valid {
|
if u.Theme.Valid {
|
||||||
theme = u.Theme.String
|
theme = u.Theme.String
|
||||||
}
|
}
|
||||||
|
firstName := ""
|
||||||
|
if u.FirstName.Valid {
|
||||||
|
firstName = u.FirstName.String
|
||||||
|
}
|
||||||
|
lastName := ""
|
||||||
|
if u.LastName.Valid {
|
||||||
|
lastName = u.LastName.String
|
||||||
|
}
|
||||||
createdAt := ""
|
createdAt := ""
|
||||||
if u.CreatedAt.Valid {
|
if u.CreatedAt.Valid {
|
||||||
createdAt = u.CreatedAt.Time.Format("2006-01-02T15:04:05Z07:00")
|
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(),
|
ID: uuid.UUID(u.ID.Bytes).String(),
|
||||||
Email: u.Email,
|
Email: u.Email,
|
||||||
Username: u.Username,
|
Username: u.Username,
|
||||||
|
FirstName: firstName,
|
||||||
|
LastName: lastName,
|
||||||
Theme: theme,
|
Theme: theme,
|
||||||
|
Role: u.Role,
|
||||||
CreatedAt: createdAt,
|
CreatedAt: createdAt,
|
||||||
UpdatedAt: updatedAt,
|
UpdatedAt: updatedAt,
|
||||||
})
|
})
|
||||||
@@ -599,7 +621,7 @@ type UpdateUsernameRequest struct {
|
|||||||
Username string `json:"username" validate:"required,min=3,max=50"`
|
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 {
|
func (h *AuthHandler) UpdateUsername(c echo.Context) error {
|
||||||
userID := c.Get("user_id").(string)
|
userID := c.Get("user_id").(string)
|
||||||
userUUID, err := uuid.Parse(userID)
|
userUUID, err := uuid.Parse(userID)
|
||||||
@@ -637,7 +659,7 @@ type UpdateEmailRequest struct {
|
|||||||
Email string `json:"email" validate:"required,email"`
|
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 {
|
func (h *AuthHandler) UpdateEmail(c echo.Context) error {
|
||||||
userID := c.Get("user_id").(string)
|
userID := c.Get("user_id").(string)
|
||||||
userUUID, err := uuid.Parse(userID)
|
userUUID, err := uuid.Parse(userID)
|
||||||
@@ -677,7 +699,7 @@ type UpdatePasswordRequest struct {
|
|||||||
ConfirmPassword string `json:"confirm_password" validate:"required"`
|
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 {
|
func (h *AuthHandler) UpdatePassword(c echo.Context) error {
|
||||||
userID := c.Get("user_id").(string)
|
userID := c.Get("user_id").(string)
|
||||||
userUUID, err := uuid.Parse(userID)
|
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"})
|
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 {
|
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)
|
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)
|
userUUID, err := uuid.Parse(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
|
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())
|
users, err := h.db.ListUsers(c.Request().Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to check existing users"})
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to check existing users"})
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(users) == 1 {
|
// Count admin users and identify the user to be deleted
|
||||||
// Convert user UUIDs to string for comparison
|
adminCount := 0
|
||||||
lastUserID := uuid.UUID(users[0].ID.Bytes).String()
|
targetUserRole := ""
|
||||||
if lastUserID == userID {
|
for _, user := range users {
|
||||||
if c.Request().Header.Get("HX-Request") == "true" {
|
if user.Role == "admin" {
|
||||||
return c.HTML(http.StatusBadRequest, `<div class="text-red-500">Cannot delete the last user account</div>`)
|
adminCount++
|
||||||
}
|
|
||||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "cannot delete the last user account"})
|
|
||||||
}
|
}
|
||||||
|
// 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)
|
// Delete user (this will cascade to delete all related data)
|
||||||
err = h.db.DeleteUser(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true})
|
err = h.db.DeleteUser(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true})
|
||||||
if err != nil {
|
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" {
|
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.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.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 {
|
type UpdateScanSettingsRequest struct {
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ templ AdminProfile(user User) {
|
|||||||
|
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h4 class="text-lg font-medium mb-3" style="color: var(--text-primary)">Username</h4>
|
<h4 class="text-lg font-medium mb-3" style="color: var(--text-primary)">Username</h4>
|
||||||
<form hx-put="/api/user/username" hx-target="#username-result" hx-swap="innerHTML" hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}' class="flex space-x-3 max-w-md">
|
<form hx-put="/api/auth/username" hx-target="#username-result" hx-swap="innerHTML" hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}' class="flex space-x-3 max-w-md">
|
||||||
<input type="text" name="username" value={ user.Username } class="flex-1 px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
|
<input type="text" name="username" value={ user.Username } class="flex-1 px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
|
||||||
<button type="submit" class="btn-primary px-4 py-2 rounded">Update</button>
|
<button type="submit" class="btn-primary px-4 py-2 rounded">Update</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -105,7 +105,7 @@ templ AdminProfile(user User) {
|
|||||||
|
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h4 class="text-lg font-medium mb-3" style="color: var(--text-primary)">Email Address</h4>
|
<h4 class="text-lg font-medium mb-3" style="color: var(--text-primary)">Email Address</h4>
|
||||||
<form hx-put="/api/user/email" hx-target="#email-result" hx-swap="innerHTML" hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}' class="flex space-x-3 max-w-md">
|
<form hx-put="/api/auth/email" hx-target="#email-result" hx-swap="innerHTML" hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}' class="flex space-x-3 max-w-md">
|
||||||
<input type="email" name="email" value={ user.Email } class="flex-1 px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
|
<input type="email" name="email" value={ user.Email } class="flex-1 px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
|
||||||
<button type="submit" class="btn-primary px-4 py-2 rounded">Update</button>
|
<button type="submit" class="btn-primary px-4 py-2 rounded">Update</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -114,7 +114,7 @@ templ AdminProfile(user User) {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-lg font-medium mb-3" style="color: var(--text-primary)">Change Password</h4>
|
<h4 class="text-lg font-medium mb-3" style="color: var(--text-primary)">Change Password</h4>
|
||||||
<form hx-put="/api/user/password" hx-target="#password-result" hx-swap="innerHTML" hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}' class="space-y-4 max-w-md">
|
<form hx-put="/api/auth/password" hx-target="#password-result" hx-swap="innerHTML" hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}' class="space-y-4 max-w-md">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Current Password</label>
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Current Password</label>
|
||||||
<input type="password" name="current_password" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
|
<input type="password" name="current_password" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
|
||||||
|
|||||||
Reference in New Issue
Block a user