554 lines
16 KiB
Go
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})
|
|
}
|