Some checks are pending
CI Pipeline / Run Tests (push) Waiting to run
CI Pipeline / Lint Code (push) Waiting to run
CI Pipeline / Security Scan (push) Waiting to run
CI Pipeline / Build Docker Images (push) Blocked by required conditions
CI Pipeline / E2E Tests (push) Blocked by required conditions
444 lines
11 KiB
Go
444 lines
11 KiB
Go
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)
|
|
} |