From a337e27f2ae26b66ad7bc89dede2a4c5b100c62e Mon Sep 17 00:00:00 2001 From: NANDO9322 Date: Fri, 12 Dec 2025 13:01:30 -0300 Subject: [PATCH] =?UTF-8?q?feat(auth):=20implementa=C3=A7=C3=A3o=20do=20fl?= =?UTF-8?q?uxo=20de=20cadastro=20duplo=20(clientes=20e=20profissionais)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - backend: atualizado /auth/register para retornar userId e access_token - backend: desabilitada criação automática de perfil parcial no registro - backend: adicionado suporte a cookie access_token no middleware e handlers - frontend: atualizado AuthContext para enviar role e retornar token - frontend: implementado registro de profissional em 2 etapas (Usuário -> Perfil) - frontend: adicionado serviço createProfessional com suporte a header de auth - frontend: definido role correto (EVENT_OWNER) para cadastro de clientes --- backend/internal/auth/handler.go | 65 ++++++++++++++++++++++--- backend/internal/auth/middleware.go | 31 +++++++----- frontend/contexts/AuthContext.tsx | 26 ++++++++-- frontend/pages/ProfessionalRegister.tsx | 62 +++++++++++++++++++++-- frontend/pages/Register.tsx | 3 +- frontend/services/apiService.ts | 40 +++++++++++++++ 6 files changed, 197 insertions(+), 30 deletions(-) diff --git a/backend/internal/auth/handler.go b/backend/internal/auth/handler.go index ec0010d..6e736f0 100644 --- a/backend/internal/auth/handler.go +++ b/backend/internal/auth/handler.go @@ -46,14 +46,15 @@ func (h *Handler) Register(c *gin.Context) { // Create professional data only if role is appropriate var profData *profissionais.CreateProfissionalInput - if req.Role == "profissional" || req.Role == "empresa" { - profData = &profissionais.CreateProfissionalInput{ - Nome: req.Nome, - Whatsapp: &req.Telefone, - } - } + // COMMENTED OUT to enable 2-step registration (User -> Full Profile) + // if req.Role == "profissional" || req.Role == "empresa" { + // profData = &profissionais.CreateProfissionalInput{ + // Nome: req.Nome, + // Whatsapp: &req.Telefone, + // } + // } - _, err := h.service.Register(c.Request.Context(), req.Email, req.Senha, req.Role, profData) + 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"}) @@ -63,7 +64,44 @@ func (h *Handler) Register(c *gin.Context) { return } - c.JSON(http.StatusCreated, gin.H{"message": "user created"}) + // 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 { @@ -119,6 +157,17 @@ func (h *Handler) Login(c *gin.Context) { 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 diff --git a/backend/internal/auth/middleware.go b/backend/internal/auth/middleware.go index d755934..433650e 100644 --- a/backend/internal/auth/middleware.go +++ b/backend/internal/auth/middleware.go @@ -12,20 +12,27 @@ import ( func AuthMiddleware(cfg *config.Config) gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") - if authHeader == "" { - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "authorization header required"}) - return - } - - parts := strings.Split(authHeader, " ") var tokenString string - if len(parts) == 2 && parts[0] == "Bearer" { - tokenString = parts[1] - } else if len(parts) == 1 && parts[0] != "" { - tokenString = parts[0] + + if authHeader != "" { + parts := strings.Split(authHeader, " ") + if len(parts) == 2 && parts[0] == "Bearer" { + tokenString = parts[1] + } else if len(parts) == 1 && parts[0] != "" { + tokenString = parts[0] + } else { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid authorization header format"}) + return + } } else { - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid authorization header format"}) - return + // Try to get from cookie + cookie, err := c.Cookie("access_token") + if err == nil && cookie != "" { + tokenString = cookie + } else { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "authorization header required"}) + return + } } claims, err := ValidateToken(tokenString, cfg.JwtAccessSecret) if err != nil { diff --git a/frontend/contexts/AuthContext.tsx b/frontend/contexts/AuthContext.tsx index 52f80fc..9d093de 100644 --- a/frontend/contexts/AuthContext.tsx +++ b/frontend/contexts/AuthContext.tsx @@ -38,7 +38,7 @@ interface AuthContextType { user: User | null; login: (email: string, password?: string) => Promise; logout: () => void; - register: (data: { nome: string; email: string; senha: string; telefone: string }) => Promise; + register: (data: { nome: string; email: string; senha: string; telefone: string; role: string }) => Promise<{ success: boolean; userId?: string; token?: string }>; availableUsers: User[]; // Helper for the login screen demo } @@ -119,7 +119,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => setUser(null); }; - const register = async (data: { nome: string; email: string; senha: string; telefone: string }) => { + const register = async (data: { nome: string; email: string; senha: string; telefone: string; role: string }) => { try { const response = await fetch(`${import.meta.env.VITE_API_URL}/auth/register`, { method: 'POST', @@ -130,11 +130,29 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => if (!response.ok) { const errorData = await response.json().catch(() => ({})); const rawError = errorData.error || 'Falha no cadastro'; - // Allow passing raw error if it's not in map, or map it throw new Error(getErrorMessage(rawError)); } - return true; + const responseData = await response.json(); + + // If user is returned (auto-login), set state + if (responseData.user) { + const backendUser = responseData.user; + const mappedUser: User = { + id: backendUser.id, + email: backendUser.email, + name: backendUser.nome || backendUser.email.split('@')[0], + role: backendUser.role as UserRole, + ativo: backendUser.ativo, + }; + setUser(mappedUser); + } + + return { + success: true, + userId: responseData.user?.id || responseData.userId, + token: responseData.access_token + }; } catch (err) { console.error('Registration error:', err); throw err; diff --git a/frontend/pages/ProfessionalRegister.tsx b/frontend/pages/ProfessionalRegister.tsx index 4cf4beb..39ab5bd 100644 --- a/frontend/pages/ProfessionalRegister.tsx +++ b/frontend/pages/ProfessionalRegister.tsx @@ -17,13 +17,65 @@ export const ProfessionalRegister: React.FC = ({ const handleSubmit = async (professionalData: ProfessionalData) => { try { - // Aqui você pode fazer a chamada para o backend para cadastrar o profissional - // Por enquanto, vamos apenas mostrar mensagem de sucesso - console.log("Dados do profissional:", professionalData); + // 1. Cadastrar Usuário (Auth) e Logar Automaticamente + const authResult = await register({ + nome: professionalData.nome, + email: professionalData.email, + senha: professionalData.senha, + telefone: professionalData.whatsapp, + role: "PHOTOGRAPHER", // Role fixa para profissionais + }); + + if (!authResult.success) { + throw new Error("Falha no cadastro de usuário."); + } + + // 2. Criar Perfil Profissional (autenticado) + const { createProfessional } = await import("../services/apiService"); + + // Mapear dados do formulário para o payload esperado pelo backend + // O curl fornecido pelo usuário mostra campos underscore (snake_case) + const payload: any = { + nome: professionalData.nome, + agencia: professionalData.agencia, + banco: professionalData.banco, + carro_disponivel: professionalData.carroDisponivel === "sim", + cidade: professionalData.cidade, + conta_pix: professionalData.contaPix, + cpf_cnpj_titular: professionalData.cpfCnpj, + endereco: `${professionalData.rua}, ${professionalData.numero} - ${professionalData.bairro}`, + equipamentos: "", // Campo não está no form explícito, talvez observação ou outro? + extra_por_equipamento: false, // Default + funcao_profissional_id: professionalData.funcaoId, + observacao: professionalData.observacao, + qtd_estudio: parseInt(professionalData.qtdEstudios) || 0, + tem_estudio: professionalData.possuiEstudio === "sim", + tipo_cartao: professionalData.tipoCartao, + uf: professionalData.uf, + whatsapp: professionalData.whatsapp, + + // Campos numéricos default + desempenho_evento: 0, + disp_horario: 0, + educacao_simpatia: 0, + qual_tec: 0, + media: 0, + tabela_free: "", + }; + + const profResult = await createProfessional(payload, authResult.token); + + if (profResult.error) { + // Se falhar o perfil, o usuário foi criado :/ + // Idealmente limparíamos ou avisaríamos para completar perfil depois + throw new Error("Usuário criado, mas erro ao salvar dados profissionais: " + profResult.error); + } + + console.log("Profissional cadastrado com sucesso!"); setIsSuccess(true); - } catch (error) { + } catch (error: any) { console.error("Erro ao cadastrar profissional:", error); - alert("Erro ao cadastrar profissional. Tente novamente."); + alert(error.message || "Erro ao cadastrar profissional. Tente novamente."); } }; diff --git a/frontend/pages/Register.tsx b/frontend/pages/Register.tsx index 9d514ee..fe6762c 100644 --- a/frontend/pages/Register.tsx +++ b/frontend/pages/Register.tsx @@ -75,6 +75,7 @@ export const Register: React.FC = ({ onNavigate }) => { email: formData.email, senha: formData.password, telefone: formData.phone, + role: "EVENT_OWNER", // Client Role }); setIsLoading(false); setIsPending(true); @@ -252,7 +253,7 @@ export const Register: React.FC = ({ onNavigate }) => { } error={ error && - (error.includes("senha") || error.includes("coincidem")) + (error.includes("senha") || error.includes("coincidem")) ? error : undefined } diff --git a/frontend/services/apiService.ts b/frontend/services/apiService.ts index d57ea88..59c9f39 100644 --- a/frontend/services/apiService.ts +++ b/frontend/services/apiService.ts @@ -75,6 +75,46 @@ export async function getFunctions(): Promise< return fetchFromBackend("/api/funcoes"); } +/** + * Cria um novo perfil profissional + */ +export async function createProfessional(data: any, token?: string): Promise> { + try { + const headers: any = { + "Content-Type": "application/json", + }; + + if (token) { + headers["Authorization"] = `Bearer ${token}`; + } + + const response = await fetch(`${API_BASE_URL}/api/profissionais`, { + method: "POST", + headers: headers, + body: JSON.stringify(data), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.error || `HTTP error! status: ${response.status}`); + } + + const responseData = await response.json(); + return { + data: responseData, + error: null, + isBackendDown: false, + }; + } catch (error) { + console.error("Error creating professional:", error); + return { + data: null, + error: error instanceof Error ? error.message : "Erro desconhecido", + isBackendDown: true, + }; + } +} + export interface EventTypeResponse { id: string; nome: string;