Files
honeydany/app/dashboard/user_api.go
T

554 lines
16 KiB
Go

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})
}