refactor: introduce internal/domain/ layer, rename model.go → entity.go

Move all business-logic packages from internal/ root into internal/domain/:
  auth, cuisine, diary, dish, home, ingredient, language, menu, product,
  recipe, recognition, recommendation, savedrecipe, tag, units, user

Rename model.go → entity.go in packages that hold domain entities:
  diary, dish, home, ingredient, menu, product, recipe, savedrecipe, user

Update all import paths accordingly (adapters, infra/server, cmd/server,
tests). No logic changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dbastrikin
2026-03-15 22:12:07 +02:00
parent 6548f868c3
commit 6594013b53
58 changed files with 73 additions and 73 deletions

View File

@@ -0,0 +1,106 @@
package auth
import (
"encoding/json"
"log/slog"
"net/http"
"github.com/food-ai/backend/internal/infra/middleware"
)
const maxRequestBodySize = 1 << 20 // 1 MB
type Handler struct {
service *Service
}
func NewHandler(service *Service) *Handler {
return &Handler{service: service}
}
type loginRequest struct {
FirebaseToken string `json:"firebase_token"`
}
type refreshRequest struct {
RefreshToken string `json:"refresh_token"`
}
func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodySize)
var req loginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErrorJSON(w, http.StatusBadRequest, "invalid request body")
return
}
if req.FirebaseToken == "" {
writeErrorJSON(w, http.StatusBadRequest, "firebase_token is required")
return
}
resp, err := h.service.Login(r.Context(), req.FirebaseToken)
if err != nil {
writeErrorJSON(w, http.StatusUnauthorized, "authentication failed")
return
}
writeJSON(w, http.StatusOK, resp)
}
func (h *Handler) Refresh(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodySize)
var req refreshRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErrorJSON(w, http.StatusBadRequest, "invalid request body")
return
}
if req.RefreshToken == "" {
writeErrorJSON(w, http.StatusBadRequest, "refresh_token is required")
return
}
resp, err := h.service.Refresh(r.Context(), req.RefreshToken)
if err != nil {
writeErrorJSON(w, http.StatusUnauthorized, "invalid refresh token")
return
}
writeJSON(w, http.StatusOK, resp)
}
func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) {
userID := middleware.UserIDFromCtx(r.Context())
if userID == "" {
writeErrorJSON(w, http.StatusUnauthorized, "unauthorized")
return
}
if err := h.service.Logout(r.Context(), userID); err != nil {
writeErrorJSON(w, http.StatusInternalServerError, "logout failed")
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
type errorResponse struct {
Error string `json:"error"`
}
func writeErrorJSON(w http.ResponseWriter, status int, msg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(errorResponse{Error: msg}); err != nil {
slog.Error("failed to write error response", "err", err)
}
}
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(v); err != nil {
slog.Error("failed to write JSON response", "err", err)
}
}

View File

@@ -0,0 +1,69 @@
package auth
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
type JWTManager struct {
secret []byte
accessDuration time.Duration
refreshDuration time.Duration
}
type Claims struct {
UserID string `json:"user_id"`
Plan string `json:"plan"`
jwt.RegisteredClaims
}
func NewJWTManager(secret string, accessDuration, refreshDuration time.Duration) *JWTManager {
return &JWTManager{
secret: []byte(secret),
accessDuration: accessDuration,
refreshDuration: refreshDuration,
}
}
func (j *JWTManager) GenerateAccessToken(userID, plan string) (string, error) {
claims := Claims{
UserID: userID,
Plan: plan,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(j.accessDuration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(j.secret)
}
func (j *JWTManager) GenerateRefreshToken() (string, time.Time) {
token := uuid.Must(uuid.NewV7()).String()
expiresAt := time.Now().Add(j.refreshDuration)
return token, expiresAt
}
func (j *JWTManager) ValidateAccessToken(tokenStr string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return j.secret, nil
})
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, fmt.Errorf("invalid token")
}
return claims, nil
}
func (j *JWTManager) AccessDuration() time.Duration {
return j.accessDuration
}

View File

@@ -0,0 +1,11 @@
package mocks
import "context"
type MockTokenVerifier struct {
VerifyTokenFn func(ctx context.Context, idToken string) (uid, email, name, avatarURL string, err error)
}
func (m *MockTokenVerifier) VerifyToken(ctx context.Context, idToken string) (uid, email, name, avatarURL string, err error) {
return m.VerifyTokenFn(ctx, idToken)
}

View File

@@ -0,0 +1,91 @@
package auth
import (
"context"
"fmt"
"github.com/food-ai/backend/internal/domain/user"
)
type Service struct {
tokenVerifier TokenVerifier
userRepo user.UserRepository
jwtManager *JWTManager
}
func NewService(tokenVerifier TokenVerifier, userRepo user.UserRepository, jwtManager *JWTManager) *Service {
return &Service{
tokenVerifier: tokenVerifier,
userRepo: userRepo,
jwtManager: jwtManager,
}
}
type LoginResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
User *user.User `json:"user"`
}
type RefreshResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
}
func (s *Service) Login(ctx context.Context, firebaseToken string) (*LoginResponse, error) {
uid, email, name, avatarURL, err := s.tokenVerifier.VerifyToken(ctx, firebaseToken)
if err != nil {
return nil, fmt.Errorf("verify firebase token: %w", err)
}
u, err := s.userRepo.UpsertByFirebaseUID(ctx, uid, email, name, avatarURL)
if err != nil {
return nil, fmt.Errorf("upsert user: %w", err)
}
accessToken, err := s.jwtManager.GenerateAccessToken(u.ID, u.Plan)
if err != nil {
return nil, fmt.Errorf("generate access token: %w", err)
}
refreshToken, expiresAt := s.jwtManager.GenerateRefreshToken()
if err := s.userRepo.SetRefreshToken(ctx, u.ID, refreshToken, expiresAt); err != nil {
return nil, fmt.Errorf("set refresh token: %w", err)
}
return &LoginResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresIn: int(s.jwtManager.AccessDuration().Seconds()),
User: u,
}, nil
}
func (s *Service) Refresh(ctx context.Context, refreshToken string) (*RefreshResponse, error) {
u, err := s.userRepo.FindByRefreshToken(ctx, refreshToken)
if err != nil {
return nil, fmt.Errorf("invalid refresh token: %w", err)
}
accessToken, err := s.jwtManager.GenerateAccessToken(u.ID, u.Plan)
if err != nil {
return nil, fmt.Errorf("generate access token: %w", err)
}
newRefreshToken, expiresAt := s.jwtManager.GenerateRefreshToken()
if err := s.userRepo.SetRefreshToken(ctx, u.ID, newRefreshToken, expiresAt); err != nil {
return nil, fmt.Errorf("set refresh token: %w", err)
}
return &RefreshResponse{
AccessToken: accessToken,
RefreshToken: newRefreshToken,
ExpiresIn: int(s.jwtManager.AccessDuration().Seconds()),
}, nil
}
func (s *Service) Logout(ctx context.Context, userID string) error {
return s.userRepo.ClearRefreshToken(ctx, userID)
}

View File

@@ -0,0 +1,8 @@
package auth
import "context"
// TokenVerifier abstracts token verification for testability.
type TokenVerifier interface {
VerifyToken(ctx context.Context, idToken string) (uid, email, name, avatarURL string, err error)
}