photum/backend/internal/auth/handler.go
NANDO9322 78c284c28a feat: Implementado a API de backend inicial e a interface de usuário de frontend para autenticação, gerenciamento de usuários e cadastro profissional.
implementação de listagem de usuários admin, padronização de roles e melhorias nos cadastros
Backend:
- Adicionados endpoints administrativos [ListUsers](cci:1://file:///c:/Projetos/photum/backend/internal/auth/service.go:268:0-270:1) e [GetUser](cci:1://file:///c:/Projetos/photum/backend/internal/auth/handler.go:475:0-514:1).
- Padronizadas as constantes de [UserRole](cci:1://file:///c:/Projetos/photum/backend/internal/auth/service.go:202:0-216:1) (`SUPERADMIN`, `BUSINESS_OWNER`, etc.) para alinhar com o frontend.
- Atualizada a função [EnsureDemoUsers](cci:1://file:///c:/Projetos/photum/backend/internal/auth/service.go:230:0-266:1) para migrar usuários existentes para as novas roles.
- Documentação Swagger regenerada.
Frontend:
- Adicionado busca automática de CEP no formulário de Cadastro Profissional (AwesomeAPI).
- Adicionado texto de ajuda e ordenação (priorizando "Não Cadastrado") no select de Empresas.
2025-12-15 11:17:34 -03:00

515 lines
14 KiB
Go

package auth
import (
"net/http"
"strings"
"photum-backend/internal/profissionais"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type Handler struct {
service *Service
}
func NewHandler(service *Service) *Handler {
return &Handler{service: service}
}
type registerRequest struct {
Email string `json:"email" binding:"required,email"`
Senha string `json:"senha" binding:"required,min=6"`
Nome string `json:"nome" binding:"required"`
Telefone string `json:"telefone"`
Role string `json:"role" binding:"required"` // Role is now required
}
// Register godoc
// @Summary Register a new user
// @Description Register a new user with email, password, name, phone and role
// @Tags auth
// @Accept json
// @Produce json
// @Param request body registerRequest true "Register Request"
// @Success 201 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /auth/register [post]
func (h *Handler) Register(c *gin.Context) {
var req registerRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Create professional data only if role is appropriate
var profData *profissionais.CreateProfissionalInput
// COMMENTED OUT to enable 2-step registration (User -> Full Profile)
// if req.Role == RolePhotographer || req.Role == RoleBusinessOwner {
// profData = &profissionais.CreateProfissionalInput{
// Nome: req.Nome,
// Whatsapp: &req.Telefone,
// }
// }
user, err := h.service.Register(c.Request.Context(), req.Email, req.Senha, req.Role, profData)
if err != nil {
if strings.Contains(err.Error(), "duplicate key") {
c.JSON(http.StatusConflict, gin.H{"error": "email already registered"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Auto-login after registration
tokenPair, _, _, err := h.service.Login(c.Request.Context(), req.Email, req.Senha)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "user created but failed to auto-login"})
return
}
http.SetCookie(c.Writer, &http.Cookie{
Name: "refresh_token",
Value: tokenPair.RefreshToken,
Path: "/auth/refresh",
HttpOnly: true,
Secure: false,
SameSite: http.SameSiteStrictMode,
MaxAge: 30 * 24 * 60 * 60,
})
// Set access_token cookie for fallback
http.SetCookie(c.Writer, &http.Cookie{
Name: "access_token",
Value: tokenPair.AccessToken,
Path: "/",
HttpOnly: true,
Secure: false,
SameSite: http.SameSiteStrictMode,
MaxAge: 15 * 60,
})
c.JSON(http.StatusCreated, gin.H{
"message": "user created",
"access_token": tokenPair.AccessToken,
"user": gin.H{
"id": uuid.UUID(user.ID.Bytes).String(),
"email": user.Email,
"role": user.Role,
"ativo": user.Ativo,
},
})
}
type loginRequest struct {
Email string `json:"email" binding:"required,email" example:"admin@photum.com"`
Senha string `json:"senha" binding:"required,min=6" example:"123456"`
}
type loginResponse struct {
AccessToken string `json:"access_token"`
ExpiresAt string `json:"expires_at"`
User userResponse `json:"user"`
Profissional interface{} `json:"profissional,omitempty"`
}
type userResponse struct {
ID string `json:"id"`
Email string `json:"email"`
Role string `json:"role"`
Ativo bool `json:"ativo"`
}
// Login godoc
// @Summary Login
// @Description Login with email and password
// @Tags auth
// @Accept json
// @Produce json
// @Param request body loginRequest true "Login Request"
// @Success 200 {object} loginResponse
// @Failure 401 {object} map[string]string
// @Failure 500 {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
}
tokenPair, user, profData, err := h.service.Login(c.Request.Context(), req.Email, req.Senha)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
return
}
http.SetCookie(c.Writer, &http.Cookie{
Name: "refresh_token",
Value: tokenPair.RefreshToken,
Path: "/auth/refresh",
HttpOnly: true,
Secure: false,
SameSite: http.SameSiteStrictMode,
MaxAge: 30 * 24 * 60 * 60,
})
// Set access_token cookie for fallback
http.SetCookie(c.Writer, &http.Cookie{
Name: "access_token",
Value: tokenPair.AccessToken,
Path: "/",
HttpOnly: true,
Secure: false,
SameSite: http.SameSiteStrictMode,
MaxAge: 15 * 60, // 15 mins
})
resp := loginResponse{
AccessToken: tokenPair.AccessToken,
ExpiresAt: "2025-...", // logic to calculate if needed, or remove field
User: userResponse{
ID: uuid.UUID(user.ID.Bytes).String(),
Email: user.Email,
Role: user.Role,
Ativo: user.Ativo,
},
}
if profData != nil {
resp.Profissional = map[string]interface{}{
"id": uuid.UUID(profData.ID.Bytes).String(),
"nome": profData.Nome,
"funcao_profissional_id": uuid.UUID(profData.FuncaoProfissionalID.Bytes).String(),
"funcao_profissional": profData.FuncaoNome.String,
"equipamentos": profData.Equipamentos.String,
}
}
c.JSON(http.StatusOK, resp)
}
// Refresh godoc
// @Summary Refresh access token
// @Description Get a new access token using a valid refresh token
// @Tags auth
// @Accept json
// @Produce json
// @Param refresh_token body string false "Refresh Token"
// @Success 200 {object} map[string]interface{}
// @Failure 401 {object} map[string]string
// @Router /auth/refresh [post]
func (h *Handler) Refresh(c *gin.Context) {
refreshToken, err := c.Cookie("refresh_token")
if err != nil {
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"
// @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 {
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)
}
c.SetCookie("refresh_token", "", -1, "/", "", false, true)
c.JSON(http.StatusOK, gin.H{"message": "logged out"})
}
// ListPending godoc
// @Summary List pending users
// @Description List users with ativo=false
// @Tags admin
// @Accept json
// @Produce json
// @Success 200 {array} map[string]interface{}
// @Failure 500 {object} map[string]string
// @Security BearerAuth
// @Router /api/admin/users/pending [get]
func (h *Handler) ListPending(c *gin.Context) {
users, err := h.service.ListPendingUsers(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Map to response
// The generated type ListUsuariosPendingRow fields are:
// ID pgtype.UUID
// Email string
// Role string
// Ativo bool
// CriadoEm pgtype.Timestamptz
// Nome pgtype.Text
// Whatsapp pgtype.Text
resp := make([]map[string]interface{}, len(users))
for i, u := range users {
var nome string
if u.Nome.Valid {
nome = u.Nome.String
}
var whatsapp string
if u.Whatsapp.Valid {
whatsapp = u.Whatsapp.String
}
resp[i] = map[string]interface{}{
"id": uuid.UUID(u.ID.Bytes).String(),
"email": u.Email,
"role": u.Role,
"ativo": u.Ativo,
"created_at": u.CriadoEm.Time,
"name": nome, // Mapped to name for frontend compatibility
"phone": whatsapp,
}
}
c.JSON(http.StatusOK, resp)
}
// Approve godoc
// @Summary Approve user
// @Description Set user ativo=true
// @Tags admin
// @Accept json
// @Produce json
// @Param id path string true "User ID"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security BearerAuth
// @Router /api/admin/users/{id}/approve [patch]
func (h *Handler) Approve(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "id required"})
return
}
err := h.service.ApproveUser(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "user approved"})
}
// AdminCreateUser godoc
// @Summary Create user (Admin)
// @Description Create a new user with specific role (Admin only)
// @Tags admin
// @Accept json
// @Produce json
// @Param request body registerRequest true "Create User Request"
// @Success 201 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security BearerAuth
// @Router /api/admin/users [post]
func (h *Handler) AdminCreateUser(c *gin.Context) {
var req registerRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Just reuse the request struct but call AdminCreateUser service
user, err := h.service.AdminCreateUser(c.Request.Context(), req.Email, req.Senha, req.Role, req.Nome)
if err != nil {
if strings.Contains(err.Error(), "duplicate key") {
c.JSON(http.StatusConflict, gin.H{"error": "email already registered"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"message": "user created",
"id": uuid.UUID(user.ID.Bytes).String(),
"email": user.Email,
})
}
type updateRoleRequest struct {
Role string `json:"role" binding:"required"`
}
// UpdateRole godoc
// @Summary Update user role
// @Description Update user role (Admin only)
// @Tags admin
// @Accept json
// @Produce json
// @Param id path string true "User ID"
// @Param request body updateRoleRequest true "Update Role Request"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security BearerAuth
// @Router /api/admin/users/{id}/role [patch]
func (h *Handler) UpdateRole(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "id required"})
return
}
var req updateRoleRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err := h.service.UpdateUserRole(c.Request.Context(), id, req.Role)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "role updated"})
}
// DeleteUser godoc
// @Summary Delete user
// @Description Delete user (Admin only)
// @Tags admin
// @Accept json
// @Produce json
// @Param id path string true "User ID"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security BearerAuth
// @Router /api/admin/users/{id} [delete]
func (h *Handler) DeleteUser(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "id required"})
return
}
err := h.service.DeleteUser(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "user deleted"})
}
// ListUsers godoc
// @Summary List all users
// @Description List all users (Admin only)
// @Tags admin
// @Accept json
// @Produce json
// @Success 200 {array} map[string]interface{}
// @Failure 500 {object} map[string]string
// @Security BearerAuth
// @Router /api/admin/users [get]
func (h *Handler) ListUsers(c *gin.Context) {
users, err := h.service.ListUsers(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
resp := make([]map[string]interface{}, len(users))
for i, u := range users {
resp[i] = map[string]interface{}{
"id": uuid.UUID(u.ID.Bytes).String(),
"email": u.Email,
"role": u.Role,
"ativo": u.Ativo,
"created_at": u.CriadoEm.Time,
}
}
c.JSON(http.StatusOK, resp)
}
// GetUser godoc
// @Summary Get user by ID
// @Description Get user details by ID (Admin only)
// @Tags admin
// @Accept json
// @Produce json
// @Param id path string true "User ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Security BearerAuth
// @Router /api/admin/users/{id} [get]
func (h *Handler) GetUser(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "id required"})
return
}
user, err := h.service.GetUser(c.Request.Context(), id)
if err != nil {
if strings.Contains(err.Error(), "no rows") {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
resp := map[string]interface{}{
"id": uuid.UUID(user.ID.Bytes).String(),
"email": user.Email,
"role": user.Role,
"ativo": user.Ativo,
"created_at": user.CriadoEm.Time,
}
c.JSON(http.StatusOK, resp)
}