fix admin panel
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.db
|
||||||
685
internal/server/admin_handlers.go
Normal file
685
internal/server/admin_handlers.go
Normal file
@@ -0,0 +1,685 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -523,6 +523,7 @@ func (s *Server) RegisterRoutes(mux *http.ServeMux) {
|
|||||||
mux.HandleFunc("/admin/admins", s.handleAdminGetAdmins)
|
mux.HandleFunc("/admin/admins", s.handleAdminGetAdmins)
|
||||||
mux.HandleFunc("/admin/admin/create", s.handleAdminCreateAdmin)
|
mux.HandleFunc("/admin/admin/create", s.handleAdminCreateAdmin)
|
||||||
mux.HandleFunc("/admin/admin/delete", s.handleAdminDeleteAdmin)
|
mux.HandleFunc("/admin/admin/delete", s.handleAdminDeleteAdmin)
|
||||||
|
mux.HandleFunc("/admin/admin/password", s.handleAdminChangePassword)
|
||||||
|
|
||||||
// Admin UI (static files served from /admin/)
|
// Admin UI (static files served from /admin/)
|
||||||
mux.HandleFunc("/admin/", s.handleAdminUI)
|
mux.HandleFunc("/admin/", s.handleAdminUI)
|
||||||
|
|||||||
Reference in New Issue
Block a user