package server import ( "context" "crypto/rand" "encoding/hex" "encoding/json" "fmt" "net/http" "time" ) // Admin authentication and management handlers // generateToken generates a secure random token func generateToken() (string, error) { bytes := make([]byte, 32) if _, err := rand.Read(bytes); err != nil { return "", err } return hex.EncodeToString(bytes), nil } // AdminLoginRequest represents a login request type AdminLoginRequest struct { Username string `json:"username"` Password string `json:"password"` } // AdminLoginResponse represents a login response type AdminLoginResponse struct { Success bool `json:"success"` Token string `json:"token,omitempty"` Message string `json:"message,omitempty"` } // handleAdminLogin handles admin login func (s *Server) handleAdminLogin(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req AdminLoginRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } admin, err := s.db.GetAdminByUsername(req.Username) if err != nil { http.Error(w, "Database error", http.StatusInternalServerError) return } if admin == nil { json.NewEncoder(w).Encode(AdminLoginResponse{ Success: false, Message: "Invalid credentials", }) return } // Verify password if admin.PasswordHash != hashAPIKey(req.Password) { json.NewEncoder(w).Encode(AdminLoginResponse{ Success: false, Message: "Invalid credentials", }) return } // Generate session token token, err := generateToken() if err != nil { http.Error(w, "Failed to generate token", http.StatusInternalServerError) return } // Create session (valid for 24 hours) expiresAt := time.Now().Add(24 * time.Hour) if err := s.db.CreateSession(admin.ID, token, expiresAt); err != nil { http.Error(w, "Failed to create session", http.StatusInternalServerError) return } // Set cookie http.SetCookie(w, &http.Cookie{ Name: "admin_token", Value: token, Path: "/", Expires: expiresAt, HttpOnly: true, SameSite: http.SameSiteStrictMode, }) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(AdminLoginResponse{ Success: true, Token: token, Message: "Login successful", }) } // handleAdminLogout handles admin logout func (s *Server) handleAdminLogout(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("admin_token") if err == nil { s.db.DeleteSession(cookie.Value) } http.SetCookie(w, &http.Cookie{ Name: "admin_token", Value: "", Path: "/", MaxAge: -1, HttpOnly: true, }) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": "Logged out successfully", }) } // authenticateAdmin checks if the request has a valid admin session func (s *Server) authenticateAdmin(r *http.Request) (*Admin, error) { cookie, err := r.Cookie("admin_token") if err != nil { return nil, err } session, err := s.db.GetSessionByToken(cookie.Value) if err != nil { return nil, err } if session == nil { return nil, nil } if session.ExpiresAt.Before(time.Now()) { s.db.DeleteSession(cookie.Value) return nil, nil } admin, err := s.db.GetAdminByID(session.AdminID) if err != nil { return nil, err } return admin, nil } // handleAdminCheck checks if admin is authenticated func (s *Server) handleAdminCheck(w http.ResponseWriter, r *http.Request) { admin, err := s.authenticateAdmin(r) if err != nil || admin == nil { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "authenticated": false, }) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "authenticated": true, "username": admin.Username, "role": admin.Role, }) } // Client management handlers // handleAdminGetClients returns all clients func (s *Server) handleAdminGetClients(w http.ResponseWriter, r *http.Request) { admin, err := s.authenticateAdmin(r) if err != nil || admin == nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } clients, err := s.db.GetAllClients() if err != nil { http.Error(w, "Database error", http.StatusInternalServerError) return } // Add usage info for each client type ClientWithUsage struct { *ClientConfig CurrentUsage int64 `json:"current_usage"` SnapshotCount int `json:"snapshot_count"` } var result []ClientWithUsage for _, c := range clients { usage, _ := s.db.GetClientUsage(c.ClientID) snapshots, _ := s.db.GetSnapshotsByClient(c.ClientID) result = append(result, ClientWithUsage{ ClientConfig: c, CurrentUsage: usage, SnapshotCount: len(snapshots), }) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(result) } // handleAdminGetClient returns a specific client func (s *Server) handleAdminGetClient(w http.ResponseWriter, r *http.Request) { admin, err := s.authenticateAdmin(r) if err != nil || admin == nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } clientID := r.URL.Query().Get("client_id") if clientID == "" { http.Error(w, "client_id required", http.StatusBadRequest) return } client, err := s.db.GetClient(clientID) if err != nil { http.Error(w, "Database error", http.StatusInternalServerError) return } if client == nil { http.Error(w, "Client not found", http.StatusNotFound) return } usage, _ := s.db.GetClientUsage(clientID) snapshots, _ := s.db.GetSnapshotsByClient(clientID) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "client": client, "current_usage": usage, "snapshot_count": len(snapshots), }) } // handleAdminCreateClient creates a new client func (s *Server) handleAdminCreateClient(w http.ResponseWriter, r *http.Request) { admin, err := s.authenticateAdmin(r) if err != nil || admin == nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req struct { ClientID string `json:"client_id"` APIKey string `json:"api_key"` MaxSizeBytes int64 `json:"max_size_bytes"` Dataset string `json:"dataset"` StorageType string `json:"storage_type"` Enabled bool `json:"enabled"` RotationPolicy *RotationPolicy `json:"rotation_policy"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.ClientID == "" || req.APIKey == "" { http.Error(w, "client_id and api_key required", http.StatusBadRequest) return } if req.StorageType == "" { req.StorageType = "s3" } client := &ClientConfig{ ClientID: req.ClientID, APIKey: hashAPIKey(req.APIKey), MaxSizeBytes: req.MaxSizeBytes, Dataset: req.Dataset, StorageType: req.StorageType, Enabled: req.Enabled, RotationPolicy: req.RotationPolicy, } if err := s.db.SaveClient(client); err != nil { http.Error(w, "Failed to create client", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": "Client created successfully", }) } // handleAdminUpdateClient updates an existing client func (s *Server) handleAdminUpdateClient(w http.ResponseWriter, r *http.Request) { admin, err := s.authenticateAdmin(r) if err != nil || admin == nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } if r.Method != http.MethodPut && r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req struct { ClientID string `json:"client_id"` APIKey string `json:"api_key"` MaxSizeBytes int64 `json:"max_size_bytes"` Dataset string `json:"dataset"` StorageType string `json:"storage_type"` Enabled bool `json:"enabled"` RotationPolicy *RotationPolicy `json:"rotation_policy"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.ClientID == "" { http.Error(w, "client_id required", http.StatusBadRequest) return } // Get existing client existing, err := s.db.GetClient(req.ClientID) if err != nil { http.Error(w, "Database error", http.StatusInternalServerError) return } if existing == nil { http.Error(w, "Client not found", http.StatusNotFound) return } // Update fields if req.APIKey != "" { existing.APIKey = hashAPIKey(req.APIKey) } if req.MaxSizeBytes > 0 { existing.MaxSizeBytes = req.MaxSizeBytes } if req.Dataset != "" { existing.Dataset = req.Dataset } if req.StorageType != "" { existing.StorageType = req.StorageType } existing.Enabled = req.Enabled if req.RotationPolicy != nil { existing.RotationPolicy = req.RotationPolicy } if err := s.db.SaveClient(existing); err != nil { http.Error(w, "Failed to update client", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": "Client updated successfully", }) } // handleAdminDeleteClient deletes a client func (s *Server) handleAdminDeleteClient(w http.ResponseWriter, r *http.Request) { admin, err := s.authenticateAdmin(r) if err != nil || admin == nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } if r.Method != http.MethodDelete && r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } clientID := r.URL.Query().Get("client_id") if clientID == "" { http.Error(w, "client_id required", http.StatusBadRequest) return } // Delete snapshots first (handled by foreign key cascade) // Get snapshots to delete from storage snapshots, _ := s.db.GetSnapshotsByClient(clientID) for _, snap := range snapshots { if snap.StorageType == "s3" && s.s3Backend != nil { s.s3Backend.Delete(context.Background(), snap.StorageKey) } } if err := s.db.DeleteClient(clientID); err != nil { http.Error(w, "Failed to delete client", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": "Client deleted successfully", }) } // handleAdminGetSnapshots returns all snapshots for a client func (s *Server) handleAdminGetSnapshots(w http.ResponseWriter, r *http.Request) { admin, err := s.authenticateAdmin(r) if err != nil || admin == nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } clientID := r.URL.Query().Get("client_id") var snapshots []*SnapshotMetadata if clientID != "" { snapshots, err = s.db.GetSnapshotsByClient(clientID) } else { snapshots, err = s.db.GetAllSnapshots() } if err != nil { http.Error(w, "Database error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(snapshots) } // handleAdminDeleteSnapshot deletes a specific snapshot func (s *Server) handleAdminDeleteSnapshot(w http.ResponseWriter, r *http.Request) { admin, err := s.authenticateAdmin(r) if err != nil || admin == nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } if r.Method != http.MethodDelete && r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } clientID := r.URL.Query().Get("client_id") snapshotID := r.URL.Query().Get("snapshot_id") if clientID == "" || snapshotID == "" { http.Error(w, "client_id and snapshot_id required", http.StatusBadRequest) return } // Get snapshot to delete from storage snap, err := s.db.GetSnapshotByID(clientID, snapshotID) if err != nil { http.Error(w, "Database error", http.StatusInternalServerError) return } if snap != nil { if snap.StorageType == "s3" && s.s3Backend != nil { s.s3Backend.Delete(context.Background(), snap.StorageKey) } } if err := s.db.DeleteSnapshot(clientID, snapshotID); err != nil { http.Error(w, "Failed to delete snapshot", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": "Snapshot deleted successfully", }) } // handleAdminGetStats returns server statistics func (s *Server) handleAdminGetStats(w http.ResponseWriter, r *http.Request) { admin, err := s.authenticateAdmin(r) if err != nil || admin == nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } clients, _ := s.db.GetAllClients() totalSnapshots, _ := s.db.GetTotalSnapshotCount() totalStorage, _ := s.db.GetTotalStorageUsed() w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "client_count": len(clients), "total_snapshots": totalSnapshots, "total_storage": totalStorage, "total_storage_gb": float64(totalStorage) / (1024 * 1024 * 1024), }) } // Admin management handlers // handleAdminGetAdmins returns all admins func (s *Server) handleAdminGetAdmins(w http.ResponseWriter, r *http.Request) { admin, err := s.authenticateAdmin(r) if err != nil || admin == nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } admins, err := s.db.GetAllAdmins() if err != nil { http.Error(w, "Database error", http.StatusInternalServerError) return } // Remove password hashes from response type AdminResponse struct { ID int `json:"id"` Username string `json:"username"` Role string `json:"role"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } var result []AdminResponse for _, a := range admins { result = append(result, AdminResponse{ ID: a.ID, Username: a.Username, Role: a.Role, CreatedAt: a.CreatedAt, UpdatedAt: a.UpdatedAt, }) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(result) } // handleAdminCreateAdmin creates a new admin func (s *Server) handleAdminCreateAdmin(w http.ResponseWriter, r *http.Request) { admin, err := s.authenticateAdmin(r) if err != nil || admin == nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req struct { Username string `json:"username"` Password string `json:"password"` Role string `json:"role"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.Username == "" || req.Password == "" { http.Error(w, "username and password required", http.StatusBadRequest) return } if req.Role == "" { req.Role = "admin" } if err := s.db.CreateAdmin(req.Username, hashAPIKey(req.Password), req.Role); err != nil { http.Error(w, "Failed to create admin (username may already exist)", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": "Admin created successfully", }) } // handleAdminDeleteAdmin deletes an admin func (s *Server) handleAdminDeleteAdmin(w http.ResponseWriter, r *http.Request) { admin, err := s.authenticateAdmin(r) if err != nil || admin == nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } if r.Method != http.MethodDelete && r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } id := r.URL.Query().Get("id") if id == "" { http.Error(w, "id required", http.StatusBadRequest) return } // Prevent deleting yourself var targetAdminID int fmt.Sscanf(id, "%d", &targetAdminID) if admin != nil && targetAdminID == admin.ID { http.Error(w, "Cannot delete yourself", http.StatusBadRequest) return } if err := s.db.DeleteAdmin(targetAdminID); err != nil { http.Error(w, "Failed to delete admin", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": "Admin deleted successfully", }) } // handleAdminChangePassword handles admin password change func (s *Server) handleAdminChangePassword(w http.ResponseWriter, r *http.Request) { admin, err := s.authenticateAdmin(r) if err != nil || admin == nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req struct { ID int `json:"id"` Password string `json:"password"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.Password == "" { http.Error(w, "Password required", http.StatusBadRequest) return } if err := s.db.UpdateAdminPassword(req.ID, hashAPIKey(req.Password)); err != nil { http.Error(w, "Failed to update password", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": "Password changed successfully", }) } // handleAdminUI serves the admin panel UI func (s *Server) handleAdminUI(w http.ResponseWriter, r *http.Request) { // Serve the embedded admin UI HTML w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(adminPanelHTML)) }