Merge pull request #19 from rede5/Front-back-integracao-task3

feat(auth): implementação do fluxo de cadastro duplo (clientes e profissionais)

- 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
This commit is contained in:
Andre F. Rodrigues 2025-12-12 13:02:38 -03:00 committed by GitHub
commit b6deacc291
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 197 additions and 30 deletions

View file

@ -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

View file

@ -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 {

View file

@ -38,7 +38,7 @@ interface AuthContextType {
user: User | null;
login: (email: string, password?: string) => Promise<boolean>;
logout: () => void;
register: (data: { nome: string; email: string; senha: string; telefone: string }) => Promise<boolean>;
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;

View file

@ -17,13 +17,65 @@ export const ProfessionalRegister: React.FC<ProfessionalRegisterProps> = ({
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.");
}
};

View file

@ -75,6 +75,7 @@ export const Register: React.FC<RegisterProps> = ({ 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<RegisterProps> = ({ onNavigate }) => {
}
error={
error &&
(error.includes("senha") || error.includes("coincidem"))
(error.includes("senha") || error.includes("coincidem"))
? error
: undefined
}

View file

@ -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<ApiResponse<any>> {
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;