Inicial
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
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})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user