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"` Nome string `json:"nome" binding:"required"` Telefone string `json:"telefone"` Role string `json:"role" binding:"required"` EmpresaID string `json:"empresa_id"` TipoProfissional string `json:"tipo_profissional"` // New field } // 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 := 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) 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"` 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"` } // 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 }) // 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 } 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, Name: user.Nome, Phone: user.Whatsapp, CompanyID: companyID, CompanyName: companyName, AllowedRegions: user.RegioesPermitidas, }, } 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 } 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...) } 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: allowedRegions, }, } if user.Role == "PHOTOGRAPHER" || user.Role == "BUSINESS_OWNER" { regiao := c.GetString("regiao") // If regiao is empty, we might skip fetching professional data or default? // For now if empty, GetProfessionalByUserID with valid=true and string="" will likely fail or return empty? // Queries check regiao = $2. If regiao is "", and DB has "SP", it won't match. // So user needs to send header for Me to see pro data. if regiao != "" { profData, err := h.service.GetProfessionalByUserID(c.Request.Context(), uuid.UUID(user.ID.Bytes).String()) if err == nil && 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 "functions": profData.Functions, "equipamentos": profData.Equipamentos.String, "avatar_url": profData.AvatarUrl.String, } } } } c.JSON(http.StatusOK, resp) } // 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 doesn't specify region in header? // Maybe Admin API should accept region in body? // For now use header context. 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) 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 { 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) }