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
|
// Create professional data only if role is appropriate
|
||||||
var profData *profissionais.CreateProfissionalInput
|
var profData *profissionais.CreateProfissionalInput
|
||||||
if req.Role == "profissional" || req.Role == "empresa" {
|
// COMMENTED OUT to enable 2-step registration (User -> Full Profile)
|
||||||
profData = &profissionais.CreateProfissionalInput{
|
// if req.Role == "profissional" || req.Role == "empresa" {
|
||||||
Nome: req.Nome,
|
// profData = &profissionais.CreateProfissionalInput{
|
||||||
Whatsapp: &req.Telefone,
|
// 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 err != nil {
|
||||||
if strings.Contains(err.Error(), "duplicate key") {
|
if strings.Contains(err.Error(), "duplicate key") {
|
||||||
c.JSON(http.StatusConflict, gin.H{"error": "email already registered"})
|
c.JSON(http.StatusConflict, gin.H{"error": "email already registered"})
|
||||||
|
|
@ -63,7 +64,44 @@ func (h *Handler) Register(c *gin.Context) {
|
||||||
return
|
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 {
|
type loginRequest struct {
|
||||||
|
|
@ -119,6 +157,17 @@ func (h *Handler) Login(c *gin.Context) {
|
||||||
MaxAge: 30 * 24 * 60 * 60,
|
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{
|
resp := loginResponse{
|
||||||
AccessToken: tokenPair.AccessToken,
|
AccessToken: tokenPair.AccessToken,
|
||||||
ExpiresAt: "2025-...", // logic to calculate if needed, or remove field
|
ExpiresAt: "2025-...", // logic to calculate if needed, or remove field
|
||||||
|
|
|
||||||
|
|
@ -12,20 +12,27 @@ import (
|
||||||
func AuthMiddleware(cfg *config.Config) gin.HandlerFunc {
|
func AuthMiddleware(cfg *config.Config) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
authHeader := c.GetHeader("Authorization")
|
authHeader := c.GetHeader("Authorization")
|
||||||
if authHeader == "" {
|
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "authorization header required"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.Split(authHeader, " ")
|
|
||||||
var tokenString string
|
var tokenString string
|
||||||
if len(parts) == 2 && parts[0] == "Bearer" {
|
|
||||||
tokenString = parts[1]
|
if authHeader != "" {
|
||||||
} else if len(parts) == 1 && parts[0] != "" {
|
parts := strings.Split(authHeader, " ")
|
||||||
tokenString = parts[0]
|
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 {
|
} else {
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid authorization header format"})
|
// Try to get from cookie
|
||||||
return
|
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)
|
claims, err := ValidateToken(tokenString, cfg.JwtAccessSecret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ interface AuthContextType {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
login: (email: string, password?: string) => Promise<boolean>;
|
login: (email: string, password?: string) => Promise<boolean>;
|
||||||
logout: () => void;
|
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
|
availableUsers: User[]; // Helper for the login screen demo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,7 +119,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||||
setUser(null);
|
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 {
|
try {
|
||||||
const response = await fetch(`${import.meta.env.VITE_API_URL}/auth/register`, {
|
const response = await fetch(`${import.meta.env.VITE_API_URL}/auth/register`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -130,11 +130,29 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
const rawError = errorData.error || 'Falha no cadastro';
|
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));
|
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) {
|
} catch (err) {
|
||||||
console.error('Registration error:', err);
|
console.error('Registration error:', err);
|
||||||
throw err;
|
throw err;
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,65 @@ export const ProfessionalRegister: React.FC<ProfessionalRegisterProps> = ({
|
||||||
|
|
||||||
const handleSubmit = async (professionalData: ProfessionalData) => {
|
const handleSubmit = async (professionalData: ProfessionalData) => {
|
||||||
try {
|
try {
|
||||||
// Aqui você pode fazer a chamada para o backend para cadastrar o profissional
|
// 1. Cadastrar Usuário (Auth) e Logar Automaticamente
|
||||||
// Por enquanto, vamos apenas mostrar mensagem de sucesso
|
const authResult = await register({
|
||||||
console.log("Dados do profissional:", professionalData);
|
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);
|
setIsSuccess(true);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error("Erro ao cadastrar profissional:", error);
|
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,
|
email: formData.email,
|
||||||
senha: formData.password,
|
senha: formData.password,
|
||||||
telefone: formData.phone,
|
telefone: formData.phone,
|
||||||
|
role: "EVENT_OWNER", // Client Role
|
||||||
});
|
});
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setIsPending(true);
|
setIsPending(true);
|
||||||
|
|
@ -252,7 +253,7 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
||||||
}
|
}
|
||||||
error={
|
error={
|
||||||
error &&
|
error &&
|
||||||
(error.includes("senha") || error.includes("coincidem"))
|
(error.includes("senha") || error.includes("coincidem"))
|
||||||
? error
|
? error
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,46 @@ export async function getFunctions(): Promise<
|
||||||
return fetchFromBackend("/api/funcoes");
|
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 {
|
export interface EventTypeResponse {
|
||||||
id: string;
|
id: string;
|
||||||
nome: string;
|
nome: string;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue