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:
parent
1b65d26bcd
commit
a337e27f2a
6 changed files with 197 additions and 30 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue