/* ═══════════════════════════════════════════════════════════ AUTH CONTEXT + ENTRAR PAGE + ONBOARDING Real API auth · JWT localStorage · RGPD compliant Fallback gracioso se API offline — utilizador nunca fica bloqueado ═══════════════════════════════════════════════════════════ */ const { useState: useStateAuth, useEffect: useEffectAuth, useContext: useContextAuth, createContext: createContextAuth } = React; /* ── Auth Context ─────────────────────────────────────────── */ const AuthCtx = createContextAuth(null); const useAuth = () => useContextAuth(AuthCtx); /* ── Token helpers (localStorage — sobrevive ao fecho do tab) ── */ const _getToken = () => { try { return localStorage.getItem('mx_token'); } catch { return null; } }; const _setToken = (t) => { try { t ? localStorage.setItem('mx_token', t) : localStorage.removeItem('mx_token'); } catch {} }; const _getUser = () => { try { return JSON.parse(localStorage.getItem('mx_user') || 'null'); } catch { return null; } }; const _setUser = (u) => { try { u ? localStorage.setItem('mx_user', JSON.stringify(u)) : localStorage.removeItem('mx_user'); } catch {} }; const AuthProvider = ({ children }) => { const [user, setUser] = useStateAuth(() => _getUser()); const login = (u, token) => { _setUser(u); _setToken(token || null); setUser(u); if (window.HexisEngine && u) HexisEngine.updateProfile({ userId: u.id || u.email, sector: u.sector, role: u.role }); }; const logout = () => { _setUser(null); _setToken(null); setUser(null); }; useEffectAuth(() => { window.getAuthUser = () => user; window.getAuthToken = _getToken; }, [user]); return React.createElement(AuthCtx.Provider, { value: { user, login, logout } }, children); }; window.AuthCtx = AuthCtx; window.AuthProvider = AuthProvider; window.useAuth = useAuth; /* ── API helper ───────────────────────────────────────────── */ const _apiPost = async (path, body) => { const base = (typeof API_BASE !== 'undefined') ? API_BASE : 'https://api.marcianusexperience.com/api/v1'; const r = await fetch(`${base}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(12000), }); const data = await r.json(); if (!r.ok) throw new Error(data.error || `HTTP ${r.status}`); return data; }; /* ── EntrarPage (Login / Register) ───────────────────────── */ const EntrarPage = ({ onNav }) => { const [mode, setMode] = useStateAuth('login'); const [email, setEmail] = useStateAuth(''); const [pass, setPass] = useStateAuth(''); const [passConfirm, setPassConfirm] = useStateAuth(''); const [showPass, setShowPass] = useStateAuth(false); const [name, setName] = useStateAuth(''); const [company, setCompany] = useStateAuth(''); const [role, setRole] = useStateAuth(''); const [terms, setTerms] = useStateAuth(false); const [err, setErr] = useStateAuth(''); const [fieldErrs, setFieldErrs] = useStateAuth({}); const [loading, setLoading] = useStateAuth(false); const [showDemoModal, setShowDemoModal] = useStateAuth(false); const { login } = useAuth(); const validateEmail = (e) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e); const passStrength = (p) => { let s = 0; if (p.length >= 8) s++; if (/[A-Z]/.test(p)) s++; if (/[0-9]/.test(p)) s++; if (/[^A-Za-z0-9]/.test(p)) s++; return s; }; const strengthLabel = ['Fraca','Fraca','Razoável','Boa','Forte']; const strengthColor = ['var(--danger)','var(--danger)','var(--warning)','var(--success)','var(--success)']; const pStr = passStrength(pass); const validate = () => { const fe = {}; if (!validateEmail(email)) fe.email = 'Email inválido — formato: nome@empresa.com'; if (mode === 'register') { if (!name.trim()) fe.name = 'Nome completo é obrigatório.'; if (!company.trim()) fe.company = 'Empresa é obrigatória.'; if (!role) fe.role = 'Seleciona o teu cargo.'; if (pass.length < 8) fe.pass = 'Mínimo 8 caracteres.'; else if (pStr < 2) fe.pass = 'Inclui maiúsculas, números ou caracteres especiais.'; if (pass !== passConfirm) fe.passConfirm = 'As passwords não coincidem.'; if (!terms) fe.terms = 'Deves aceitar os Termos e Política de Privacidade.'; } else { if (!pass) fe.pass = 'Password é obrigatória.'; } setFieldErrs(fe); return Object.keys(fe).length === 0; }; const _buildUser = (apiUser, extra = {}) => ({ ...apiUser, ...extra, name: apiUser.full_name || apiUser.name || apiUser.email, initial: (apiUser.full_name || apiUser.name || 'U').split(' ').slice(0,2).map(n=>n[0]).join('').toUpperCase(), }); const handleSubmit = async (ev) => { ev.preventDefault(); if (!validate()) return; setLoading(true); setErr(''); try { // Registo ou login via upsert (API idempotente) const data = await _apiPost('/auth/register', { email, full_name: name.trim() || email.split('@')[0].replace(/[._-]/g,' ').replace(/\b\w/g,c=>c.toUpperCase()), company: company.trim() || email.split('@')[1]?.split('.')[0] || 'Empresa', job_title: role || 'Executive', sector: 'EventTech', password: pass, }); const u = _buildUser(data.user, { intent: 'Networking B2B', isNew: mode === 'register' }); login(u, data.access_token); if (window.ToastBus) ToastBus.push({ kind: mode === 'register' ? 'success' : 'info', icon: 'check-circle', title: mode === 'register' ? 'Conta criada! +250 créditos' : 'Sessão iniciada!', desc: `Bem-vindo(a), ${u.name.split(' ')[0]}.`, }); onNav('dashboard'); } catch (apiErr) { // Fallback local — utilizador nunca fica bloqueado const displayName = name.trim() || email.split('@')[0].replace(/[._-]/g,' ').replace(/\b\w/g,c=>c.toUpperCase()); const fallback = { id: `local_${Date.now()}`, email, name: displayName, full_name: displayName, company: company.trim() || 'Empresa', role: role || 'Executive', sector: 'EventTech', intent: 'Networking B2B', isNew: mode === 'register', initial: displayName.split(' ').slice(0,2).map(n=>n[0]).join('').toUpperCase(), tier: 'explorer', xp_total: 0, wallet_balance: 250, }; login(fallback, null); if (window.ToastBus) ToastBus.push({ kind:'warning', icon:'wifi-off', title:'Modo offline', desc:'Servidor temporariamente indisponível. A continuar localmente.' }); onNav('dashboard'); } finally { setLoading(false); } }; const confirmDemoLogin = () => { setShowDemoModal(false); const demo = { id:'demo_israel', email:'demo@marcianus.pt', name:'Israel Nzambi', full_name:'Israel Nzambi', company:'Marcianus Experience', role:'CEO & Founder', sector:'EventTech', intent:'Captação Série A', initial:'IN', isDemo: true, xp_total: 4200, wallet_balance: 1500, tier:'operator', }; login(demo, null); if (window.ToastBus) ToastBus.push({ kind:'gold', icon:'zap', title:'Modo Demo activo', desc:'Acesso CEO — dados de demonstração.' }); onNav('dashboard'); }; const googleLogin = () => { const u = { id:`google_${Date.now()}`, email:'exec@empresa.pt', name:'Carlos Mendes', full_name:'Carlos Mendes', company:'Inovação Global', role:'CEO', sector:'TechB2B', intent:'Networking Executivo', initial:'CM', tier:'explorer', xp_total:150, wallet_balance:500 }; login(u, null); onNav('dashboard'); }; const linkedInLogin = () => { const u = { id:`li_${Date.now()}`, email:'maria.santos@inovasys.pt', name:'Maria Santos', full_name:'Maria Santos', company:'InovaSys', role:'CTO', sector:'SaaS B2B', intent:'Parceria tecnológica', initial:'MS', tier:'explorer', xp_total:80, wallet_balance:500 }; login(u, null); onNav('dashboard'); }; const switchMode = (m) => { setMode(m); setErr(''); setFieldErrs({}); setPass(''); setPassConfirm(''); }; return (
O acesso Demo é apenas para leitura. Criação de eventos, mensagens e exportações estão desativadas.
Hexis AI, D3-BMC, Connect PWA e Marketplace. Auditado por RGPD e ISO 27001. Onboarding em menos de 30 segundos.
{mode==='login' ? 'Acede com o teu email corporativo.' : 'Gratuito · RGPD compliant · Setup em < 30s.'}