package dashboard import ( "encoding/json" "net/http" "strconv" "strings" "time" ) // UserAPI handles user management endpoints type UserAPI struct { authManager *AuthManager securityManager *SecurityManager } // NewUserAPI creates a new user API instance func NewUserAPI(authManager *AuthManager, securityManager *SecurityManager) *UserAPI { return &UserAPI{ authManager: authManager, securityManager: securityManager, } } // RegisterUserRoutes registers all user management routes func (ua *UserAPI) RegisterUserRoutes(mux *http.ServeMux, sm *SecurityManager) { // Authentication routes (no auth required) mux.HandleFunc("/login", sm.LoginHandler) mux.HandleFunc("/logout", sm.LogoutHandler) mux.HandleFunc("/api/auth/login", ua.handleAPILogin) // User management routes (require authentication) mux.HandleFunc("/api/users", sm.APIAuthMiddleware(ua.handleUsers)) mux.HandleFunc("/api/users/", sm.APIAuthMiddleware(ua.handleUser)) mux.HandleFunc("/api/users/me", sm.APIAuthMiddleware(ua.handleCurrentUser)) mux.HandleFunc("/api/users/me/password", sm.APIAuthMiddleware(sm.CSRFMiddleware(ua.handleChangePassword))) // API key management routes mux.HandleFunc("/api/apikeys", sm.APIAuthMiddleware(ua.handleAPIKeys)) mux.HandleFunc("/api/apikeys/", sm.APIAuthMiddleware(ua.handleAPIKey)) // User management page will be handled in web.go } // handleAPILogin handles API-based login func (ua *UserAPI) handleAPILogin(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { ua.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed) return } var loginReq struct { Username string `json:"username"` Password string `json:"password"` } if err := json.NewDecoder(r.Body).Decode(&loginReq); err != nil { ua.sendJSONError(w, "Invalid JSON", http.StatusBadRequest) return } // Validate input if err := ua.securityManager.ValidateInput().ValidateUsername(loginReq.Username); err != nil { ua.sendJSONError(w, err.Error(), http.StatusBadRequest) return } // Authenticate user user, err := ua.authManager.AuthenticateUser(loginReq.Username, loginReq.Password) if err != nil { ua.sendJSONError(w, "Invalid credentials", http.StatusUnauthorized) return } // Create session session, err := ua.authManager.CreateSession(user.ID, r.RemoteAddr, r.UserAgent()) if err != nil { ua.sendJSONError(w, "Failed to create session", http.StatusInternalServerError) return } // Set session cookie http.SetCookie(w, &http.Cookie{ Name: "session_token", Value: session.Token, Path: "/", Expires: session.ExpiresAt, HttpOnly: true, Secure: r.TLS != nil, SameSite: http.SameSiteStrictMode, }) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "user": map[string]interface{}{ "id": user.ID, "username": user.Username, "email": user.Email, "role": user.Role, }, }) } // handleUsers handles GET/POST /api/users func (ua *UserAPI) handleUsers(w http.ResponseWriter, r *http.Request) { user := ua.securityManager.GetUserFromContext(r.Context()) if user == nil { ua.sendJSONError(w, "Unauthorized", http.StatusUnauthorized) return } switch r.Method { case "GET": ua.handleGetUsers(w, r, user) case "POST": ua.handleCreateUser(w, r, user) default: ua.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed) } } // handleGetUsers retrieves all users func (ua *UserAPI) handleGetUsers(w http.ResponseWriter, r *http.Request, currentUser *User) { // Only admins can list all users if currentUser.Role != "admin" { ua.sendJSONError(w, "Insufficient permissions", http.StatusForbidden) return } users, err := ua.authManager.GetUsers() if err != nil { ua.sendJSONError(w, "Failed to retrieve users", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "users": users, "count": len(users), }) } // handleCreateUser creates a new user func (ua *UserAPI) handleCreateUser(w http.ResponseWriter, r *http.Request, currentUser *User) { // Only admins can create users if currentUser.Role != "admin" { ua.sendJSONError(w, "Insufficient permissions", http.StatusForbidden) return } var createReq struct { Username string `json:"username"` Password string `json:"password"` Email string `json:"email"` Role string `json:"role"` } if err := json.NewDecoder(r.Body).Decode(&createReq); err != nil { ua.sendJSONError(w, "Invalid JSON", http.StatusBadRequest) return } // Validate input validator := ua.securityManager.ValidateInput() if err := validator.ValidateUsername(createReq.Username); err != nil { ua.sendJSONError(w, err.Error(), http.StatusBadRequest) return } if err := validator.ValidatePassword(createReq.Password); err != nil { ua.sendJSONError(w, err.Error(), http.StatusBadRequest) return } if err := validator.ValidateEmail(createReq.Email); err != nil { ua.sendJSONError(w, err.Error(), http.StatusBadRequest) return } if err := validator.ValidateRole(createReq.Role); err != nil { ua.sendJSONError(w, err.Error(), http.StatusBadRequest) return } // Create user user, err := ua.authManager.CreateUser(createReq.Username, createReq.Password, createReq.Email, createReq.Role) if err != nil { if strings.Contains(err.Error(), "UNIQUE constraint failed") { ua.sendJSONError(w, "Username already exists", http.StatusConflict) } else { ua.sendJSONError(w, "Failed to create user", http.StatusInternalServerError) } return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "user": user, }) } // handleUser handles individual user operations func (ua *UserAPI) handleUser(w http.ResponseWriter, r *http.Request) { user := ua.securityManager.GetUserFromContext(r.Context()) if user == nil { ua.sendJSONError(w, "Unauthorized", http.StatusUnauthorized) return } // Extract user ID from URL path := strings.TrimPrefix(r.URL.Path, "/api/users/") userID, err := strconv.Atoi(path) if err != nil { ua.sendJSONError(w, "Invalid user ID", http.StatusBadRequest) return } switch r.Method { case "GET": ua.handleGetUser(w, r, user, userID) case "PUT": ua.handleUpdateUser(w, r, user, userID) case "DELETE": ua.handleDeleteUser(w, r, user, userID) default: ua.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed) } } // handleGetUser retrieves a specific user func (ua *UserAPI) handleGetUser(w http.ResponseWriter, r *http.Request, currentUser *User, userID int) { // Users can only view their own profile, admins can view any if currentUser.Role != "admin" && currentUser.ID != userID { ua.sendJSONError(w, "Insufficient permissions", http.StatusForbidden) return } user, err := ua.authManager.GetUserByID(userID) if err != nil { ua.sendJSONError(w, "User not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(user) } // handleUpdateUser updates a user func (ua *UserAPI) handleUpdateUser(w http.ResponseWriter, r *http.Request, currentUser *User, userID int) { // Users can only update their own profile, admins can update any if currentUser.Role != "admin" && currentUser.ID != userID { ua.sendJSONError(w, "Insufficient permissions", http.StatusForbidden) return } var updateReq struct { Username string `json:"username"` Email string `json:"email"` Role string `json:"role"` Active *bool `json:"active"` } if err := json.NewDecoder(r.Body).Decode(&updateReq); err != nil { ua.sendJSONError(w, "Invalid JSON", http.StatusBadRequest) return } // Get existing user existingUser, err := ua.authManager.GetUserByID(userID) if err != nil { ua.sendJSONError(w, "User not found", http.StatusNotFound) return } // Validate input validator := ua.securityManager.ValidateInput() if updateReq.Username != "" { if err := validator.ValidateUsername(updateReq.Username); err != nil { ua.sendJSONError(w, err.Error(), http.StatusBadRequest) return } } else { updateReq.Username = existingUser.Username } if updateReq.Email != "" { if err := validator.ValidateEmail(updateReq.Email); err != nil { ua.sendJSONError(w, err.Error(), http.StatusBadRequest) return } } else { updateReq.Email = existingUser.Email } // Only admins can change role and active status if currentUser.Role != "admin" { updateReq.Role = existingUser.Role active := existingUser.Active updateReq.Active = &active } else { if updateReq.Role != "" { if err := validator.ValidateRole(updateReq.Role); err != nil { ua.sendJSONError(w, err.Error(), http.StatusBadRequest) return } } else { updateReq.Role = existingUser.Role } if updateReq.Active == nil { active := existingUser.Active updateReq.Active = &active } } // Update user err = ua.authManager.UpdateUser(userID, updateReq.Username, updateReq.Email, updateReq.Role, *updateReq.Active) if err != nil { if strings.Contains(err.Error(), "UNIQUE constraint failed") { ua.sendJSONError(w, "Username already exists", http.StatusConflict) } else { ua.sendJSONError(w, "Failed to update user", http.StatusInternalServerError) } return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"success": "User updated successfully"}) } // handleDeleteUser deletes a user func (ua *UserAPI) handleDeleteUser(w http.ResponseWriter, r *http.Request, currentUser *User, userID int) { // Only admins can delete users if currentUser.Role != "admin" { ua.sendJSONError(w, "Insufficient permissions", http.StatusForbidden) return } // Prevent deleting self if currentUser.ID == userID { ua.sendJSONError(w, "Cannot delete your own account", http.StatusBadRequest) return } err := ua.authManager.DeleteUser(userID) if err != nil { ua.sendJSONError(w, "Failed to delete user", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"success": "User deleted successfully"}) } // handleCurrentUser returns current user info func (ua *UserAPI) handleCurrentUser(w http.ResponseWriter, r *http.Request) { user := ua.securityManager.GetUserFromContext(r.Context()) if user == nil { ua.sendJSONError(w, "Unauthorized", http.StatusUnauthorized) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(user) } // handleChangePassword changes user password func (ua *UserAPI) handleChangePassword(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { ua.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed) return } user := ua.securityManager.GetUserFromContext(r.Context()) if user == nil { ua.sendJSONError(w, "Unauthorized", http.StatusUnauthorized) return } var changeReq struct { CurrentPassword string `json:"current_password"` NewPassword string `json:"new_password"` } if err := json.NewDecoder(r.Body).Decode(&changeReq); err != nil { ua.sendJSONError(w, "Invalid JSON", http.StatusBadRequest) return } // Verify current password _, err := ua.authManager.AuthenticateUser(user.Username, changeReq.CurrentPassword) if err != nil { ua.sendJSONError(w, "Current password is incorrect", http.StatusBadRequest) return } // Validate new password if err := ua.securityManager.ValidateInput().ValidatePassword(changeReq.NewPassword); err != nil { ua.sendJSONError(w, err.Error(), http.StatusBadRequest) return } // Update password err = ua.authManager.UpdateUserPassword(user.ID, changeReq.NewPassword) if err != nil { ua.sendJSONError(w, "Failed to update password", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"success": "Password updated successfully"}) } // handleAPIKeys handles API key management func (ua *UserAPI) handleAPIKeys(w http.ResponseWriter, r *http.Request) { user := ua.securityManager.GetUserFromContext(r.Context()) if user == nil { ua.sendJSONError(w, "Unauthorized", http.StatusUnauthorized) return } switch r.Method { case "GET": ua.handleGetAPIKeys(w, r, user) case "POST": ua.handleCreateAPIKey(w, r, user) default: ua.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed) } } // handleGetAPIKeys retrieves API keys for current user func (ua *UserAPI) handleGetAPIKeys(w http.ResponseWriter, r *http.Request, user *User) { keys, err := ua.authManager.GetAPIKeys(user.ID) if err != nil { ua.sendJSONError(w, "Failed to retrieve API keys", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "api_keys": keys, "count": len(keys), }) } // handleCreateAPIKey creates a new API key func (ua *UserAPI) handleCreateAPIKey(w http.ResponseWriter, r *http.Request, user *User) { var createReq struct { Name string `json:"name"` Permissions []string `json:"permissions"` ExpiresIn *int `json:"expires_in"` // Days from now } if err := json.NewDecoder(r.Body).Decode(&createReq); err != nil { ua.sendJSONError(w, "Invalid JSON", http.StatusBadRequest) return } // Validate input if err := ua.securityManager.ValidateInput().ValidateAPIKeyName(createReq.Name); err != nil { ua.sendJSONError(w, err.Error(), http.StatusBadRequest) return } var expiresAt *time.Time if createReq.ExpiresIn != nil && *createReq.ExpiresIn > 0 { expiry := time.Now().AddDate(0, 0, *createReq.ExpiresIn) expiresAt = &expiry } // Create API key apiKey, err := ua.authManager.CreateAPIKey(createReq.Name, user.ID, createReq.Permissions, expiresAt) if err != nil { ua.sendJSONError(w, "Failed to create API key", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "api_key": apiKey, "warning": "Save this key securely. It will not be shown again.", }) } // handleAPIKey handles individual API key operations func (ua *UserAPI) handleAPIKey(w http.ResponseWriter, r *http.Request) { user := ua.securityManager.GetUserFromContext(r.Context()) if user == nil { ua.sendJSONError(w, "Unauthorized", http.StatusUnauthorized) return } // Extract API key ID from URL path := strings.TrimPrefix(r.URL.Path, "/api/apikeys/") keyID, err := strconv.Atoi(path) if err != nil { ua.sendJSONError(w, "Invalid API key ID", http.StatusBadRequest) return } switch r.Method { case "DELETE": ua.handleRevokeAPIKey(w, r, user, keyID) default: ua.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed) } } // handleRevokeAPIKey revokes an API key func (ua *UserAPI) handleRevokeAPIKey(w http.ResponseWriter, r *http.Request, user *User, keyID int) { // Users can only revoke their own API keys, admins can revoke any if user.Role != "admin" { // Check if the API key belongs to the current user keys, err := ua.authManager.GetAPIKeys(user.ID) if err != nil { ua.sendJSONError(w, "Failed to verify API key ownership", http.StatusInternalServerError) return } found := false for _, key := range keys { if key.ID == keyID { found = true break } } if !found { ua.sendJSONError(w, "API key not found", http.StatusNotFound) return } } err := ua.authManager.RevokeAPIKey(keyID) if err != nil { ua.sendJSONError(w, "Failed to revoke API key", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"success": "API key revoked successfully"}) } // sendJSONError sends a JSON error response func (ua *UserAPI) sendJSONError(w http.ResponseWriter, message string, statusCode int) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) json.NewEncoder(w).Encode(map[string]string{"error": message}) }