From 5dc398f0d66e08cc362fb941ee82e493f59737d0 Mon Sep 17 00:00:00 2001 From: dbastrikin Date: Sun, 15 Mar 2026 21:40:18 +0200 Subject: [PATCH] refactor: move Firebase implementation to adapters/firebase, drop pexels interface - Create internal/adapters/firebase/auth.go with Auth, noopAuth, NewAuthOrNoop (renamed from FirebaseAuth, noopTokenVerifier, NewFirebaseAuthOrNoop) - Reduce internal/auth/firebase.go to TokenVerifier interface only - Remove PhotoSearcher interface from adapters/pexels (belongs to consumers) - Update wire.go and wire_gen.go to use firebase.NewAuthOrNoop Co-Authored-By: Claude Sonnet 4.6 --- backend/cmd/server/wire.go | 3 +- backend/cmd/server/wire_gen.go | 3 +- backend/internal/adapters/firebase/auth.go | 88 ++++++++++++++++++++++ backend/internal/adapters/pexels/client.go | 5 -- backend/internal/auth/firebase.go | 83 +------------------- 5 files changed, 93 insertions(+), 89 deletions(-) create mode 100644 backend/internal/adapters/firebase/auth.go diff --git a/backend/cmd/server/wire.go b/backend/cmd/server/wire.go index facc344..98452a5 100644 --- a/backend/cmd/server/wire.go +++ b/backend/cmd/server/wire.go @@ -6,6 +6,7 @@ import ( "net/http" "github.com/food-ai/backend/internal/auth" + "github.com/food-ai/backend/internal/adapters/firebase" "github.com/food-ai/backend/internal/infra/config" "github.com/food-ai/backend/internal/diary" "github.com/food-ai/backend/internal/dish" @@ -37,7 +38,7 @@ func initRouter(appConfig *config.Config, pool *pgxpool.Pool) (http.Handler, err newFirebaseCredentialsFile, // Auth - auth.NewFirebaseAuthOrNoop, + firebase.NewAuthOrNoop, newJWTManager, newJWTAdapter, newAuthMiddleware, diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index 2d0eb6e..9602cdd 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -8,6 +8,7 @@ package main import ( "github.com/food-ai/backend/internal/auth" + "github.com/food-ai/backend/internal/adapters/firebase" "github.com/food-ai/backend/internal/infra/config" "github.com/food-ai/backend/internal/diary" "github.com/food-ai/backend/internal/dish" @@ -28,7 +29,7 @@ import ( func initRouter(appConfig *config.Config, pool *pgxpool.Pool) (http.Handler, error) { string2 := newFirebaseCredentialsFile(appConfig) - tokenVerifier, err := auth.NewFirebaseAuthOrNoop(string2) + tokenVerifier, err := firebase.NewAuthOrNoop(string2) if err != nil { return nil, err } diff --git a/backend/internal/adapters/firebase/auth.go b/backend/internal/adapters/firebase/auth.go new file mode 100644 index 0000000..0233544 --- /dev/null +++ b/backend/internal/adapters/firebase/auth.go @@ -0,0 +1,88 @@ +package firebase + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "os" + + firebasesdk "firebase.google.com/go/v4" + firebaseAuth "firebase.google.com/go/v4/auth" + "google.golang.org/api/option" + + "github.com/food-ai/backend/internal/auth" +) + +// Auth is the Firebase-backed implementation of auth.TokenVerifier. +type Auth struct { + client *firebaseAuth.Client +} + +// noopAuth is used in local development when Firebase credentials are not available. +// It rejects all tokens with a clear error message. +type noopAuth struct{} + +func (noopAuthInstance *noopAuth) VerifyToken(_ context.Context, _ string) (uid, email, name, avatarURL string, verifyError error) { + return "", "", "", "", fmt.Errorf("Firebase auth is not configured (running in noop mode)") +} + +// NewAuthOrNoop 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 NewAuthOrNoop(credentialsFile string) (auth.TokenVerifier, error) { + data, readError := os.ReadFile(credentialsFile) + if readError != nil || len(data) == 0 { + slog.Warn("firebase credentials not found, running without Firebase auth", "file", credentialsFile) + return &noopAuth{}, nil + } + + var creds map[string]interface{} + if unmarshalError := json.Unmarshal(data, &creds); unmarshalError != nil || creds["type"] == nil { + slog.Warn("firebase credentials invalid, running without Firebase auth", "file", credentialsFile) + return &noopAuth{}, nil + } + + firebaseAuth, newAuthError := newAuth(credentialsFile) + if newAuthError != nil { + return nil, newAuthError + } + return firebaseAuth, nil +} + +func newAuth(credentialsFile string) (*Auth, error) { + opt := option.WithCredentialsFile(credentialsFile) + app, appError := firebasesdk.NewApp(context.Background(), nil, opt) + if appError != nil { + return nil, appError + } + + client, clientError := app.Auth(context.Background()) + if clientError != nil { + return nil, clientError + } + + return &Auth{client: client}, nil +} + +// VerifyToken verifies the given Firebase ID token and returns the user's claims. +func (firebaseAuthInstance *Auth) VerifyToken(requestContext context.Context, idToken string) (uid, email, name, avatarURL string, verifyError error) { + token, verifyError := firebaseAuthInstance.client.VerifyIDToken(requestContext, idToken) + if verifyError != nil { + return "", "", "", "", verifyError + } + + uid = token.UID + + if value, ok := token.Claims["email"].(string); ok { + email = value + } + if value, ok := token.Claims["name"].(string); ok { + name = value + } + if value, ok := token.Claims["picture"].(string); ok { + avatarURL = value + } + + return uid, email, name, avatarURL, nil +} diff --git a/backend/internal/adapters/pexels/client.go b/backend/internal/adapters/pexels/client.go index f18e20c..3110252 100644 --- a/backend/internal/adapters/pexels/client.go +++ b/backend/internal/adapters/pexels/client.go @@ -14,11 +14,6 @@ const ( defaultPlaceholder = "https://images.pexels.com/photos/1640777/pexels-photo-1640777.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750" ) -// PhotoSearcher can search for a photo by text query. -type PhotoSearcher interface { - SearchPhoto(ctx context.Context, query string) (string, error) -} - // Client is an HTTP client for the Pexels Photos API. type Client struct { apiKey string diff --git a/backend/internal/auth/firebase.go b/backend/internal/auth/firebase.go index 679194b..413b280 100644 --- a/backend/internal/auth/firebase.go +++ b/backend/internal/auth/firebase.go @@ -1,89 +1,8 @@ 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" -) +import "context" // 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 -}