All checks were successful
CI/CD / Test (push) Successful in 31s
CI/CD / Lint (push) Successful in 42s
CI/CD / Generate SBOM (push) Successful in 17s
CI/CD / Build (darwin-amd64) (push) Successful in 22s
CI/CD / Build (linux-amd64) (push) Successful in 22s
CI/CD / Build (darwin-arm64) (push) Successful in 23s
CI/CD / Build (linux-arm64) (push) Successful in 22s
CI/CD / Build & Push Docker Image (push) Successful in 22s
CI/CD / Mirror to GitHub (push) Successful in 16s
CI/CD / Release (push) Has been skipped
New features in v3.3.0: - audit.go: Security audit logging with JSON/text format, log rotation - validation.go: Magic bytes content validation with wildcard patterns - quota.go: Per-user storage quotas with Redis/memory tracking - admin.go: Admin API for stats, file management, user quotas, bans Integration: - Updated main.go with feature initialization and handler integration - Added audit logging for auth success/failure, uploads, downloads - Added quota checking before upload, tracking after successful upload - Added content validation with magic bytes detection Config: - New template: config-enhanced-features.toml with all new options - Updated README.md with feature documentation
757 lines
19 KiB
Go
757 lines
19 KiB
Go
// admin.go - Admin API for operations and monitoring
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/fs"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// AdminConfig holds admin API configuration
|
|
type AdminConfig struct {
|
|
Enabled bool `toml:"enabled" mapstructure:"enabled"`
|
|
Bind string `toml:"bind" mapstructure:"bind"` // Separate bind address (e.g., "127.0.0.1:8081")
|
|
PathPrefix string `toml:"path_prefix" mapstructure:"path_prefix"` // Path prefix (e.g., "/admin")
|
|
Auth AdminAuthConfig `toml:"auth" mapstructure:"auth"`
|
|
}
|
|
|
|
// AdminAuthConfig holds admin authentication configuration
|
|
type AdminAuthConfig struct {
|
|
Type string `toml:"type" mapstructure:"type"` // "bearer" | "basic"
|
|
Token string `toml:"token" mapstructure:"token"` // For bearer auth
|
|
Username string `toml:"username" mapstructure:"username"` // For basic auth
|
|
Password string `toml:"password" mapstructure:"password"` // For basic auth
|
|
}
|
|
|
|
// AdminStats represents system statistics
|
|
type AdminStats struct {
|
|
Storage StorageStats `json:"storage"`
|
|
Users UserStats `json:"users"`
|
|
Requests RequestStats `json:"requests"`
|
|
System SystemStats `json:"system"`
|
|
}
|
|
|
|
// StorageStats represents storage statistics
|
|
type StorageStats struct {
|
|
UsedBytes int64 `json:"used_bytes"`
|
|
UsedHuman string `json:"used_human"`
|
|
FileCount int64 `json:"file_count"`
|
|
FreeBytes int64 `json:"free_bytes,omitempty"`
|
|
FreeHuman string `json:"free_human,omitempty"`
|
|
TotalBytes int64 `json:"total_bytes,omitempty"`
|
|
TotalHuman string `json:"total_human,omitempty"`
|
|
}
|
|
|
|
// UserStats represents user statistics
|
|
type UserStats struct {
|
|
Total int64 `json:"total"`
|
|
Active24h int64 `json:"active_24h"`
|
|
Active7d int64 `json:"active_7d"`
|
|
}
|
|
|
|
// RequestStats represents request statistics
|
|
type RequestStats struct {
|
|
Uploads24h int64 `json:"uploads_24h"`
|
|
Downloads24h int64 `json:"downloads_24h"`
|
|
Errors24h int64 `json:"errors_24h"`
|
|
}
|
|
|
|
// SystemStats represents system statistics
|
|
type SystemStats struct {
|
|
Uptime string `json:"uptime"`
|
|
Version string `json:"version"`
|
|
GoVersion string `json:"go_version"`
|
|
NumGoroutines int `json:"num_goroutines"`
|
|
MemoryUsageMB int64 `json:"memory_usage_mb"`
|
|
NumCPU int `json:"num_cpu"`
|
|
}
|
|
|
|
// FileInfo represents file information for admin API
|
|
type FileInfo struct {
|
|
ID string `json:"id"`
|
|
Path string `json:"path"`
|
|
Name string `json:"name"`
|
|
Size int64 `json:"size"`
|
|
SizeHuman string `json:"size_human"`
|
|
ContentType string `json:"content_type"`
|
|
ModTime time.Time `json:"mod_time"`
|
|
Owner string `json:"owner,omitempty"`
|
|
}
|
|
|
|
// FileListResponse represents paginated file list
|
|
type FileListResponse struct {
|
|
Files []FileInfo `json:"files"`
|
|
Total int64 `json:"total"`
|
|
Page int `json:"page"`
|
|
Limit int `json:"limit"`
|
|
TotalPages int `json:"total_pages"`
|
|
}
|
|
|
|
// UserInfo represents user information for admin API
|
|
type UserInfo struct {
|
|
JID string `json:"jid"`
|
|
QuotaUsed int64 `json:"quota_used"`
|
|
QuotaLimit int64 `json:"quota_limit"`
|
|
FileCount int64 `json:"file_count"`
|
|
LastActive time.Time `json:"last_active,omitempty"`
|
|
IsBanned bool `json:"is_banned"`
|
|
}
|
|
|
|
// BanInfo represents ban information
|
|
type BanInfo struct {
|
|
IP string `json:"ip"`
|
|
Reason string `json:"reason"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
ExpiresAt time.Time `json:"expires_at,omitempty"`
|
|
IsPermanent bool `json:"is_permanent"`
|
|
}
|
|
|
|
var (
|
|
serverStartTime = time.Now()
|
|
adminConfig *AdminConfig
|
|
)
|
|
|
|
// SetupAdminRoutes sets up admin API routes
|
|
func SetupAdminRoutes(mux *http.ServeMux, config *AdminConfig) {
|
|
adminConfig = config
|
|
|
|
if !config.Enabled {
|
|
log.Info("Admin API is disabled")
|
|
return
|
|
}
|
|
|
|
prefix := config.PathPrefix
|
|
if prefix == "" {
|
|
prefix = "/admin"
|
|
}
|
|
|
|
// Wrap all admin handlers with authentication
|
|
adminMux := http.NewServeMux()
|
|
|
|
adminMux.HandleFunc(prefix+"/stats", handleAdminStats)
|
|
adminMux.HandleFunc(prefix+"/files", handleAdminFiles)
|
|
adminMux.HandleFunc(prefix+"/files/", handleAdminFileByID)
|
|
adminMux.HandleFunc(prefix+"/users", handleAdminUsers)
|
|
adminMux.HandleFunc(prefix+"/users/", handleAdminUserByJID)
|
|
adminMux.HandleFunc(prefix+"/bans", handleAdminBans)
|
|
adminMux.HandleFunc(prefix+"/bans/", handleAdminBanByIP)
|
|
adminMux.HandleFunc(prefix+"/health", handleAdminHealth)
|
|
adminMux.HandleFunc(prefix+"/config", handleAdminConfig)
|
|
|
|
// Register with authentication middleware
|
|
mux.Handle(prefix+"/", AdminAuthMiddleware(adminMux))
|
|
|
|
log.Infof("Admin API enabled at %s (auth: %s)", prefix, config.Auth.Type)
|
|
}
|
|
|
|
// AdminAuthMiddleware handles admin authentication
|
|
func AdminAuthMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if adminConfig == nil || !adminConfig.Enabled {
|
|
http.Error(w, "Admin API disabled", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
authorized := false
|
|
|
|
switch adminConfig.Auth.Type {
|
|
case "bearer":
|
|
auth := r.Header.Get("Authorization")
|
|
if strings.HasPrefix(auth, "Bearer ") {
|
|
token := strings.TrimPrefix(auth, "Bearer ")
|
|
authorized = token == adminConfig.Auth.Token
|
|
}
|
|
case "basic":
|
|
username, password, ok := r.BasicAuth()
|
|
if ok {
|
|
authorized = username == adminConfig.Auth.Username &&
|
|
password == adminConfig.Auth.Password
|
|
}
|
|
default:
|
|
// No auth configured, check if request is from localhost
|
|
clientIP := getClientIP(r)
|
|
authorized = clientIP == "127.0.0.1" || clientIP == "::1"
|
|
}
|
|
|
|
if !authorized {
|
|
AuditEvent("admin_auth_failure", r, nil)
|
|
w.Header().Set("WWW-Authenticate", `Bearer realm="admin"`)
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// handleAdminStats returns system statistics
|
|
func handleAdminStats(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
AuditAdminAction(r, "get_stats", "system", nil)
|
|
|
|
ctx := r.Context()
|
|
stats := AdminStats{}
|
|
|
|
// Storage stats
|
|
confMutex.RLock()
|
|
storagePath := conf.Server.StoragePath
|
|
confMutex.RUnlock()
|
|
|
|
storageStats := calculateStorageStats(storagePath)
|
|
stats.Storage = storageStats
|
|
|
|
// User stats
|
|
stats.Users = calculateUserStats(ctx)
|
|
|
|
// Request stats from Prometheus metrics
|
|
stats.Requests = calculateRequestStats()
|
|
|
|
// System stats
|
|
var mem runtime.MemStats
|
|
runtime.ReadMemStats(&mem)
|
|
|
|
stats.System = SystemStats{
|
|
Uptime: time.Since(serverStartTime).Round(time.Second).String(),
|
|
Version: "3.3.0",
|
|
GoVersion: runtime.Version(),
|
|
NumGoroutines: runtime.NumGoroutine(),
|
|
MemoryUsageMB: int64(mem.Alloc / 1024 / 1024),
|
|
NumCPU: runtime.NumCPU(),
|
|
}
|
|
|
|
writeJSONResponseAdmin(w, stats)
|
|
}
|
|
|
|
// handleAdminFiles handles file listing
|
|
func handleAdminFiles(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
listFiles(w, r)
|
|
default:
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
// listFiles returns paginated file list
|
|
func listFiles(w http.ResponseWriter, r *http.Request) {
|
|
AuditAdminAction(r, "list_files", "files", nil)
|
|
|
|
// Parse query parameters
|
|
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
|
if limit < 1 || limit > 100 {
|
|
limit = 50
|
|
}
|
|
sortBy := r.URL.Query().Get("sort")
|
|
if sortBy == "" {
|
|
sortBy = "date"
|
|
}
|
|
filterOwner := r.URL.Query().Get("owner")
|
|
|
|
confMutex.RLock()
|
|
storagePath := conf.Server.StoragePath
|
|
confMutex.RUnlock()
|
|
|
|
var files []FileInfo
|
|
|
|
err := filepath.WalkDir(storagePath, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return nil // Skip errors
|
|
}
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
info, err := d.Info()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
relPath, _ := filepath.Rel(storagePath, path)
|
|
|
|
fileInfo := FileInfo{
|
|
ID: relPath,
|
|
Path: relPath,
|
|
Name: filepath.Base(path),
|
|
Size: info.Size(),
|
|
SizeHuman: formatBytes(info.Size()),
|
|
ContentType: GetContentType(path),
|
|
ModTime: info.ModTime(),
|
|
}
|
|
|
|
// Apply owner filter if specified (simplified: would need metadata lookup)
|
|
_ = filterOwner // Unused for now, but kept for future implementation
|
|
|
|
files = append(files, fileInfo)
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("Error listing files: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Sort files
|
|
switch sortBy {
|
|
case "date":
|
|
sort.Slice(files, func(i, j int) bool {
|
|
return files[i].ModTime.After(files[j].ModTime)
|
|
})
|
|
case "size":
|
|
sort.Slice(files, func(i, j int) bool {
|
|
return files[i].Size > files[j].Size
|
|
})
|
|
case "name":
|
|
sort.Slice(files, func(i, j int) bool {
|
|
return files[i].Name < files[j].Name
|
|
})
|
|
}
|
|
|
|
// Paginate
|
|
total := len(files)
|
|
start := (page - 1) * limit
|
|
end := start + limit
|
|
if start > total {
|
|
start = total
|
|
}
|
|
if end > total {
|
|
end = total
|
|
}
|
|
|
|
response := FileListResponse{
|
|
Files: files[start:end],
|
|
Total: int64(total),
|
|
Page: page,
|
|
Limit: limit,
|
|
TotalPages: (total + limit - 1) / limit,
|
|
}
|
|
|
|
writeJSONResponseAdmin(w, response)
|
|
}
|
|
|
|
// handleAdminFileByID handles single file operations
|
|
func handleAdminFileByID(w http.ResponseWriter, r *http.Request) {
|
|
// Extract file ID from path
|
|
prefix := adminConfig.PathPrefix
|
|
if prefix == "" {
|
|
prefix = "/admin"
|
|
}
|
|
fileID := strings.TrimPrefix(r.URL.Path, prefix+"/files/")
|
|
|
|
if fileID == "" {
|
|
http.Error(w, "File ID required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
getFileInfo(w, r, fileID)
|
|
case http.MethodDelete:
|
|
deleteFile(w, r, fileID)
|
|
default:
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
// getFileInfo returns information about a specific file
|
|
func getFileInfo(w http.ResponseWriter, r *http.Request, fileID string) {
|
|
confMutex.RLock()
|
|
storagePath := conf.Server.StoragePath
|
|
confMutex.RUnlock()
|
|
|
|
filePath := filepath.Join(storagePath, fileID)
|
|
|
|
// Validate path is within storage
|
|
absPath, err := filepath.Abs(filePath)
|
|
if err != nil || !strings.HasPrefix(absPath, storagePath) {
|
|
http.Error(w, "Invalid file ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
info, err := os.Stat(filePath)
|
|
if os.IsNotExist(err) {
|
|
http.Error(w, "File not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("Error accessing file: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
fileInfo := FileInfo{
|
|
ID: fileID,
|
|
Path: fileID,
|
|
Name: filepath.Base(filePath),
|
|
Size: info.Size(),
|
|
SizeHuman: formatBytes(info.Size()),
|
|
ContentType: GetContentType(filePath),
|
|
ModTime: info.ModTime(),
|
|
}
|
|
|
|
writeJSONResponseAdmin(w, fileInfo)
|
|
}
|
|
|
|
// deleteFile deletes a specific file
|
|
func deleteFile(w http.ResponseWriter, r *http.Request, fileID string) {
|
|
confMutex.RLock()
|
|
storagePath := conf.Server.StoragePath
|
|
confMutex.RUnlock()
|
|
|
|
filePath := filepath.Join(storagePath, fileID)
|
|
|
|
// Validate path is within storage
|
|
absPath, err := filepath.Abs(filePath)
|
|
if err != nil || !strings.HasPrefix(absPath, storagePath) {
|
|
http.Error(w, "Invalid file ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get file info before deletion for audit
|
|
info, err := os.Stat(filePath)
|
|
if os.IsNotExist(err) {
|
|
http.Error(w, "File not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
AuditAdminAction(r, "delete_file", fileID, map[string]interface{}{
|
|
"size": info.Size(),
|
|
})
|
|
|
|
if err := os.Remove(filePath); err != nil {
|
|
http.Error(w, fmt.Sprintf("Error deleting file: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// handleAdminUsers handles user listing
|
|
func handleAdminUsers(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
AuditAdminAction(r, "list_users", "users", nil)
|
|
|
|
ctx := r.Context()
|
|
qm := GetQuotaManager()
|
|
|
|
var users []UserInfo
|
|
|
|
if qm != nil && qm.config.Enabled {
|
|
quotas, err := qm.GetAllQuotas(ctx)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("Error getting quotas: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
for _, quota := range quotas {
|
|
users = append(users, UserInfo{
|
|
JID: quota.JID,
|
|
QuotaUsed: quota.Used,
|
|
QuotaLimit: quota.Limit,
|
|
FileCount: quota.FileCount,
|
|
})
|
|
}
|
|
}
|
|
|
|
writeJSONResponseAdmin(w, users)
|
|
}
|
|
|
|
// handleAdminUserByJID handles single user operations
|
|
func handleAdminUserByJID(w http.ResponseWriter, r *http.Request) {
|
|
prefix := adminConfig.PathPrefix
|
|
if prefix == "" {
|
|
prefix = "/admin"
|
|
}
|
|
|
|
path := strings.TrimPrefix(r.URL.Path, prefix+"/users/")
|
|
parts := strings.Split(path, "/")
|
|
jid := parts[0]
|
|
|
|
if jid == "" {
|
|
http.Error(w, "JID required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Check for sub-paths
|
|
if len(parts) > 1 {
|
|
switch parts[1] {
|
|
case "files":
|
|
handleUserFiles(w, r, jid)
|
|
return
|
|
case "quota":
|
|
handleUserQuota(w, r, jid)
|
|
return
|
|
}
|
|
}
|
|
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
getUserInfo(w, r, jid)
|
|
default:
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
// getUserInfo returns information about a specific user
|
|
func getUserInfo(w http.ResponseWriter, r *http.Request, jid string) {
|
|
ctx := r.Context()
|
|
qm := GetQuotaManager()
|
|
|
|
if qm == nil || !qm.config.Enabled {
|
|
http.Error(w, "Quota tracking not enabled", http.StatusNotImplemented)
|
|
return
|
|
}
|
|
|
|
quota, err := qm.GetQuotaInfo(ctx, jid)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("Error getting quota: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
user := UserInfo{
|
|
JID: jid,
|
|
QuotaUsed: quota.Used,
|
|
QuotaLimit: quota.Limit,
|
|
FileCount: quota.FileCount,
|
|
}
|
|
|
|
writeJSONResponseAdmin(w, user)
|
|
}
|
|
|
|
// handleUserFiles handles user file operations
|
|
func handleUserFiles(w http.ResponseWriter, r *http.Request, jid string) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
// List user's files
|
|
AuditAdminAction(r, "list_user_files", jid, nil)
|
|
// Would need file ownership tracking to implement fully
|
|
writeJSONResponseAdmin(w, []FileInfo{})
|
|
case http.MethodDelete:
|
|
// Delete all user's files
|
|
AuditAdminAction(r, "delete_user_files", jid, nil)
|
|
// Would need file ownership tracking to implement fully
|
|
w.WriteHeader(http.StatusNoContent)
|
|
default:
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
// handleUserQuota handles user quota operations
|
|
func handleUserQuota(w http.ResponseWriter, r *http.Request, jid string) {
|
|
qm := GetQuotaManager()
|
|
if qm == nil {
|
|
http.Error(w, "Quota management not enabled", http.StatusNotImplemented)
|
|
return
|
|
}
|
|
|
|
switch r.Method {
|
|
case http.MethodPost:
|
|
// Set custom quota
|
|
var req struct {
|
|
Quota string `json:"quota"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
quota, err := parseSize(req.Quota)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("Invalid quota: %v", err), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
qm.SetCustomQuota(jid, quota)
|
|
AuditAdminAction(r, "set_quota", jid, map[string]interface{}{"quota": req.Quota})
|
|
|
|
writeJSONResponseAdmin(w, map[string]interface{}{
|
|
"success": true,
|
|
"jid": jid,
|
|
"quota": quota,
|
|
})
|
|
case http.MethodDelete:
|
|
// Remove custom quota
|
|
qm.RemoveCustomQuota(jid)
|
|
AuditAdminAction(r, "remove_quota", jid, nil)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
default:
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
// handleAdminBans handles ban listing
|
|
func handleAdminBans(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
AuditAdminAction(r, "list_bans", "bans", nil)
|
|
|
|
// Would need ban management implementation
|
|
writeJSONResponseAdmin(w, []BanInfo{})
|
|
}
|
|
|
|
// handleAdminBanByIP handles single ban operations
|
|
func handleAdminBanByIP(w http.ResponseWriter, r *http.Request) {
|
|
prefix := adminConfig.PathPrefix
|
|
if prefix == "" {
|
|
prefix = "/admin"
|
|
}
|
|
ip := strings.TrimPrefix(r.URL.Path, prefix+"/bans/")
|
|
|
|
if ip == "" {
|
|
http.Error(w, "IP required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
switch r.Method {
|
|
case http.MethodDelete:
|
|
// Unban IP
|
|
AuditAdminAction(r, "unban", ip, nil)
|
|
// Would need ban management implementation
|
|
w.WriteHeader(http.StatusNoContent)
|
|
default:
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
// handleAdminHealth returns admin-specific health info
|
|
func handleAdminHealth(w http.ResponseWriter, r *http.Request) {
|
|
health := map[string]interface{}{
|
|
"status": "healthy",
|
|
"timestamp": time.Now().UTC(),
|
|
"uptime": time.Since(serverStartTime).String(),
|
|
}
|
|
|
|
// Check Redis
|
|
if redisClient != nil && redisConnected {
|
|
health["redis"] = "connected"
|
|
} else if redisClient != nil {
|
|
health["redis"] = "disconnected"
|
|
}
|
|
|
|
writeJSONResponseAdmin(w, health)
|
|
}
|
|
|
|
// handleAdminConfig returns current configuration (sanitized)
|
|
func handleAdminConfig(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
AuditAdminAction(r, "get_config", "config", nil)
|
|
|
|
confMutex.RLock()
|
|
// Return sanitized config (no secrets)
|
|
sanitized := map[string]interface{}{
|
|
"server": map[string]interface{}{
|
|
"listen_address": conf.Server.ListenAddress,
|
|
"storage_path": conf.Server.StoragePath,
|
|
"max_upload_size": conf.Server.MaxUploadSize,
|
|
"metrics_enabled": conf.Server.MetricsEnabled,
|
|
},
|
|
"security": map[string]interface{}{
|
|
"enhanced_security": conf.Security.EnhancedSecurity,
|
|
"jwt_enabled": conf.Security.EnableJWT,
|
|
},
|
|
"clamav": map[string]interface{}{
|
|
"enabled": conf.ClamAV.ClamAVEnabled,
|
|
},
|
|
"redis": map[string]interface{}{
|
|
"enabled": conf.Redis.RedisEnabled,
|
|
},
|
|
"deduplication": map[string]interface{}{
|
|
"enabled": conf.Deduplication.Enabled,
|
|
},
|
|
}
|
|
confMutex.RUnlock()
|
|
|
|
writeJSONResponseAdmin(w, sanitized)
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func calculateStorageStats(storagePath string) StorageStats {
|
|
var totalSize int64
|
|
var fileCount int64
|
|
|
|
_ = filepath.WalkDir(storagePath, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil || d.IsDir() {
|
|
return nil
|
|
}
|
|
if info, err := d.Info(); err == nil {
|
|
totalSize += info.Size()
|
|
fileCount++
|
|
}
|
|
return nil
|
|
})
|
|
|
|
return StorageStats{
|
|
UsedBytes: totalSize,
|
|
UsedHuman: formatBytes(totalSize),
|
|
FileCount: fileCount,
|
|
}
|
|
}
|
|
|
|
func calculateUserStats(ctx context.Context) UserStats {
|
|
qm := GetQuotaManager()
|
|
if qm == nil || !qm.config.Enabled {
|
|
return UserStats{}
|
|
}
|
|
|
|
quotas, err := qm.GetAllQuotas(ctx)
|
|
if err != nil {
|
|
return UserStats{}
|
|
}
|
|
|
|
return UserStats{
|
|
Total: int64(len(quotas)),
|
|
}
|
|
}
|
|
|
|
func calculateRequestStats() RequestStats {
|
|
// These would ideally come from Prometheus metrics
|
|
return RequestStats{}
|
|
}
|
|
|
|
func writeJSONResponseAdmin(w http.ResponseWriter, data interface{}) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(data); err != nil {
|
|
log.Errorf("Failed to encode JSON response: %v", err)
|
|
}
|
|
}
|
|
|
|
// DefaultAdminConfig returns default admin configuration
|
|
func DefaultAdminConfig() AdminConfig {
|
|
return AdminConfig{
|
|
Enabled: false,
|
|
Bind: "",
|
|
PathPrefix: "/admin",
|
|
Auth: AdminAuthConfig{
|
|
Type: "bearer",
|
|
},
|
|
}
|
|
}
|