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) }