907 lines
27 KiB
Go
907 lines
27 KiB
Go
package auth
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"photum-backend/internal/profissionais"
|
|
"photum-backend/internal/storage"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type Handler struct {
|
|
service *Service
|
|
s3Service *storage.S3Service
|
|
}
|
|
|
|
func NewHandler(service *Service, s3Service *storage.S3Service) *Handler {
|
|
return &Handler{service: service, s3Service: s3Service}
|
|
}
|
|
|
|
type uploadURLRequest struct {
|
|
Filename string `json:"filename" binding:"required"`
|
|
ContentType string `json:"content_type" binding:"required"`
|
|
}
|
|
|
|
// GetUploadURL godoc
|
|
// @Summary Get S3 Presigned URL for upload
|
|
// @Description Get a pre-signed URL to upload a file directly to S3/Civo
|
|
// @Tags auth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body uploadURLRequest true "Upload URL Request"
|
|
// @Success 200 {object} map[string]string
|
|
// @Router /auth/upload-url [post]
|
|
func (h *Handler) GetUploadURL(c *gin.Context) {
|
|
var req uploadURLRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
uploadURL, publicURL, err := h.s3Service.GeneratePresignedURL(req.Filename, req.ContentType)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"upload_url": uploadURL,
|
|
"public_url": publicURL,
|
|
})
|
|
}
|
|
|
|
type registerRequest struct {
|
|
Email string `json:"email" binding:"required,email"`
|
|
Senha string `json:"senha" binding:"required,min=6"`
|
|
Role string `json:"role" binding:"required"`
|
|
Nome string `json:"nome" binding:"required"`
|
|
Telefone string `json:"telefone"`
|
|
TipoProfissional string `json:"professional_type"`
|
|
EmpresaID string `json:"empresa_id"`
|
|
Regiao string `json:"regiao"`
|
|
CpfCnpj string `json:"cpf_cnpj"`
|
|
Cep string `json:"cep"`
|
|
Endereco string `json:"endereco"`
|
|
Numero string `json:"numero"`
|
|
Complemento string `json:"complemento"`
|
|
Bairro string `json:"bairro"`
|
|
Cidade string `json:"cidade"`
|
|
Estado string `json:"estado"`
|
|
}
|
|
|
|
// Register godoc
|
|
// @Summary Register a new user
|
|
// @Description Register a new user with email, password, name, phone, role and professional type
|
|
// @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
|
|
if req.Role == "BUSINESS_OWNER" {
|
|
profData = &profissionais.CreateProfissionalInput{
|
|
Nome: req.Nome,
|
|
Whatsapp: &req.Telefone,
|
|
}
|
|
}
|
|
|
|
var empresaIDPtr *string
|
|
if req.EmpresaID != "" {
|
|
empresaIDPtr = &req.EmpresaID
|
|
}
|
|
|
|
regiao := req.Regiao
|
|
if regiao == "" {
|
|
regiao = req.Estado
|
|
}
|
|
if regiao == "" {
|
|
regiao = c.GetString("regiao")
|
|
}
|
|
if regiao == "" {
|
|
regiao = c.GetHeader("x-regiao")
|
|
}
|
|
// Default to SP if still empty
|
|
if regiao == "" {
|
|
regiao = "SP"
|
|
}
|
|
|
|
user, err := h.service.Register(c.Request.Context(), req.Email, req.Senha, req.Role, req.Nome, req.Telefone, req.TipoProfissional, empresaIDPtr, profData, regiao, req.CpfCnpj, req.Cep, req.Endereco, req.Numero, req.Complemento, req.Bairro, req.Cidade, req.Estado)
|
|
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,
|
|
})
|
|
|
|
// Calculate MaxAge for access token cookie based on role
|
|
accessMaxAge := 180 * 60 // Default 3 hours
|
|
if req.Role == "RESEARCHER" {
|
|
accessMaxAge = 30 * 24 * 60 * 60 // 30 days
|
|
}
|
|
|
|
// 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: accessMaxAge,
|
|
})
|
|
|
|
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"`
|
|
Name string `json:"name,omitempty"`
|
|
Phone string `json:"phone,omitempty"`
|
|
CompanyID string `json:"company_id,omitempty"`
|
|
CompanyName string `json:"company_name,omitempty"`
|
|
AllowedRegions []string `json:"allowed_regions"`
|
|
CpfCnpj string `json:"cpf_cnpj,omitempty"`
|
|
Cep string `json:"cep,omitempty"`
|
|
Endereco string `json:"endereco,omitempty"`
|
|
Numero string `json:"numero,omitempty"`
|
|
Complemento string `json:"complemento,omitempty"`
|
|
Bairro string `json:"bairro,omitempty"`
|
|
Cidade string `json:"cidade,omitempty"`
|
|
Estado string `json:"estado,omitempty"`
|
|
}
|
|
|
|
// 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,
|
|
})
|
|
|
|
// Calculate MaxAge for access token cookie based on role
|
|
accessMaxAge := 180 * 60 // Default 3 hours
|
|
if user.Role == "RESEARCHER" {
|
|
accessMaxAge = 30 * 24 * 60 * 60 // 30 days
|
|
}
|
|
|
|
// 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: accessMaxAge,
|
|
})
|
|
|
|
// Handle Nullable Fields
|
|
var companyID, companyName string
|
|
if user.EmpresaID.Valid {
|
|
companyID = uuid.UUID(user.EmpresaID.Bytes).String()
|
|
}
|
|
if user.EmpresaNome.Valid {
|
|
companyName = user.EmpresaNome.String
|
|
}
|
|
|
|
// Prepare response
|
|
uResp := userResponse{
|
|
ID: uuid.UUID(user.ID.Bytes).String(),
|
|
Email: user.Email,
|
|
Role: user.Role,
|
|
Ativo: user.Ativo, // Added this back from original
|
|
Name: user.Nome,
|
|
CompanyID: companyID,
|
|
CompanyName: companyName,
|
|
AllowedRegions: user.RegioesPermitidas,
|
|
Phone: "",
|
|
}
|
|
|
|
// Helper to get phone from profData or Client Data
|
|
if user.Role == "PHOTOGRAPHER" && profData != nil {
|
|
if profData.Whatsapp.Valid {
|
|
uResp.Phone = profData.Whatsapp.String
|
|
}
|
|
} else if user.Role == "EVENT_OWNER" {
|
|
// Fetch Client Data
|
|
clientData, err := h.service.GetClientData(c.Request.Context(), uResp.ID)
|
|
if err == nil && clientData != nil {
|
|
if clientData.Telefone.Valid {
|
|
uResp.Phone = clientData.Telefone.String
|
|
}
|
|
if clientData.CpfCnpj.Valid {
|
|
uResp.CpfCnpj = clientData.CpfCnpj.String
|
|
}
|
|
if clientData.Cep.Valid {
|
|
uResp.Cep = clientData.Cep.String
|
|
}
|
|
if clientData.Endereco.Valid {
|
|
uResp.Endereco = clientData.Endereco.String
|
|
}
|
|
if clientData.Numero.Valid {
|
|
uResp.Numero = clientData.Numero.String
|
|
}
|
|
if clientData.Complemento.Valid {
|
|
uResp.Complemento = clientData.Complemento.String
|
|
}
|
|
if clientData.Bairro.Valid {
|
|
uResp.Bairro = clientData.Bairro.String
|
|
}
|
|
if clientData.Cidade.Valid {
|
|
uResp.Cidade = clientData.Cidade.String
|
|
}
|
|
if clientData.Estado.Valid {
|
|
uResp.Estado = clientData.Estado.String
|
|
}
|
|
// Use client name if available ?? Or user name is fine.
|
|
// Usually user.Name comes from `usuarios` table, but `cadastro_clientes` also has nome.
|
|
// Let's stick to user.Name for consistency, usually they should be same.
|
|
}
|
|
}
|
|
|
|
resp := loginResponse{
|
|
AccessToken: tokenPair.AccessToken,
|
|
ExpiresAt: "2025-...",
|
|
User: uResp,
|
|
}
|
|
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": "", // Deprecated/Removed from query
|
|
"functions": profData.Functions,
|
|
"equipamentos": profData.Equipamentos.String,
|
|
"avatar_url": profData.AvatarUrl.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
|
|
}
|
|
|
|
// Set new access_token cookie
|
|
http.SetCookie(c.Writer, &http.Cookie{
|
|
Name: "access_token",
|
|
Value: accessToken,
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
Secure: false,
|
|
SameSite: http.SameSiteStrictMode,
|
|
MaxAge: 180 * 60, // 3 hours
|
|
})
|
|
|
|
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"})
|
|
}
|
|
|
|
// Me godoc
|
|
// @Summary Get current user
|
|
// @Description Get current authenticated user
|
|
// @Tags auth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Success 200 {object} loginResponse
|
|
// @Router /api/me [get]
|
|
func (h *Handler) Me(c *gin.Context) {
|
|
// User ID comes from context (AuthMiddleware)
|
|
userID, exists := c.Get("userID")
|
|
if !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
return
|
|
}
|
|
|
|
// We can fetch fresh user data
|
|
user, err := h.service.GetUser(c.Request.Context(), userID.(string))
|
|
if err != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not found"})
|
|
return
|
|
}
|
|
|
|
var empresaNome string
|
|
if user.EmpresaNome.Valid {
|
|
empresaNome = user.EmpresaNome.String
|
|
}
|
|
var empresaID string
|
|
if user.EmpresaID.Valid {
|
|
empresaID = uuid.UUID(user.EmpresaID.Bytes).String()
|
|
}
|
|
|
|
// Ensure valid slice for JSON response (avoid null)
|
|
allowedRegions := make([]string, 0)
|
|
if user.RegioesPermitidas != nil {
|
|
allowedRegions = append(allowedRegions, user.RegioesPermitidas...)
|
|
}
|
|
|
|
uResp := userResponse{
|
|
ID: uuid.UUID(user.ID.Bytes).String(),
|
|
Email: user.Email,
|
|
Role: user.Role,
|
|
Ativo: user.Ativo,
|
|
Name: user.Nome,
|
|
Phone: user.Whatsapp, // Default to user.Whatsapp
|
|
CompanyName: empresaNome,
|
|
CompanyID: empresaID,
|
|
AllowedRegions: allowedRegions,
|
|
}
|
|
|
|
if user.Role == "EVENT_OWNER" {
|
|
clientData, err := h.service.GetClientData(c.Request.Context(), uResp.ID)
|
|
if err == nil && clientData != nil {
|
|
if clientData.Telefone.Valid {
|
|
uResp.Phone = clientData.Telefone.String
|
|
}
|
|
if clientData.CpfCnpj.Valid {
|
|
uResp.CpfCnpj = clientData.CpfCnpj.String
|
|
}
|
|
if clientData.Cep.Valid {
|
|
uResp.Cep = clientData.Cep.String
|
|
}
|
|
if clientData.Endereco.Valid {
|
|
uResp.Endereco = clientData.Endereco.String
|
|
}
|
|
if clientData.Numero.Valid {
|
|
uResp.Numero = clientData.Numero.String
|
|
}
|
|
if clientData.Complemento.Valid {
|
|
uResp.Complemento = clientData.Complemento.String
|
|
}
|
|
if clientData.Bairro.Valid {
|
|
uResp.Bairro = clientData.Bairro.String
|
|
}
|
|
if clientData.Cidade.Valid {
|
|
uResp.Cidade = clientData.Cidade.String
|
|
}
|
|
if clientData.Estado.Valid {
|
|
uResp.Estado = clientData.Estado.String
|
|
}
|
|
}
|
|
}
|
|
|
|
resp := loginResponse{
|
|
User: uResp,
|
|
}
|
|
|
|
if user.Role == "PHOTOGRAPHER" || user.Role == "BUSINESS_OWNER" {
|
|
regiao := c.GetString("regiao")
|
|
if regiao == "" {
|
|
regiao = c.GetHeader("x-regiao")
|
|
}
|
|
|
|
profData, err := h.service.GetProfessionalByUserID(c.Request.Context(), uuid.UUID(user.ID.Bytes).String())
|
|
if err == nil && profData != nil {
|
|
// Update phone from professional data if valid
|
|
if profData.Whatsapp.Valid {
|
|
resp.User.Phone = profData.Whatsapp.String
|
|
}
|
|
|
|
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": "", // Deprecated
|
|
"functions": profData.Functions,
|
|
"equipamentos": profData.Equipamentos.String,
|
|
"avatar_url": profData.AvatarUrl.String,
|
|
}
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, resp)
|
|
}
|
|
|
|
type updateMeRequest struct {
|
|
Nome string `json:"name"`
|
|
Telefone string `json:"phone"`
|
|
CpfCnpj string `json:"cpf_cnpj"`
|
|
Cep string `json:"cep"`
|
|
Endereco string `json:"endereco"`
|
|
Numero string `json:"numero"`
|
|
Complemento string `json:"complemento"`
|
|
Bairro string `json:"bairro"`
|
|
Cidade string `json:"cidade"`
|
|
Estado string `json:"estado"`
|
|
}
|
|
|
|
// UpdateMe godoc
|
|
// @Summary Update current user profile
|
|
// @Description Update profile information (Name, Phone, Address). Currently for EVENT_OWNER.
|
|
// @Tags auth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Success 200 {object} map[string]string
|
|
// @Failure 400 {object} map[string]string
|
|
// @Failure 500 {object} map[string]string
|
|
// @Router /api/me [put]
|
|
func (h *Handler) UpdateMe(c *gin.Context) {
|
|
userID, exists := c.Get("userID")
|
|
if !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
return
|
|
}
|
|
|
|
var req updateMeRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
// For now, we assume this is mostly for Clients (EVENT_OWNER)
|
|
// based on the fields provided (address, cpf, etc).
|
|
// Future: Check role and call appropriate service if needed.
|
|
// But UpdateClientData uses "UpdateCadastroCliente" which is linked to user_id.
|
|
// If the user does not have a client record, it might fail or do nothing if query uses WHERE exists?
|
|
// The query uses UPDATE, so if no row, no update.
|
|
|
|
// We should probably check role or just try to update.
|
|
// But to be safe, let's just call UpdateClientData.
|
|
// If the user is a Photographer, they should use the /profissionais/me PUT (if exists) or similar.
|
|
// But wait, the user complaint is about Clients.
|
|
|
|
err := h.service.UpdateClientData(c.Request.Context(), userID.(string), UpdateClientInput{
|
|
Nome: req.Nome,
|
|
Telefone: req.Telefone,
|
|
CpfCnpj: req.CpfCnpj,
|
|
Cep: req.Cep,
|
|
Endereco: req.Endereco,
|
|
Numero: req.Numero,
|
|
Complemento: req.Complemento,
|
|
Bairro: req.Bairro,
|
|
Cidade: req.Cidade,
|
|
Estado: req.Estado,
|
|
})
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update profile: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "profile updated successfully"})
|
|
}
|
|
|
|
// 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) {
|
|
regiao := c.GetString("regiao")
|
|
if regiao == "" {
|
|
regiao = c.GetHeader("x-regiao")
|
|
}
|
|
|
|
users, err := h.service.ListPendingUsers(c.Request.Context(), regiao)
|
|
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 {
|
|
nome := u.Nome
|
|
whatsapp := u.Whatsapp
|
|
|
|
var empresaNome string
|
|
if u.EmpresaNome.Valid {
|
|
empresaNome = u.EmpresaNome.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,
|
|
"company_name": empresaNome,
|
|
"professional_type": u.TipoProfissional.String, // Add this
|
|
}
|
|
}
|
|
|
|
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
|
|
regiao := c.GetString("regiao")
|
|
|
|
// If Admin explicitly sends regiao in body, use it (override context)
|
|
if req.Regiao != "" {
|
|
regiao = req.Regiao
|
|
}
|
|
|
|
if regiao == "" {
|
|
// Fallback or Error? Admin creation usually implies target region.
|
|
// Let's assume header is present or default.
|
|
regiao = "SP" // Default for now if missing? Or error?
|
|
}
|
|
user, err := h.service.AdminCreateUser(c.Request.Context(), req.Email, req.Senha, req.Role, req.Nome, req.TipoProfissional, true, regiao, req.EmpresaID, req.Telefone, req.CpfCnpj, req.Cep, req.Endereco, req.Numero, req.Complemento, req.Bairro, req.Cidade, req.Estado)
|
|
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) {
|
|
regiao := c.GetString("regiao")
|
|
if regiao == "" {
|
|
regiao = c.GetHeader("x-regiao")
|
|
}
|
|
users, err := h.service.ListUsers(c.Request.Context(), regiao)
|
|
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 {
|
|
empresaId := ""
|
|
if u.EmpresaID.Valid {
|
|
empresaId = uuid.UUID(u.EmpresaID.Bytes).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": u.Nome,
|
|
"phone": u.Whatsapp,
|
|
"company_name": u.EmpresaNome.String,
|
|
"company_id": empresaId,
|
|
"professional_type": u.TipoProfissional.String,
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
var empresaNome string
|
|
if user.EmpresaNome.Valid {
|
|
empresaNome = user.EmpresaNome.String
|
|
}
|
|
var empresaID string
|
|
if user.EmpresaID.Valid {
|
|
empresaID = uuid.UUID(user.EmpresaID.Bytes).String()
|
|
}
|
|
|
|
resp := loginResponse{
|
|
User: userResponse{
|
|
ID: uuid.UUID(user.ID.Bytes).String(),
|
|
Email: user.Email,
|
|
Role: user.Role,
|
|
Ativo: user.Ativo,
|
|
Name: user.Nome,
|
|
Phone: user.Whatsapp,
|
|
CompanyName: empresaNome,
|
|
CompanyID: empresaID,
|
|
AllowedRegions: user.RegioesPermitidas,
|
|
},
|
|
}
|
|
|
|
c.JSON(http.StatusOK, resp)
|
|
}
|