package admin import ( "database/sql" "encoding/json" "fmt" "net/http" "strconv" "time" "git.sovbit.dev/enki/torrentGateway/internal/profile" "git.sovbit.dev/enki/torrentGateway/internal/storage" "github.com/gorilla/mux" ) // GatewayInterface defines the methods needed from the gateway type GatewayInterface interface { GetDB() *sql.DB GetStorage() *storage.Backend CleanupOldFiles(olderThan time.Duration) (map[string]interface{}, error) CleanupOrphanedChunks() (map[string]interface{}, error) CleanupInactiveUsers(days int) (map[string]interface{}, error) } // AdminHandlers provides admin-related HTTP handlers type AdminHandlers struct { adminAuth *AdminAuth gateway GatewayInterface profileFetcher *profile.ProfileFetcher } // NewAdminHandlers creates new admin handlers func NewAdminHandlers(adminAuth *AdminAuth, gateway GatewayInterface, defaultRelays []string) *AdminHandlers { return &AdminHandlers{ adminAuth: adminAuth, gateway: gateway, profileFetcher: profile.NewProfileFetcher(defaultRelays), } } // AdminStatsResponse represents admin statistics type AdminStatsResponse struct { TotalFiles int `json:"total_files"` TotalUsers int `json:"total_users"` TotalStorage int64 `json:"total_storage"` BannedUsers int `json:"banned_users"` PendingReports int `json:"pending_reports"` RecentUploads int `json:"recent_uploads_24h"` ErrorRate float64 `json:"error_rate"` } // AdminUser represents a user in admin view type AdminUser struct { Pubkey string `json:"pubkey"` DisplayName string `json:"display_name"` FileCount int `json:"file_count"` StorageUsed int64 `json:"storage_used"` LastLogin time.Time `json:"last_login"` CreatedAt time.Time `json:"created_at"` IsBanned bool `json:"is_banned"` Profile *profile.ProfileMetadata `json:"profile,omitempty"` } // AdminFile represents a file in admin view type AdminFile struct { Hash string `json:"hash"` Name string `json:"name"` Size int64 `json:"size"` StorageType string `json:"storage_type"` AccessLevel string `json:"access_level"` OwnerPubkey string `json:"owner_pubkey"` CreatedAt time.Time `json:"created_at"` AccessCount int `json:"access_count"` ReportCount int `json:"report_count"` OwnerProfile *profile.ProfileMetadata `json:"owner_profile,omitempty"` } // AdminStatsHandler returns system statistics for admins func (ah *AdminHandlers) AdminStatsHandler(w http.ResponseWriter, r *http.Request) { adminPubkey, err := ah.adminAuth.ValidateAdminRequest(r) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "error": "Unauthorized", }) return } // Get total files var totalFiles int err = ah.gateway.GetDB().QueryRow("SELECT COUNT(*) FROM files").Scan(&totalFiles) if err != nil { http.Error(w, "Failed to get file count", http.StatusInternalServerError) return } // Get total users var totalUsers int err = ah.gateway.GetDB().QueryRow("SELECT COUNT(*) FROM users").Scan(&totalUsers) if err != nil { http.Error(w, "Failed to get user count", http.StatusInternalServerError) return } // Get total storage var totalStorage int64 err = ah.gateway.GetDB().QueryRow("SELECT COALESCE(SUM(size), 0) FROM files").Scan(&totalStorage) if err != nil { http.Error(w, "Failed to get storage total", http.StatusInternalServerError) return } // Get banned users count var bannedUsers int err = ah.gateway.GetDB().QueryRow("SELECT COUNT(*) FROM banned_users").Scan(&bannedUsers) if err != nil { http.Error(w, "Failed to get banned users count", http.StatusInternalServerError) return } // Get pending reports var pendingReports int err = ah.gateway.GetDB().QueryRow("SELECT COUNT(*) FROM content_reports WHERE status = 'pending'").Scan(&pendingReports) if err != nil { http.Error(w, "Failed to get pending reports count", http.StatusInternalServerError) return } // Get recent uploads (24h) var recentUploads int err = ah.gateway.GetDB().QueryRow("SELECT COUNT(*) FROM files WHERE created_at > datetime('now', '-1 day')").Scan(&recentUploads) if err != nil { http.Error(w, "Failed to get recent uploads count", http.StatusInternalServerError) return } // Log admin action ah.adminAuth.LogAdminAction(adminPubkey, "view_stats", "", "Admin viewed system statistics") response := AdminStatsResponse{ TotalFiles: totalFiles, TotalUsers: totalUsers, TotalStorage: totalStorage, BannedUsers: bannedUsers, PendingReports: pendingReports, RecentUploads: recentUploads, ErrorRate: 0.0, // TODO: Implement error rate tracking } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // AdminUsersHandler returns list of users for admin management func (ah *AdminHandlers) AdminUsersHandler(w http.ResponseWriter, r *http.Request) { adminPubkey, err := ah.adminAuth.ValidateAdminRequest(r) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "error": "Unauthorized", }) return } // Parse query parameters limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) if limit <= 0 || limit > 100 { limit = 50 } offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) query := ` SELECT u.pubkey, COALESCE(u.display_name, '') as display_name, u.file_count, u.storage_used, u.last_login, u.created_at, EXISTS(SELECT 1 FROM banned_users WHERE pubkey = u.pubkey) as is_banned FROM users u ORDER BY u.created_at DESC LIMIT ? OFFSET ? ` rows, err := ah.gateway.GetDB().Query(query, limit, offset) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "error": "Failed to query users", }) return } defer rows.Close() var users []AdminUser for rows.Next() { var user AdminUser err := rows.Scan(&user.Pubkey, &user.DisplayName, &user.FileCount, &user.StorageUsed, &user.LastLogin, &user.CreatedAt, &user.IsBanned) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "error": "Failed to scan user", }) return } users = append(users, user) } // Skip profile fetching - let frontend handle it asynchronously // Log admin action ah.adminAuth.LogAdminAction(adminPubkey, "view_users", "", "Admin viewed user list") w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(users) } // AdminFilesHandler returns list of files for admin management func (ah *AdminHandlers) AdminFilesHandler(w http.ResponseWriter, r *http.Request) { adminPubkey, err := ah.adminAuth.ValidateAdminRequest(r) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "error": "Unauthorized", }) return } // Parse query parameters limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) if limit <= 0 || limit > 100 { limit = 50 } offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) storageType := r.URL.Query().Get("storage_type") accessLevel := r.URL.Query().Get("access_level") // Build query with filters query := ` SELECT f.hash, f.original_name, f.size, f.storage_type, f.access_level, COALESCE(f.owner_pubkey, '') as owner_pubkey, f.created_at, f.access_count, COALESCE((SELECT COUNT(*) FROM content_reports WHERE file_hash = f.hash), 0) as report_count FROM files f WHERE 1=1 ` args := []interface{}{} if storageType != "" { query += " AND f.storage_type = ?" args = append(args, storageType) } if accessLevel != "" { query += " AND f.access_level = ?" args = append(args, accessLevel) } query += " ORDER BY f.created_at DESC LIMIT ? OFFSET ?" args = append(args, limit, offset) rows, err := ah.gateway.GetDB().Query(query, args...) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "error": "Failed to query files", }) return } defer rows.Close() var files []AdminFile for rows.Next() { var file AdminFile err := rows.Scan(&file.Hash, &file.Name, &file.Size, &file.StorageType, &file.AccessLevel, &file.OwnerPubkey, &file.CreatedAt, &file.AccessCount, &file.ReportCount) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "error": "Failed to scan file", }) return } files = append(files, file) } // Skip profile fetching - let frontend handle it asynchronously // Log admin action ah.adminAuth.LogAdminAction(adminPubkey, "view_files", "", "Admin viewed file list") w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(files) } // AdminDeleteFileHandler deletes a file with admin privileges func (ah *AdminHandlers) AdminDeleteFileHandler(w http.ResponseWriter, r *http.Request) { adminPubkey, err := ah.adminAuth.ValidateAdminRequest(r) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "error": "Unauthorized", }) return } vars := mux.Vars(r) fileHash := vars["hash"] if fileHash == "" { http.Error(w, "Missing file hash", http.StatusBadRequest) return } // Get reason from request body var reqBody struct { Reason string `json:"reason"` } if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } // Get file info before deletion for logging metadata, err := ah.gateway.GetStorage().GetFileMetadata(fileHash) if err != nil { http.Error(w, "File not found", http.StatusNotFound) return } // Admin can delete any file err = ah.gateway.GetStorage().AdminDeleteFile(fileHash) if err != nil { http.Error(w, "Failed to delete file", http.StatusInternalServerError) return } // Log admin action reason := reqBody.Reason if reason == "" { reason = "Admin deletion" } ah.adminAuth.LogAdminAction(adminPubkey, "delete_file", fileHash, fmt.Sprintf("Deleted file '%s' (owner: %s) - %s", metadata.OriginalName, metadata.OwnerPubkey, reason)) response := map[string]interface{}{ "success": true, "message": "File deleted successfully", "hash": fileHash, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // BanUserRequest represents a user ban request type BanUserRequest struct { Reason string `json:"reason"` } // AdminBanUserHandler bans a user func (ah *AdminHandlers) AdminBanUserHandler(w http.ResponseWriter, r *http.Request) { adminPubkey, err := ah.adminAuth.ValidateAdminRequest(r) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "error": "Unauthorized", }) return } vars := mux.Vars(r) userPubkey := vars["pubkey"] if userPubkey == "" { http.Error(w, "Missing user pubkey", http.StatusBadRequest) return } var req BanUserRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } // Ban the user err = ah.adminAuth.BanUser(userPubkey, adminPubkey, req.Reason) if err != nil { http.Error(w, fmt.Sprintf("Failed to ban user: %v", err), http.StatusInternalServerError) return } response := map[string]interface{}{ "success": true, "message": "User banned successfully", "pubkey": userPubkey, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // AdminUnbanUserHandler unbans a user func (ah *AdminHandlers) AdminUnbanUserHandler(w http.ResponseWriter, r *http.Request) { adminPubkey, err := ah.adminAuth.ValidateAdminRequest(r) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "error": "Unauthorized", }) return } vars := mux.Vars(r) userPubkey := vars["pubkey"] if userPubkey == "" { http.Error(w, "Missing user pubkey", http.StatusBadRequest) return } var req struct { Reason string `json:"reason"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } // Unban the user err = ah.adminAuth.UnbanUser(userPubkey, adminPubkey, req.Reason) if err != nil { http.Error(w, fmt.Sprintf("Failed to unban user: %v", err), http.StatusInternalServerError) return } response := map[string]interface{}{ "success": true, "message": "User unbanned successfully", "pubkey": userPubkey, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // AdminReportsHandler returns content reports func (ah *AdminHandlers) AdminReportsHandler(w http.ResponseWriter, r *http.Request) { adminPubkey, err := ah.adminAuth.ValidateAdminRequest(r) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "error": "Unauthorized", }) return } // Parse query parameters limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) if limit <= 0 || limit > 100 { limit = 50 } offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) status := r.URL.Query().Get("status") query := ` SELECT cr.id, cr.file_hash, cr.reporter_pubkey, cr.reason, cr.status, cr.created_at, f.original_name, f.size, f.owner_pubkey FROM content_reports cr LEFT JOIN files f ON cr.file_hash = f.hash WHERE 1=1 ` args := []interface{}{} if status != "" { query += " AND cr.status = ?" args = append(args, status) } query += " ORDER BY cr.created_at DESC LIMIT ? OFFSET ?" args = append(args, limit, offset) rows, err := ah.gateway.GetDB().Query(query, args...) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "error": "Failed to query reports", }) return } defer rows.Close() var reports []map[string]interface{} for rows.Next() { var report ContentReport var fileName, ownerPubkey sql.NullString var fileSize sql.NullInt64 err := rows.Scan(&report.ID, &report.FileHash, &report.ReporterPubkey, &report.Reason, &report.Status, &report.CreatedAt, &fileName, &fileSize, &ownerPubkey) if err != nil { http.Error(w, "Failed to scan report", http.StatusInternalServerError) return } reportData := map[string]interface{}{ "id": report.ID, "file_hash": report.FileHash, "reporter_pubkey": report.ReporterPubkey, "reason": report.Reason, "status": report.Status, "created_at": report.CreatedAt, "file_name": fileName.String, "file_size": fileSize.Int64, "file_owner": ownerPubkey.String, } reports = append(reports, reportData) } // Log admin action ah.adminAuth.LogAdminAction(adminPubkey, "view_reports", "", "Admin viewed content reports") w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(reports) } // AdminCleanupHandler triggers cleanup operations func (ah *AdminHandlers) AdminCleanupHandler(w http.ResponseWriter, r *http.Request) { adminPubkey, err := ah.adminAuth.ValidateAdminRequest(r) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "error": "Unauthorized", }) return } var req struct { Operation string `json:"operation"` MaxAge string `json:"max_age,omitempty"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } var cleanupResult map[string]interface{} var cleanupErr error switch req.Operation { case "old_files": maxAge := "90d" if req.MaxAge != "" { maxAge = req.MaxAge } duration, err := time.ParseDuration(maxAge) if err != nil { http.Error(w, "Invalid max_age format", http.StatusBadRequest) return } cleanupResult, cleanupErr = ah.gateway.CleanupOldFiles(duration) case "orphaned_chunks": cleanupResult, cleanupErr = ah.gateway.CleanupOrphanedChunks() case "inactive_users": days := 365 if req.MaxAge != "" { if d, err := strconv.Atoi(req.MaxAge); err == nil { days = d } } cleanupResult, cleanupErr = ah.gateway.CleanupInactiveUsers(days) default: http.Error(w, "Invalid cleanup operation", http.StatusBadRequest) return } if cleanupErr != nil { http.Error(w, fmt.Sprintf("Cleanup failed: %v", cleanupErr), http.StatusInternalServerError) return } // Log admin action ah.adminAuth.LogAdminAction(adminPubkey, "cleanup", req.Operation, fmt.Sprintf("Executed cleanup operation: %s", req.Operation)) response := map[string]interface{}{ "success": true, "operation": req.Operation, "result": cleanupResult, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // AdminLogsHandler returns admin action logs func (ah *AdminHandlers) AdminLogsHandler(w http.ResponseWriter, r *http.Request) { _, err := ah.adminAuth.ValidateAdminRequest(r) if err != nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // Parse query parameters limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) if limit <= 0 || limit > 100 { limit = 50 } offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) actions, err := ah.adminAuth.GetAdminActions(limit, offset, "") if err != nil { http.Error(w, "Failed to get admin actions", http.StatusInternalServerError) return } // Log admin action (don't log viewing logs to avoid spam) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(actions) }