photum/frontend/contexts/AuthContext.tsx
2026-01-21 20:44:27 -03:00

276 lines
9.6 KiB
TypeScript

import React, { createContext, useContext, useState, ReactNode, useEffect } from 'react';
import { User, UserRole } from '../types';
// Mock Users Database
const MOCK_USERS: User[] = [
{
id: 'superadmin-1',
name: 'Dev Admin',
email: 'admin@photum.com',
role: UserRole.SUPERADMIN,
avatar: 'https://i.pravatar.cc/150?u=admin'
},
{
id: 'owner-1',
name: 'PHOTUM CEO',
email: 'empresa@photum.com',
role: UserRole.BUSINESS_OWNER,
avatar: 'https://i.pravatar.cc/150?u=ceo'
},
{
id: 'photographer-1',
name: 'COLABORADOR PHOTUM',
email: 'foto@photum.com',
role: UserRole.PHOTOGRAPHER,
avatar: 'https://i.pravatar.cc/150?u=photo'
},
{
id: 'client-1',
name: 'PARCEIRO PHOTUM',
email: 'cliente@photum.com',
role: UserRole.EVENT_OWNER,
avatar: 'https://i.pravatar.cc/150?u=client'
},
{
id: 'viewer-1',
name: 'VISUALIZADOR PHOTUM',
email: 'viewer@photum.com',
role: UserRole.AGENDA_VIEWER,
avatar: 'https://i.pravatar.cc/150?u=viewer'
}
];
interface AuthContextType {
user: User | null;
login: (email: string, password?: string) => Promise<boolean>;
logout: () => void;
register: (data: { nome: string; email: string; senha: string; telefone: string; role: string; empresaId?: string; tipo_profissional?: string }) => Promise<{ success: boolean; userId?: string; token?: string }>;
availableUsers: User[]; // Helper for the login screen demo
token: string | null;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(localStorage.getItem("token"));
useEffect(() => {
const restoreSession = async () => {
if (!token) return;
try {
const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8080";
const response = await fetch(`${API_URL}/api/me`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data = await response.json();
const backendUser = data.user;
const mappedUser: User = {
id: backendUser.id,
email: backendUser.email,
name: data.profissional?.nome || data.empresa?.nome || backendUser.name || backendUser.nome || backendUser.email.split('@')[0],
role: backendUser.role as UserRole,
ativo: backendUser.ativo,
empresaId: backendUser.company_id || backendUser.empresa_id || backendUser.companyId,
companyName: backendUser.company_name || backendUser.empresa_nome || backendUser.companyName,
avatar: data.profissional?.avatar_url || data.empresa?.avatar_url || backendUser.avatar,
};
if (!backendUser.ativo) {
console.warn("User is not active, logging out.");
logout();
return;
}
setUser(mappedUser);
} else {
// Token invalid or expired
logout();
}
} catch (err) {
console.error("Session restore error:", err);
logout();
}
};
if (!user && token) {
restoreSession();
}
}, [token]); // removed 'user' from dependency to avoid loop if user is set, though !user check handles it. safer to just depend on token mount.
const getErrorMessage = (errorKey: string): string => {
// Map backend error messages to Portuguese
const errorMap: { [key: string]: string } = {
"email already registered": "Este e-mail já está cadastrado.",
"invalid credentials": "E-mail ou senha incorretos.",
"user not found": "Usuário não encontrado.",
"crypto/bcrypt: hashedPassword is not the hash of the given password": "Senha incorreta.",
"password is too short": "A senha é muito curta.",
"cpf already registered": "CPF já cadastrado.",
};
// Check for partial matches or exact matches
if (errorMap[errorKey]) return errorMap[errorKey];
// Fallbacks
if (errorKey.includes('hashedPassword')) return "Senha incorreta.";
if (errorKey.includes('email')) return "Erro relacionado ao e-mail.";
if (errorKey.includes('password')) return "Erro relacionado à senha.";
return "Ocorreu um erro inesperado. Tente novamente.";
};
const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8080";
const login = async (email: string, password?: string) => {
// 1. Try Real API first
try {
const response = await fetch(`${API_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, senha: password })
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(getErrorMessage(errorData.error || 'Falha no login'));
}
const data = await response.json();
const backendUser = data.user;
// Enforce active check
if (!backendUser.ativo) {
throw new Error("Cadastro pendente de aprovação.");
}
// Store token (optional, if you need it for other requests outside cookies)
localStorage.setItem('token', data.access_token);
setToken(data.access_token);
// Map backend user to frontend User type
const mappedUser: User = {
id: backendUser.id,
email: backendUser.email,
name: data.profissional?.nome || data.empresa?.nome || backendUser.name || backendUser.nome || backendUser.email.split('@')[0],
role: backendUser.role as UserRole,
ativo: backendUser.ativo,
empresaId: backendUser.company_id || backendUser.empresa_id || backendUser.companyId,
companyName: backendUser.company_name || backendUser.empresa_nome || backendUser.companyName,
avatar: data.profissional?.avatar_url || data.empresa?.avatar_url || backendUser.avatar,
};
setUser(mappedUser);
return true;
} catch (err) {
console.error('Login error:', err);
// 2. Fallback to Demo/Mock users if API fails
const mockUser = MOCK_USERS.find(u => u.email === email);
if (mockUser) {
console.log('Using demo user:', mockUser.email);
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate delay
setUser({ ...mockUser, ativo: true });
return true;
}
throw err;
}
};
const logout = async () => {
try {
if (token) {
await fetch(`${import.meta.env.VITE_API_URL}/auth/logout`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}` // Though refresh token is in cookie
},
// body: JSON.stringify({ refresh_token: ... }) // If we had it in client state, but it is in cookie.
});
}
} catch (error) {
console.error("Logout failed", error);
} finally {
localStorage.removeItem('token');
setToken(null);
setUser(null);
// Force redirect or let app router handle it safely
window.location.href = '/';
}
};
const register = async (data: { nome: string; email: string; senha: string; telefone: string; role: string; empresaId?: string; tipo_profissional?: string }) => {
try {
// Destructure to separate empresaId from the rest
const { empresaId, ...rest } = data;
const payload = {
...rest,
empresa_id: empresaId
};
const response = await fetch(`${import.meta.env.VITE_API_URL}/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const rawError = errorData.error || 'Falha no cadastro';
throw new Error(getErrorMessage(rawError));
}
const responseData = await response.json();
// IF user is returned (auto-login), logic:
// Only set user/token if they are ACTIVE (which they won't be for standard clients/professionals)
// This allows the "Pending Approval" modal to show instead of auto-redirecting.
if (responseData.user && responseData.user.ativo) {
if (responseData.access_token) {
localStorage.setItem('token', responseData.access_token);
setToken(responseData.access_token);
}
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,
empresaId: backendUser.company_id || backendUser.empresa_id || backendUser.companyId,
companyName: backendUser.company_name || backendUser.empresa_nome || backendUser.companyName,
};
setUser(mappedUser);
}
// If user is NOT active, we do NOT set the token/user state, preventing auto-login.
return {
success: true,
userId: responseData.user?.id || responseData.userId,
token: responseData.access_token
};
} catch (err) {
console.error('Registration error:', err);
throw err;
}
};
return (
<AuthContext.Provider value={{ user, token, login, logout, register, availableUsers: MOCK_USERS }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth must be used within an AuthProvider');
return context;
};