mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 15:51:15 +00:00
refactor(db): replace sqlc with handwritten pgx repos
- Finish Paddle-to-Stripe billing migration; drop paddle/stripe libs - Renumber migrations; add 00012 memberships+manage_token, 00013 location_type_capacity - Add tests: admin, auth, bookings customer service - Remove graphify-out artifacts and stale env files Authored By: TDvorak <[email protected]> Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
2039669e2c
commit
f2f7439050
93 files changed
+6987
-69060
No files matched your search
@@ -65,29 +65,42 @@ func (v *Verifier) Close() {
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Verifier) verifyLocal(tokenString string) (jwt.MapClaims, error) {
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return v.localSecret, nil
|
||||
}, jwt.WithIssuer("bookra-auth"), jwt.WithAudience("bookra"), jwt.WithLeeway(15*time.Second))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok || !token.Valid {
|
||||
return nil, errors.New("invalid token claims")
|
||||
}
|
||||
if tokenType, _ := claims["type"].(string); tokenType != "access" {
|
||||
return nil, errors.New("invalid token type")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func (v *Verifier) Verify(tokenString string) (jwt.MapClaims, error) {
|
||||
if !v.enabled {
|
||||
return nil, errors.New("neon auth verifier is disabled")
|
||||
}
|
||||
|
||||
if len(v.localSecret) > 0 && v.jwks == nil {
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return v.localSecret, nil
|
||||
}, jwt.WithIssuer("bookra-auth"), jwt.WithAudience("bookra"), jwt.WithLeeway(15*time.Second))
|
||||
if err != nil {
|
||||
if len(v.localSecret) > 0 {
|
||||
claims, err := v.verifyLocal(tokenString)
|
||||
if err == nil {
|
||||
return claims, nil
|
||||
}
|
||||
if v.jwks == nil {
|
||||
return nil, err
|
||||
}
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok || !token.Valid {
|
||||
return nil, errors.New("invalid token claims")
|
||||
}
|
||||
if tokenType, _ := claims["type"].(string); tokenType != "access" {
|
||||
return nil, errors.New("invalid token type")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
if v.jwks == nil {
|
||||
return nil, errors.New("no token verification mechanism configured")
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(tokenString, v.jwks.Keyfunc,
|
||||
|
||||
@@ -20,6 +20,8 @@ const (
|
||||
refreshTokenTTL = 30 * 24 * time.Hour
|
||||
magicLinkTTL = 15 * time.Minute
|
||||
passwordResetTTL = 30 * time.Minute
|
||||
tokenIssuer = "bookra-auth"
|
||||
tokenAudience = "bookra"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -227,7 +229,7 @@ func (s *Service) ValidateToken(tokenString string) (*Claims, error) {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return s.jwtSecret, nil
|
||||
})
|
||||
}, jwt.WithIssuer(tokenIssuer), jwt.WithAudience(tokenAudience))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -266,6 +268,8 @@ func (s *Service) generateTokenPair(userID, email, name, role string) (*TokenPai
|
||||
Role: role,
|
||||
Type: "access",
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: tokenIssuer,
|
||||
Audience: jwt.ClaimStrings{tokenAudience},
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(accessTokenTTL)),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
@@ -285,6 +289,8 @@ func (s *Service) generateTokenPair(userID, email, name, role string) (*TokenPai
|
||||
Role: role,
|
||||
Type: "refresh",
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: tokenIssuer,
|
||||
Audience: jwt.ClaimStrings{tokenAudience},
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(refreshTokenTTL)),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"bookra/apps/backend/internal/db"
|
||||
)
|
||||
|
||||
func TestLocalTokensVerifyWithIssuerAndAudience(t *testing.T) {
|
||||
repo := db.NewMemoryRepository()
|
||||
secret := "test-secret-that-is-long-enough"
|
||||
svc := NewService(repo, secret)
|
||||
|
||||
_, tokens, err := svc.RegisterWithPassword(context.Background(), "[email protected]", "password123", "Test User")
|
||||
if err != nil {
|
||||
t.Fatalf("register: %v", err)
|
||||
}
|
||||
|
||||
verifier, err := NewVerifier("", secret)
|
||||
if err != nil {
|
||||
t.Fatalf("create verifier: %v", err)
|
||||
}
|
||||
defer verifier.Close()
|
||||
|
||||
claims, err := verifier.Verify(tokens.AccessToken)
|
||||
if err != nil {
|
||||
t.Fatalf("verify access token: %v", err)
|
||||
}
|
||||
if iss, _ := claims["iss"].(string); iss != tokenIssuer {
|
||||
t.Fatalf("expected issuer %s, got %v", tokenIssuer, claims["iss"])
|
||||
}
|
||||
if sub, _ := claims["sub"].(string); sub == "" {
|
||||
t.Fatal("expected non-empty sub")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalVerifierRejectsTamperedAndWrongTypeTokens(t *testing.T) {
|
||||
repo := db.NewMemoryRepository()
|
||||
secret := "test-secret-that-is-long-enough"
|
||||
svc := NewService(repo, secret)
|
||||
|
||||
_, tokens, err := svc.RegisterWithPassword(context.Background(), "[email protected]", "password123", "Test User")
|
||||
if err != nil {
|
||||
t.Fatalf("register: %v", err)
|
||||
}
|
||||
|
||||
verifier, err := NewVerifier("", secret)
|
||||
if err != nil {
|
||||
t.Fatalf("create verifier: %v", err)
|
||||
}
|
||||
defer verifier.Close()
|
||||
|
||||
// Refresh tokens carry type=refresh and must not authenticate API calls.
|
||||
if _, err := verifier.Verify(tokens.RefreshToken); err == nil {
|
||||
t.Fatal("expected refresh token to be rejected as access token")
|
||||
}
|
||||
|
||||
// Token signed with a different secret must fail.
|
||||
other := NewService(repo, "different-secret")
|
||||
otherUser, otherTokens, err := other.RegisterWithPassword(context.Background(), "[email protected]", "password123", "Other")
|
||||
if err != nil {
|
||||
t.Fatalf("register other: %v", err)
|
||||
}
|
||||
_ = otherUser
|
||||
if _, err := verifier.Verify(otherTokens.AccessToken); err == nil {
|
||||
t.Fatal("expected token signed with a different secret to be rejected")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user