package auth import ( "crypto/rand" "encoding/base64" "fmt" "time" "github.com/jfraeys/fetch_ml/internal/storage" ) // ShareTokenOptions provides options for creating share tokens type ShareTokenOptions struct { ExpiresAt *time.Time // nil = never expires MaxAccesses *int // nil = unlimited } // GenerateShareToken creates a new cryptographically random share token. // Returns the token string that can be shared with others. func GenerateShareToken(db *storage.DB, taskID, experimentID *string, createdBy string, opts ShareTokenOptions) (string, error) { // Generate 32 bytes of random data raw := make([]byte, 32) if _, err := rand.Read(raw); err != nil { return "", fmt.Errorf("failed to generate random token: %w", err) } // Encode as base64url (URL-safe, no padding) token := base64.RawURLEncoding.EncodeToString(raw) // Store in database if err := db.CreateShareToken(token, taskID, experimentID, createdBy, opts.ExpiresAt, opts.MaxAccesses); err != nil { return "", fmt.Errorf("failed to store share token: %w", err) } return token, nil } // CanAccessWithToken checks if a token grants access to a task or experiment. // This is called for unauthenticated access via signed share links. // Returns true if the token is valid, not expired, and within access limits. func CanAccessWithToken(db *storage.DB, token string, taskID, experimentID *string) bool { t, err := db.ValidateShareToken(token, taskID, experimentID) if err != nil { return false } if t == nil { return false } // Increment access count if err := db.IncrementTokenAccessCount(token); err != nil { // Log error but don't fail the request - access was already validated // This is a best-effort operation for tracking purposes fmt.Printf("WARNING: failed to increment token access count: %v\n", err) } return true } // BuildShareLink constructs a shareable URL from a token. // The path should be the base API path (e.g., "/api/tasks/123" or "/api/experiments/456"). func BuildShareLink(baseURL, path, token string) string { return fmt.Sprintf("%s%s?token=%s", baseURL, path, token) }