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 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/auth"
|
"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/infra/config"
|
||||||
"github.com/food-ai/backend/internal/diary"
|
"github.com/food-ai/backend/internal/diary"
|
||||||
"github.com/food-ai/backend/internal/dish"
|
"github.com/food-ai/backend/internal/dish"
|
||||||
@@ -37,7 +38,7 @@ func initRouter(appConfig *config.Config, pool *pgxpool.Pool) (http.Handler, err
|
|||||||
newFirebaseCredentialsFile,
|
newFirebaseCredentialsFile,
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
auth.NewFirebaseAuthOrNoop,
|
firebase.NewAuthOrNoop,
|
||||||
newJWTManager,
|
newJWTManager,
|
||||||
newJWTAdapter,
|
newJWTAdapter,
|
||||||
newAuthMiddleware,
|
newAuthMiddleware,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/food-ai/backend/internal/auth"
|
"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/infra/config"
|
||||||
"github.com/food-ai/backend/internal/diary"
|
"github.com/food-ai/backend/internal/diary"
|
||||||
"github.com/food-ai/backend/internal/dish"
|
"github.com/food-ai/backend/internal/dish"
|
||||||
@@ -28,7 +29,7 @@ import (
|
|||||||
|
|
||||||
func initRouter(appConfig *config.Config, pool *pgxpool.Pool) (http.Handler, error) {
|
func initRouter(appConfig *config.Config, pool *pgxpool.Pool) (http.Handler, error) {
|
||||||
string2 := newFirebaseCredentialsFile(appConfig)
|
string2 := newFirebaseCredentialsFile(appConfig)
|
||||||
tokenVerifier, err := auth.NewFirebaseAuthOrNoop(string2)
|
tokenVerifier, err := firebase.NewAuthOrNoop(string2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
88
backend/internal/adapters/firebase/auth.go
Normal file
88
backend/internal/adapters/firebase/auth.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -14,11 +14,6 @@ const (
|
|||||||
defaultPlaceholder = "https://images.pexels.com/photos/1640777/pexels-photo-1640777.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750"
|
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.
|
// Client is an HTTP client for the Pexels Photos API.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
apiKey string
|
apiKey string
|
||||||
|
|||||||
@@ -1,89 +1,8 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import "context"
|
||||||
"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.
|
// TokenVerifier abstracts Firebase token verification for testability.
|
||||||
type TokenVerifier interface {
|
type TokenVerifier interface {
|
||||||
VerifyToken(ctx context.Context, idToken string) (uid, email, name, avatarURL string, err error)
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user