package storage import ( "fmt" "time" "github.com/golang-jwt/jwt/v5" "github.com/openaccounting/oa-server/core/util/id" ) // TokenService handles JWT token generation and validation for file access type TokenService struct { signingKey []byte } // NewTokenService creates a new token service with a signing key func NewTokenService(signingKey string) *TokenService { if signingKey == "" { // Generate a random signing key if none provided // In production, this should be a consistent secret from config signingKey = id.String(id.New()) } return &TokenService{ signingKey: []byte(signingKey), } } // FileClaims represents the JWT claims for file access type FileClaims struct { FilePath string `json:"file_path"` UserID string `json:"user_id,omitempty"` OrgID string `json:"org_id,omitempty"` jwt.RegisteredClaims } // GenerateFileToken creates a JWT token for accessing a specific file func (ts *TokenService) GenerateFileToken(filePath string, userID, orgID string, expiry time.Duration) (string, error) { now := time.Now() claims := FileClaims{ FilePath: filePath, UserID: userID, OrgID: orgID, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(now.Add(expiry)), IssuedAt: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now), Issuer: "openaccounting-server", Subject: "file-access", ID: id.String(id.New()), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString(ts.signingKey) if err != nil { return "", fmt.Errorf("failed to sign token: %w", err) } return tokenString, nil } // ValidateFileToken validates a JWT token and returns the file claims func (ts *TokenService) ValidateFileToken(tokenString string) (*FileClaims, error) { token, err := jwt.ParseWithClaims(tokenString, &FileClaims{}, func(token *jwt.Token) (interface{}, error) { // Verify the signing method if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return ts.signingKey, nil }) if err != nil { return nil, fmt.Errorf("failed to parse token: %w", err) } claims, ok := token.Claims.(*FileClaims) if !ok || !token.Valid { return nil, fmt.Errorf("invalid token claims") } return claims, nil }