package auth import ( "context" "strings" "time" "github.com/gofiber/fiber/v2" goredis "github.com/redis/go-redis/v9" "github.com/techxcar/backend/internal/config" redispkg "github.com/techxcar/backend/pkg/redis" ) // LoginRepository is a subset of tenant.Repository used by auth handlers. // Uses local types to avoid a circular import with the tenant package. type LoginRepository interface { GetSuperAdminByEmail(ctx context.Context, email string) (*LoginSuperAdmin, error) GetTenantBySlug(ctx context.Context, slug string) (*LoginTenant, error) GetTenantUserByEmail(ctx context.Context, tenantID, email string) (*LoginUser, error) } type LoginSuperAdmin struct { ID string Email string PasswordHash string } type LoginTenant struct { ID string Status string } type LoginUser struct { ID string Email string PasswordHash string Role string Name string Active bool } const refreshCookieName = "refresh_token" const refreshCookieTTL = 30 * 24 * time.Hour func SetRefreshCookie(c *fiber.Ctx, token string, cfg *config.Config) { c.Cookie(&fiber.Cookie{ Name: refreshCookieName, Value: token, MaxAge: int(refreshCookieTTL.Seconds()), HTTPOnly: true, Secure: cfg.AppEnv == "production", SameSite: "Strict", Path: "/api/v1/auth", }) } type loginRequest struct { Email string `json:"email"` Password string `json:"password"` TenantSlug string `json:"tenant_slug"` } func LoginHandler(repo LoginRepository, rdb *redispkg.Redis, cfg *config.Config) fiber.Handler { return func(c *fiber.Ctx) error { var req loginRequest if err := c.BodyParser(&req); err != nil { return fiber.NewError(400, "corpo do pedido inválido") } req.Email = strings.TrimSpace(strings.ToLower(req.Email)) if req.Email == "" || req.Password == "" { return fiber.NewError(400, "email e password são obrigatórios") } var userID, tenantID, role string userPayload := fiber.Map{} if req.TenantSlug == "" { admin, err := repo.GetSuperAdminByEmail(c.Context(), req.Email) if err != nil || admin == nil || !VerifyPassword(req.Password, admin.PasswordHash) { return fiber.NewError(401, "credenciais inválidas") } userID, tenantID, role = admin.ID, "", "super_admin" userPayload = fiber.Map{ "id": admin.ID, "email": admin.Email, "name": admin.Email, "role": "super_admin", } } else { ten, err := repo.GetTenantBySlug(c.Context(), req.TenantSlug) if err != nil || ten == nil || ten.Status != "active" { return fiber.NewError(401, "credenciais inválidas") } user, err := repo.GetTenantUserByEmail(c.Context(), ten.ID, req.Email) if err != nil || user == nil || !user.Active || !VerifyPassword(req.Password, user.PasswordHash) { return fiber.NewError(401, "credenciais inválidas") } userID, tenantID, role = user.ID, ten.ID, user.Role userPayload = fiber.Map{ "id": user.ID, "email": user.Email, "name": user.Name, "role": user.Role, "tenantId": ten.ID, } } access, err := GenerateAccessToken(userID, tenantID, role, cfg.JWTSecret) if err != nil { return fiber.NewError(500, "erro ao gerar token") } refresh, err := GenerateRefreshToken(userID, tenantID, role, cfg.JWTSecret) if err != nil { return fiber.NewError(500, "erro ao gerar token") } if rdb != nil { ctx := c.Context() rdb.Client.Set(ctx, "refresh:"+userID, refresh, refreshCookieTTL) } SetRefreshCookie(c, refresh, cfg) return c.JSON(fiber.Map{"data": fiber.Map{"access_token": access, "user": userPayload}, "error": nil}) } } func RefreshHandler(rdb *redispkg.Redis, cfg *config.Config) fiber.Handler { return func(c *fiber.Ctx) error { refreshToken := c.Cookies(refreshCookieName) if refreshToken == "" { return fiber.NewError(401, "refresh token em falta") } claims, err := ValidateToken(refreshToken, cfg.JWTSecret) if err != nil { return fiber.NewError(401, "refresh token inválido ou expirado") } if rdb != nil { stored, err := rdb.Client.Get(c.Context(), "refresh:"+claims.UserID).Result() if err == goredis.Nil || stored != refreshToken { return fiber.NewError(401, "sessão inválida") } } access, err := GenerateAccessToken(claims.UserID, claims.TenantID, claims.Role, cfg.JWTSecret) if err != nil { return fiber.NewError(500, "erro ao renovar token") } newRefresh, err := GenerateRefreshToken(claims.UserID, claims.TenantID, claims.Role, cfg.JWTSecret) if err != nil { return fiber.NewError(500, "erro ao renovar token") } if rdb != nil { rdb.Client.Set(c.Context(), "refresh:"+claims.UserID, newRefresh, refreshCookieTTL) } SetRefreshCookie(c, newRefresh, cfg) return c.JSON(fiber.Map{"data": fiber.Map{"access_token": access}, "error": nil}) } } func LogoutHandler(rdb *redispkg.Redis) fiber.Handler { return func(c *fiber.Ctx) error { c.Cookie(&fiber.Cookie{ Name: refreshCookieName, Value: "", MaxAge: -1, HTTPOnly: true, Path: "/api/v1/auth", }) return c.JSON(fiber.Map{"data": nil, "error": nil}) } }