package api import ( "encoding/json" "fmt" "net/http" "time" "git.sovbit.dev/enki/torrentGateway/internal/auth" "git.sovbit.dev/enki/torrentGateway/internal/middleware" "github.com/gorilla/mux" ) // AuthHandlers provides authentication-related HTTP handlers type AuthHandlers struct { nostrAuth *auth.NostrAuth gateway *Gateway } // NewAuthHandlers creates new authentication handlers func NewAuthHandlers(nostrAuth *auth.NostrAuth, gateway *Gateway) *AuthHandlers { return &AuthHandlers{ nostrAuth: nostrAuth, gateway: gateway, } } // LoginRequest represents a login request type LoginRequest struct { AuthType string `json:"auth_type"` // "nip07" or "nip46" AuthEvent string `json:"auth_event"` // For NIP-07: signed event JSON BunkerURL string `json:"bunker_url"` // For NIP-46: bunker connection URL } // LoginResponse represents a login response type LoginResponse struct { Success bool `json:"success"` SessionToken string `json:"session_token,omitempty"` Pubkey string `json:"pubkey,omitempty"` Message string `json:"message,omitempty"` Challenge string `json:"challenge,omitempty"` } // UserStatsResponse represents user statistics type UserStatsResponse struct { Pubkey string `json:"pubkey"` DisplayName string `json:"display_name,omitempty"` FileCount int `json:"file_count"` StorageUsed int64 `json:"storage_used"` LastLogin string `json:"last_login"` } // UserFile represents a file in user's file list type UserFile struct { Hash string `json:"hash"` Name string `json:"name"` Size int64 `json:"size"` StorageType string `json:"storage_type"` AccessLevel string `json:"access_level"` UploadedAt string `json:"uploaded_at"` } // LoginHandler handles user authentication func (ah *AuthHandlers) LoginHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req LoginRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } var pubkey string var err error switch req.AuthType { case "nip07": pubkey, err = ah.nostrAuth.ValidateNIP07(req.AuthEvent) if err != nil { response := LoginResponse{ Success: false, Message: fmt.Sprintf("NIP-07 validation failed: %v", err), } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(response) return } case "nip46": pubkey, err = ah.nostrAuth.ValidateNIP46(req.BunkerURL) if err != nil { response := LoginResponse{ Success: false, Message: fmt.Sprintf("NIP-46 validation failed: %v", err), } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(response) return } default: response := LoginResponse{ Success: false, Message: "Invalid auth_type: must be 'nip07' or 'nip46'", } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(response) return } // Create session sessionToken, err := ah.nostrAuth.CreateSession(pubkey) if err != nil { response := LoginResponse{ Success: false, Message: fmt.Sprintf("Failed to create session: %v", err), } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(response) return } // Set session cookie cookie := &http.Cookie{ Name: "session_token", Value: sessionToken, Expires: time.Now().Add(24 * time.Hour), HttpOnly: true, Secure: false, // Set to true in production with HTTPS SameSite: http.SameSiteStrictMode, Path: "/", } http.SetCookie(w, cookie) response := LoginResponse{ Success: true, SessionToken: sessionToken, Pubkey: pubkey, Message: "Login successful", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // LogoutHandler handles user logout func (ah *AuthHandlers) LogoutHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Get session token from cookie or header var token string if cookie, err := r.Cookie("session_token"); err == nil { token = cookie.Value } if token != "" { // Revoke session ah.nostrAuth.RevokeSession(token) } // Clear session cookie cookie := &http.Cookie{ Name: "session_token", Value: "", Expires: time.Now().Add(-1 * time.Hour), HttpOnly: true, Path: "/", } http.SetCookie(w, cookie) response := map[string]bool{"success": true} w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // UserStatsHandler returns user statistics func (ah *AuthHandlers) UserStatsHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } pubkey := middleware.GetUserFromContext(r.Context()) if pubkey == "" { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // Get user info user, err := ah.nostrAuth.GetUser(pubkey) if err != nil { http.Error(w, "Failed to get user info", http.StatusInternalServerError) return } // Calculate current stats storageUsed, fileCount, err := ah.gateway.storage.GetUserStats(pubkey) if err != nil { http.Error(w, "Failed to calculate stats", http.StatusInternalServerError) return } // Update cached stats ah.nostrAuth.UpdateUserStats(pubkey, storageUsed, fileCount) response := UserStatsResponse{ Pubkey: pubkey, FileCount: fileCount, StorageUsed: storageUsed, } if user != nil { response.DisplayName = user.DisplayName response.LastLogin = user.LastLogin.Format(time.RFC3339) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // UserFilesHandler returns user's files func (ah *AuthHandlers) UserFilesHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } pubkey := middleware.GetUserFromContext(r.Context()) if pubkey == "" { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // Get user's files files, err := ah.gateway.storage.GetUserFiles(pubkey) if err != nil { http.Error(w, "Failed to get user files", http.StatusInternalServerError) return } // Convert to response format var userFiles []UserFile if files != nil { for _, file := range files { userFiles = append(userFiles, UserFile{ Hash: file.Hash, Name: file.OriginalName, Size: file.Size, StorageType: file.StorageType, AccessLevel: file.AccessLevel, UploadedAt: file.CreatedAt.Format(time.RFC3339), }) } } // Ensure we always return an array, never null if userFiles == nil { userFiles = []UserFile{} } response := struct { Files []UserFile `json:"files"` }{ Files: userFiles, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // DeleteFileHandler deletes a user's file func (ah *AuthHandlers) DeleteFileHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } pubkey := middleware.GetUserFromContext(r.Context()) if pubkey == "" { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } vars := mux.Vars(r) fileHash := vars["hash"] if fileHash == "" { http.Error(w, "Missing file hash", http.StatusBadRequest) return } // Delete the file err := ah.gateway.storage.DeleteUserFile(fileHash, pubkey) if err != nil { if err.Error() == "file not found" { http.Error(w, "File not found", http.StatusNotFound) return } if err.Error() == "permission denied: not file owner" { http.Error(w, "Permission denied", http.StatusForbidden) return } http.Error(w, "Failed to delete file", http.StatusInternalServerError) return } response := map[string]interface{}{ "success": true, "message": "File deleted successfully", "hash": fileHash, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // ChallengeHandler generates an authentication challenge func (ah *AuthHandlers) ChallengeHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } challenge, err := auth.GenerateChallenge() if err != nil { http.Error(w, "Failed to generate challenge", http.StatusInternalServerError) return } response := map[string]string{ "challenge": challenge, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // UpdateFileAccessRequest represents a file access update request type UpdateFileAccessRequest struct { AccessLevel string `json:"access_level"` } // UpdateFileAccessHandler updates a file's access level func (ah *AuthHandlers) UpdateFileAccessHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPut { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } pubkey := middleware.GetUserFromContext(r.Context()) if pubkey == "" { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } vars := mux.Vars(r) fileHash := vars["hash"] if fileHash == "" { http.Error(w, "Missing file hash", http.StatusBadRequest) return } var req UpdateFileAccessRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } // Validate access level if req.AccessLevel != "public" && req.AccessLevel != "private" { http.Error(w, "Invalid access level: must be 'public' or 'private'", http.StatusBadRequest) return } // Update the file access level err := ah.gateway.storage.UpdateFileAccess(fileHash, pubkey, req.AccessLevel) if err != nil { if err.Error() == "file not found" { http.Error(w, "File not found", http.StatusNotFound) return } if err.Error() == "permission denied: not file owner" { http.Error(w, "Permission denied", http.StatusForbidden) return } http.Error(w, "Failed to update file access", http.StatusInternalServerError) return } response := map[string]interface{}{ "success": true, "message": "File access level updated successfully", "hash": fileHash, "access_level": req.AccessLevel, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // AdminStatusHandler checks if the authenticated user is an admin func (ah *AuthHandlers) AdminStatusHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } pubkey := middleware.GetUserFromContext(r.Context()) if pubkey == "" { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // Check if user is admin - this would depend on your admin config // For now, we'll check against the config admin pubkeys isAdmin := false if ah.gateway.config.Admin.Enabled { for _, adminPubkey := range ah.gateway.config.Admin.Pubkeys { if adminPubkey == pubkey { isAdmin = true break } } } response := map[string]interface{}{ "is_admin": isAdmin, "pubkey": pubkey, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) }