photum/backend/internal/auth/handler.go

188 lines
5.4 KiB
Go

package auth
import (
"log"
"net/http"
"photum-backend/internal/config"
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5/pgconn"
)
type Handler struct {
service *Service
cfg *config.Config
}
func NewHandler(service *Service, cfg *config.Config) *Handler {
return &Handler{service: service, cfg: cfg}
}
type registerRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"senha" binding:"required,min=6"`
}
// Register godoc
// @Summary Register a new user
// @Description Create a new user account with email and password
// @Tags auth
// @Accept json
// @Produce json
// @Param request body registerRequest true "Register Request"
// @Success 201 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /auth/register [post]
func (h *Handler) Register(c *gin.Context) {
log.Println("Register endpoint called")
var req registerRequest
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("Bind error: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
log.Printf("Attempting to register user: %s", req.Email)
user, err := h.service.Register(c.Request.Context(), req.Email, req.Password)
if err != nil {
if pgErr, ok := err.(*pgconn.PgError); ok && pgErr.Code == "23505" {
c.JSON(http.StatusBadRequest, gin.H{"error": "email já cadastrado"})
return
}
log.Printf("Error registering user: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "falha ao registrar usuário"})
return
}
log.Printf("User registered: %s", user.Email)
c.JSON(http.StatusCreated, gin.H{"id": user.ID, "email": user.Email})
}
type loginRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"senha" binding:"required"`
}
// Login godoc
// @Summary Login user
// @Description Authenticate user and return access token and refresh token
// @Tags auth
// @Accept json
// @Produce json
// @Param request body loginRequest true "Login Request"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Router /auth/login [post]
func (h *Handler) Login(c *gin.Context) {
var req loginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
userAgent := c.Request.UserAgent()
ip := c.ClientIP()
accessToken, refreshToken, accessExp, user, err := h.service.Login(
c.Request.Context(),
req.Email,
req.Password,
userAgent,
ip,
)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
return
}
// Set Refresh Token in Cookie (HttpOnly)
maxAge := h.cfg.JwtRefreshTTLDays * 24 * 60 * 60
secure := h.cfg.AppEnv == "production"
c.SetCookie("refresh_token", refreshToken, maxAge, "/", "", secure, true)
// Use %v for UUID (or .String())
c.JSON(http.StatusOK, gin.H{
"access_token": accessToken,
"expires_at": accessExp,
"user": gin.H{
"id": user.ID, // %v works fine; no formatting needed here
"email": user.Email,
"role": user.Role,
},
})
}
// Refresh godoc
// @Summary Refresh access token
// @Description Get a new access token using a valid refresh token (cookie or body)
// @Tags auth
// @Accept json
// @Produce json
// @Param refresh_token body string false "Refresh Token (optional if in cookie)"
// @Success 200 {object} map[string]interface{}
// @Failure 401 {object} map[string]string
// @Router /auth/refresh [post]
func (h *Handler) Refresh(c *gin.Context) {
// Try to get from cookie first
refreshToken, err := c.Cookie("refresh_token")
if err != nil {
// Try from body if mobile
var req struct {
RefreshToken string `json:"refresh_token"`
}
if err := c.ShouldBindJSON(&req); err == nil {
refreshToken = req.RefreshToken
}
}
if refreshToken == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "refresh token required"})
return
}
accessToken, accessExp, err := h.service.Refresh(c.Request.Context(), refreshToken)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid refresh token"})
return
}
c.JSON(http.StatusOK, gin.H{
"access_token": accessToken,
"expires_at": accessExp,
})
}
// Logout godoc
// @Summary Logout user
// @Description Revoke refresh token and clear cookie
// @Tags auth
// @Accept json
// @Produce json
// @Param refresh_token body string false "Refresh Token (optional if in cookie)"
// @Success 200 {object} map[string]string
// @Router /auth/logout [post]
func (h *Handler) Logout(c *gin.Context) {
refreshToken, err := c.Cookie("refresh_token")
if err != nil {
// Try from body
var req struct {
RefreshToken string `json:"refresh_token"`
}
if err := c.ShouldBindJSON(&req); err == nil {
refreshToken = req.RefreshToken
}
}
if refreshToken != "" {
_ = h.service.Logout(c.Request.Context(), refreshToken)
}
// Clear cookie
secure := h.cfg.AppEnv == "production"
c.SetCookie("refresh_token", "", -1, "/", "", secure, true)
c.JSON(http.StatusOK, gin.H{"message": "logged out"})
}