- Rename catalog: ingredient/* → product/* (canonical_name, barcode, nutrition per 100g)
- Rename pantry: product/* → userproduct/* (user-owned items with expiry)
- Squash migrations into single 001_initial_schema.sql (clean-db baseline)
- product_categories: add English canonical name column; fix COALESCE in queries
- Remove product_translations: product names are stored in their original language
- Add default_unit_name to product API responses via unit_translations JOIN
- Add cmd/importoff: bulk import from OpenFoodFacts JSONL dump (COPY + ON CONFLICT)
- Diary: support product_id entries alongside dish_id (CHECK num_nonnulls = 1)
- Home: getLoggedCalories joins both recipes and catalog products
- Flutter: rename models/providers/services to match backend rename
- Flutter: add barcode scan flow for diary (mobile_scanner, product_portion_sheet)
- Flutter: localise 6 new keys across 12 languages (barcode scan, portion weight)
- Routes: GET /products/search, GET /products/barcode/{barcode}, /user-products
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
216 lines
7.5 KiB
Go
216 lines
7.5 KiB
Go
package main
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/food-ai/backend/internal/domain/auth"
|
|
"github.com/food-ai/backend/internal/infra/config"
|
|
"github.com/food-ai/backend/internal/domain/diary"
|
|
"github.com/food-ai/backend/internal/domain/dish"
|
|
"github.com/food-ai/backend/internal/adapters/kafka"
|
|
"github.com/food-ai/backend/internal/adapters/openai"
|
|
"github.com/food-ai/backend/internal/domain/home"
|
|
"github.com/food-ai/backend/internal/domain/menu"
|
|
"github.com/food-ai/backend/internal/infra/middleware"
|
|
"github.com/food-ai/backend/internal/adapters/pexels"
|
|
"github.com/food-ai/backend/internal/domain/product"
|
|
"github.com/food-ai/backend/internal/domain/recipe"
|
|
"github.com/food-ai/backend/internal/domain/recognition"
|
|
"github.com/food-ai/backend/internal/domain/recommendation"
|
|
"github.com/food-ai/backend/internal/domain/savedrecipe"
|
|
"github.com/food-ai/backend/internal/domain/cuisine"
|
|
"github.com/food-ai/backend/internal/infra/server"
|
|
"github.com/food-ai/backend/internal/domain/tag"
|
|
"github.com/food-ai/backend/internal/domain/units"
|
|
"github.com/food-ai/backend/internal/domain/user"
|
|
"github.com/food-ai/backend/internal/domain/userproduct"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Newtypes for config primitives — prevents Wire type collisions.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Newtypes for list handlers — prevents Wire type collisions on http.HandlerFunc.
|
|
type unitsListHandler http.HandlerFunc
|
|
type cuisineListHandler http.HandlerFunc
|
|
type tagListHandler http.HandlerFunc
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type openaiAPIKey string
|
|
type pexelsAPIKey string
|
|
type jwtSecret string
|
|
type jwtAccessDuration time.Duration
|
|
type jwtRefreshDuration time.Duration
|
|
type allowedOrigins []string
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Config extractors
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func newOpenAIAPIKey(appConfig *config.Config) openaiAPIKey {
|
|
return openaiAPIKey(appConfig.OpenAIAPIKey)
|
|
}
|
|
|
|
func newPexelsAPIKey(appConfig *config.Config) pexelsAPIKey {
|
|
return pexelsAPIKey(appConfig.PexelsAPIKey)
|
|
}
|
|
|
|
func newJWTSecret(appConfig *config.Config) jwtSecret {
|
|
return jwtSecret(appConfig.JWTSecret)
|
|
}
|
|
|
|
func newJWTAccessDuration(appConfig *config.Config) jwtAccessDuration {
|
|
return jwtAccessDuration(appConfig.JWTAccessDuration)
|
|
}
|
|
|
|
func newJWTRefreshDuration(appConfig *config.Config) jwtRefreshDuration {
|
|
return jwtRefreshDuration(appConfig.JWTRefreshDuration)
|
|
}
|
|
|
|
func newAllowedOrigins(appConfig *config.Config) allowedOrigins {
|
|
return allowedOrigins(appConfig.AllowedOrigins)
|
|
}
|
|
|
|
// newFirebaseCredentialsFile is the only string that reaches NewFirebaseAuthOrNoop,
|
|
// so no newtype is needed here.
|
|
func newFirebaseCredentialsFile(appConfig *config.Config) string {
|
|
return appConfig.FirebaseCredentialsFile
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Constructor wrappers for functions that accept primitive newtypes
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func newOpenAIClient(key openaiAPIKey) *openai.Client {
|
|
return openai.NewClient(string(key))
|
|
}
|
|
|
|
func newPexelsClient(key pexelsAPIKey) *pexels.Client {
|
|
return pexels.NewClient(string(key))
|
|
}
|
|
|
|
func newJWTManager(
|
|
secret jwtSecret,
|
|
accessDuration jwtAccessDuration,
|
|
refreshDuration jwtRefreshDuration,
|
|
) *auth.JWTManager {
|
|
return auth.NewJWTManager(string(secret), time.Duration(accessDuration), time.Duration(refreshDuration))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// List handler providers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func newUnitsListHandler(pool *pgxpool.Pool) unitsListHandler {
|
|
return unitsListHandler(units.NewListHandler(pool))
|
|
}
|
|
|
|
func newCuisineListHandler(pool *pgxpool.Pool) cuisineListHandler {
|
|
return cuisineListHandler(cuisine.NewListHandler(pool))
|
|
}
|
|
|
|
func newTagListHandler(pool *pgxpool.Pool) tagListHandler {
|
|
return tagListHandler(tag.NewListHandler(pool))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// newRouter wraps server.NewRouter to accept newtypes.
|
|
func newRouter(
|
|
pool *pgxpool.Pool,
|
|
authHandler *auth.Handler,
|
|
userHandler *user.Handler,
|
|
recommendationHandler *recommendation.Handler,
|
|
savedRecipeHandler *savedrecipe.Handler,
|
|
productHandler *product.Handler,
|
|
userProductHandler *userproduct.Handler,
|
|
recognitionHandler *recognition.Handler,
|
|
menuHandler *menu.Handler,
|
|
diaryHandler *diary.Handler,
|
|
homeHandler *home.Handler,
|
|
dishHandler *dish.Handler,
|
|
recipeHandler *recipe.Handler,
|
|
authMiddleware func(http.Handler) http.Handler,
|
|
origins allowedOrigins,
|
|
unitsHandler unitsListHandler,
|
|
cuisineHandler cuisineListHandler,
|
|
tagHandler tagListHandler,
|
|
) http.Handler {
|
|
return server.NewRouter(
|
|
pool,
|
|
authHandler,
|
|
userHandler,
|
|
recommendationHandler,
|
|
savedRecipeHandler,
|
|
productHandler,
|
|
userProductHandler,
|
|
recognitionHandler,
|
|
menuHandler,
|
|
diaryHandler,
|
|
homeHandler,
|
|
dishHandler,
|
|
recipeHandler,
|
|
authMiddleware,
|
|
[]string(origins),
|
|
http.HandlerFunc(unitsHandler),
|
|
http.HandlerFunc(cuisineHandler),
|
|
http.HandlerFunc(tagHandler),
|
|
)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// jwtAdapter — adapts *auth.JWTManager to middleware.AccessTokenValidator.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type jwtAdapter struct {
|
|
jwtManager *auth.JWTManager
|
|
}
|
|
|
|
func newJWTAdapter(jm *auth.JWTManager) *jwtAdapter {
|
|
return &jwtAdapter{jwtManager: jm}
|
|
}
|
|
|
|
func (adapter *jwtAdapter) ValidateAccessToken(tokenStr string) (*middleware.TokenClaims, error) {
|
|
claims, validationError := adapter.jwtManager.ValidateAccessToken(tokenStr)
|
|
if validationError != nil {
|
|
return nil, validationError
|
|
}
|
|
return &middleware.TokenClaims{
|
|
UserID: claims.UserID,
|
|
Plan: claims.Plan,
|
|
}, nil
|
|
}
|
|
|
|
// newAuthMiddleware wraps middleware.Auth for Wire injection.
|
|
func newAuthMiddleware(validator middleware.AccessTokenValidator) func(http.Handler) http.Handler {
|
|
return middleware.Auth(validator)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Kafka providers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func newKafkaProducer(appConfig *config.Config) (*kafka.Producer, error) {
|
|
return kafka.NewProducer(appConfig.KafkaBrokers)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Interface assertions (compile-time checks)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
var _ middleware.AccessTokenValidator = (*jwtAdapter)(nil)
|
|
var _ menu.PhotoSearcher = (*pexels.Client)(nil)
|
|
var _ menu.UserLoader = (*user.Repository)(nil)
|
|
var _ menu.ProductLister = (*userproduct.Repository)(nil)
|
|
var _ menu.RecipeSaver = (*dish.Repository)(nil)
|
|
var _ recommendation.PhotoSearcher = (*pexels.Client)(nil)
|
|
var _ recommendation.UserLoader = (*user.Repository)(nil)
|
|
var _ recommendation.ProductLister = (*userproduct.Repository)(nil)
|
|
var _ recognition.ProductRepository = (*product.Repository)(nil)
|
|
var _ recognition.KafkaPublisher = (*kafka.Producer)(nil)
|
|
var _ recognition.JobRepository = (*recognition.PostgresJobRepository)(nil)
|
|
var _ user.UserRepository = (*user.Repository)(nil)
|