Add photo galleries API and frontend for public and admin viewing.
Introduces the Go photo-api service, nginx/systemd deploy wiring, and Next.js gallery/lightbox pages so event photos can be managed and browsed. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
// Package auth validates the JWTs minted by backend/src/lib/auth.ts:
|
||||
// HS256 with the shared JWT_SECRET, issuer "spanglish", audience
|
||||
// "spanglish-app", plus the same DB-backed tokenVersion / account_status
|
||||
// revocation check the backend's getAuthUser performs.
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
"git.azzamo.net/Michilis/Spanglish/photo-api/internal/store"
|
||||
)
|
||||
|
||||
const (
|
||||
issuer = "spanglish"
|
||||
audience = "spanglish-app"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoToken = errors.New("no bearer token")
|
||||
ErrInvalidToken = errors.New("invalid token")
|
||||
)
|
||||
|
||||
// AdminRoles mirrors backend/src/routes/media.ts: photo administration is
|
||||
// admin/organizer only (review decision #6).
|
||||
var AdminRoles = []string{"admin", "organizer"}
|
||||
|
||||
type User struct {
|
||||
ID string
|
||||
Email string
|
||||
Role string
|
||||
}
|
||||
|
||||
func (u User) IsAdmin() bool {
|
||||
for _, r := range AdminRoles {
|
||||
if u.Role == r {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type claims struct {
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
TokenVersion *int `json:"tokenVersion"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
type Verifier struct {
|
||||
secret []byte
|
||||
db *store.DB
|
||||
}
|
||||
|
||||
func NewVerifier(secret string, db *store.DB) *Verifier {
|
||||
return &Verifier{secret: []byte(secret), db: db}
|
||||
}
|
||||
|
||||
// FromRequest returns the authenticated user, ErrNoToken when no
|
||||
// Authorization header is present, or ErrInvalidToken for anything bad.
|
||||
func (v *Verifier) FromRequest(r *http.Request) (User, error) {
|
||||
header := r.Header.Get("Authorization")
|
||||
if header == "" {
|
||||
return User{}, ErrNoToken
|
||||
}
|
||||
raw, ok := strings.CutPrefix(header, "Bearer ")
|
||||
if !ok {
|
||||
return User{}, ErrInvalidToken
|
||||
}
|
||||
|
||||
var c claims
|
||||
_, err := jwt.ParseWithClaims(raw, &c, func(*jwt.Token) (any, error) { return v.secret, nil },
|
||||
jwt.WithValidMethods([]string{"HS256"}),
|
||||
jwt.WithIssuer(issuer),
|
||||
jwt.WithAudience(audience),
|
||||
jwt.WithExpirationRequired(),
|
||||
)
|
||||
if err != nil || c.Subject == "" {
|
||||
return User{}, ErrInvalidToken
|
||||
}
|
||||
|
||||
// DB-backed revocation, same semantics as backend getAuthUser
|
||||
// (backend/src/lib/auth.ts:320-327).
|
||||
ua, err := v.db.GetUserAuth(r.Context(), c.Subject)
|
||||
if err != nil {
|
||||
return User{}, ErrInvalidToken
|
||||
}
|
||||
if ua.AccountStatus != "active" {
|
||||
return User{}, ErrInvalidToken
|
||||
}
|
||||
if c.TokenVersion != nil && *c.TokenVersion != ua.TokenVersion {
|
||||
return User{}, ErrInvalidToken
|
||||
}
|
||||
// Role from the DB, not the token, so demotions apply immediately.
|
||||
return User{ID: ua.ID, Email: c.Email, Role: ua.Role}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user