Backend (Go): - Project structure with chi router, pgxpool, goose migrations - JWT auth (access/refresh tokens) with Firebase token verification - NoopTokenVerifier for local dev without Firebase credentials - PostgreSQL user repository with atomic profile updates (transactions) - Mifflin-St Jeor calorie calculation based on profile data - REST API: POST /auth/login, /auth/refresh, /auth/logout, GET/PUT /profile, GET /health - Middleware: auth, CORS (localhost wildcard), logging, recovery, request_id - Unit tests (51 passing) and integration tests (testcontainers) - Docker Compose setup with postgres healthcheck and graceful shutdown Flutter client: - Riverpod state management with GoRouter navigation - Firebase Auth (email/password + Google sign-in with web popup support) - Platform-aware API URLs (web/Android/iOS) - Dio HTTP client with JWT auth interceptor and concurrent refresh handling - Secure token storage - Screens: Login, Register, Home (tabs: Menu, Recipes, Products, Profile) - Unit tests (17 passing) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
90 lines
2.5 KiB
Go
90 lines
2.5 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
|
|
firebase "firebase.google.com/go/v4"
|
|
firebaseAuth "firebase.google.com/go/v4/auth"
|
|
"google.golang.org/api/option"
|
|
)
|
|
|
|
// TokenVerifier abstracts Firebase token verification for testability.
|
|
type TokenVerifier interface {
|
|
VerifyToken(ctx context.Context, idToken string) (uid, email, name, avatarURL string, err error)
|
|
}
|
|
|
|
type FirebaseAuth struct {
|
|
client *firebaseAuth.Client
|
|
}
|
|
|
|
// noopTokenVerifier is used in local development when Firebase credentials are not available.
|
|
// It rejects all tokens with a clear error message.
|
|
type noopTokenVerifier struct{}
|
|
|
|
func (n *noopTokenVerifier) VerifyToken(_ context.Context, _ string) (uid, email, name, avatarURL string, err error) {
|
|
return "", "", "", "", fmt.Errorf("Firebase auth is not configured (running in noop mode)")
|
|
}
|
|
|
|
// NewFirebaseAuthOrNoop tries to initialize Firebase auth from the credentials file.
|
|
// If the file is missing, empty, or not a valid service account, it logs a warning
|
|
// and returns a noop verifier that rejects all tokens.
|
|
func NewFirebaseAuthOrNoop(credentialsFile string) (TokenVerifier, error) {
|
|
data, err := os.ReadFile(credentialsFile)
|
|
if err != nil || len(data) == 0 {
|
|
slog.Warn("firebase credentials not found, running without Firebase auth", "file", credentialsFile)
|
|
return &noopTokenVerifier{}, nil
|
|
}
|
|
|
|
var creds map[string]interface{}
|
|
if err := json.Unmarshal(data, &creds); err != nil || creds["type"] == nil {
|
|
slog.Warn("firebase credentials invalid, running without Firebase auth", "file", credentialsFile)
|
|
return &noopTokenVerifier{}, nil
|
|
}
|
|
|
|
fa, err := NewFirebaseAuth(credentialsFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return fa, nil
|
|
}
|
|
|
|
func NewFirebaseAuth(credentialsFile string) (*FirebaseAuth, error) {
|
|
opt := option.WithCredentialsFile(credentialsFile)
|
|
app, err := firebase.NewApp(context.Background(), nil, opt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
client, err := app.Auth(context.Background())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &FirebaseAuth{client: client}, nil
|
|
}
|
|
|
|
func (f *FirebaseAuth) VerifyToken(ctx context.Context, idToken string) (uid, email, name, avatarURL string, err error) {
|
|
token, err := f.client.VerifyIDToken(ctx, idToken)
|
|
if err != nil {
|
|
return "", "", "", "", err
|
|
}
|
|
|
|
uid = token.UID
|
|
|
|
if v, ok := token.Claims["email"].(string); ok {
|
|
email = v
|
|
}
|
|
if v, ok := token.Claims["name"].(string); ok {
|
|
name = v
|
|
}
|
|
if v, ok := token.Claims["picture"].(string); ok {
|
|
avatarURL = v
|
|
}
|
|
|
|
return uid, email, name, avatarURL, nil
|
|
}
|