// Auth layer — login, forgot, reset, invite panels + AuthGate wrapper
const { useState, useEffect, createContext, useContext } = React;
const _AuthCtx = createContext(null);
// ── Styles ────────────────────────────────────────────────────────────────────
const S = {
screen: {
position: 'fixed', inset: 0, background: 'var(--canvas)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
zIndex: 1000,
},
card: {
width: '100%', maxWidth: '400px', padding: '40px 36px',
background: '#1a1a1a', border: '1px solid #2a2a2a', borderRadius: '16px',
display: 'flex', flexDirection: 'column', gap: '8px',
},
wordmark: {
fontFamily: 'var(--f-mono)', fontSize: '22px', fontWeight: 700,
color: 'var(--mint)', letterSpacing: '-0.5px', marginBottom: '8px',
textAlign: 'center',
},
title: {
fontFamily: 'var(--f-sans)', fontSize: '14px', fontWeight: 500,
color: 'var(--text-meta)', textAlign: 'center', marginBottom: '16px',
},
input: {
width: '100%', padding: '10px 14px', background: '#111', border: '1px solid #333',
borderRadius: '8px', color: 'var(--text)', fontFamily: 'var(--f-sans)', fontSize: '14px',
outline: 'none', boxSizing: 'border-box', transition: 'border-color .15s',
},
btn: {
width: '100%', padding: '11px', background: 'var(--mint)', border: 'none',
borderRadius: '8px', color: '#131313', fontFamily: 'var(--f-sans)', fontSize: '14px',
fontWeight: 700, cursor: 'pointer', transition: 'opacity .15s',
},
ghost: {
width: '100%', padding: '10px', background: 'transparent',
border: '1px solid #333', borderRadius: '8px', color: 'var(--text-muted)',
fontFamily: 'var(--f-sans)', fontSize: '14px', cursor: 'pointer',
},
link: {
background: 'none', border: 'none', color: 'var(--text-meta)', fontSize: '13px',
cursor: 'pointer', textDecoration: 'underline', padding: '4px 0', fontFamily: 'var(--f-sans)',
},
msg: (ok) => ({
padding: '10px 14px', borderRadius: '8px', fontSize: '13px',
background: ok ? 'rgba(60,255,208,.12)' : 'rgba(255,76,76,.12)',
color: ok ? 'var(--mint)' : '#ff4c4c',
}),
privacyRow: {
display: 'flex', alignItems: 'flex-start', gap: '8px', fontSize: '13px',
color: 'var(--text-meta)',
},
gap16: { display: 'flex', flexDirection: 'column', gap: '12px' },
};
function Msg({ text, ok }) {
if (!text) return null;
return
{text}
;
}
// ── Login panel ───────────────────────────────────────────────────────────────
function LoginPanel({ onAuth, switchTo, appendMsg }) {
const [email, setEmail] = useState('');
const [pw, setPw] = useState('');
const [busy, setBusy] = useState(false);
const [msg, setMsg] = useState(null);
const [needsVerify, setNeedsVerify] = useState(false);
async function submit() {
if (!email || !pw) { setMsg({ text: 'Rellena email y contraseña', ok: false }); return; }
setBusy(true);
try {
const r = await fetch('/api/auth/login', {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email.trim().toLowerCase(), password: pw }),
});
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || 'Error al iniciar sesión'); }
const me = await fetch('/api/auth/me', { cache: 'no-store', credentials: 'include' });
const user = await me.json();
onAuth(user);
} catch (e) {
setMsg({ text: e.message, ok: false });
setNeedsVerify(/verific/i.test(e.message || ''));
} finally { setBusy(false); }
}
async function resendVerification() {
if (!email) { setMsg({ text: 'Introduce tu email primero', ok: false }); return; }
setBusy(true);
try {
await fetch('/api/auth/resend-verification', {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email.trim().toLowerCase() }),
});
setMsg({ text: 'Si tu cuenta existe y no está verificada, te hemos reenviado el email.', ok: true });
setNeedsVerify(false);
} catch { setMsg({ text: 'Error de red. Inténtalo de nuevo.', ok: false }); }
finally { setBusy(false); }
}
return (
{appendMsg &&
{appendMsg.text}
}
setEmail(e.target.value)}
onKeyDown={e => e.key === 'Enter' && submit()} />
setPw(e.target.value)}
onKeyDown={e => e.key === 'Enter' && submit()} />
{needsVerify && (
)}
);
}
// ── Forgot password panel ─────────────────────────────────────────────────────
function ForgotPanel({ switchTo }) {
const [email, setEmail] = useState('');
const [busy, setBusy] = useState(false);
const [msg, setMsg] = useState(null);
async function submit() {
if (!email) { setMsg({ text: 'Introduce tu email', ok: false }); return; }
setBusy(true);
try {
const r = await fetch('/api/auth/request-reset', {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email.trim().toLowerCase() }),
});
if (r.ok) setMsg({ text: 'Si el email existe, recibirás un enlace en unos minutos.', ok: true });
else { const e = await r.json().catch(() => ({})); setMsg({ text: e.detail || 'Error al enviar.', ok: false }); }
} catch { setMsg({ text: 'Error de red. Inténtalo de nuevo.', ok: false }); }
finally { setBusy(false); }
}
return (
setEmail(e.target.value)}
onKeyDown={e => e.key === 'Enter' && submit()} />
);
}
// ── Reset password panel ──────────────────────────────────────────────────────
function ResetPanel({ resetToken, switchTo }) {
const [pw, setPw] = useState('');
const [pw2, setPw2] = useState('');
const [busy, setBusy] = useState(false);
const [msg, setMsg] = useState(null);
async function submit() {
if (pw.length < 8) { setMsg({ text: 'La contraseña debe tener al menos 8 caracteres.', ok: false }); return; }
if (pw !== pw2) { setMsg({ text: 'Las contraseñas no coinciden.', ok: false }); return; }
setBusy(true);
try {
const r = await fetch('/api/auth/reset', {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: resetToken, password: pw }),
});
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || 'Token inválido o caducado'); }
setMsg({ text: 'Contraseña actualizada. Ya puedes iniciar sesión.', ok: true });
history.replaceState({}, '', '/');
setTimeout(() => switchTo('login'), 1800);
} catch (e) { setMsg({ text: e.message, ok: false }); }
finally { setBusy(false); }
}
return (
setPw(e.target.value)}
onKeyDown={e => e.key === 'Enter' && submit()} />
setPw2(e.target.value)}
onKeyDown={e => e.key === 'Enter' && submit()} />
);
}
// ── Invite redemption panel ───────────────────────────────────────────────────
function InvitePanel({ inviteCode, switchTo }) {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [pw, setPw] = useState('');
const [privacy, setPrivacy] = useState(false);
const [busy, setBusy] = useState(false);
const [msg, setMsg] = useState(null);
async function submit() {
if (!email || !pw) { setMsg({ text: 'Rellena email y contraseña', ok: false }); return; }
if (!privacy) { setMsg({ text: 'Debes aceptar la política de privacidad', ok: false }); return; }
setBusy(true);
try {
const r = await fetch('/api/auth/redeem-invite', {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: inviteCode, email: email.trim().toLowerCase(), password: pw, name, accepted_privacy: true }),
});
const data = await r.json().catch(() => ({}));
if (!r.ok) throw new Error(data.detail || 'Error al crear la cuenta');
setMsg({ text: 'Cuenta creada. Revisa tu email para verificarla antes de iniciar sesión.', ok: true });
history.replaceState({}, '', '/');
setTimeout(() => switchTo('login'), 2200);
} catch (e) { setMsg({ text: e.message, ok: false }); }
finally { setBusy(false); }
}
return (
);
}
// ── Auth screen ───────────────────────────────────────────────────────────────
const TITLES = {
login: 'Iniciar sesión',
forgot: 'Recuperar contraseña',
reset: 'Nueva contraseña',
invite: 'Crear cuenta',
};
function AuthScreen({ onAuth, initialPanel, resetToken, inviteCode }) {
const [panel, setPanel] = useState(initialPanel || 'login');
const [verifyMsg, setVerifyMsg] = useState(null);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const ver = params.get('verify_token');
if (ver) {
setVerifyMsg({ text: 'Verificando email…', ok: true });
fetch(`/api/auth/verify-email?token=${encodeURIComponent(ver)}`, { credentials: 'include' })
.then(r => r.json())
.then(d => {
setVerifyMsg({ text: d.message || 'Email verificado. Ya puedes iniciar sesión.', ok: true });
history.replaceState({}, '', '/');
setTimeout(() => setVerifyMsg(null), 3000);
})
.catch(() => setVerifyMsg({ text: 'Error al verificar. El enlace puede haber caducado.', ok: false }));
}
}, []);
return (
Ruticomidas
{TITLES[panel] || ''}
{panel === 'login' &&
}
{panel === 'forgot' &&
}
{panel === 'reset' &&
}
{panel === 'invite' &&
}
);
}
// ── Loading screen ─────────────────────────────────────────────────────────────
function LoadingScreen() {
return (
);
}
// ── AuthGate ──────────────────────────────────────────────────────────────────
function AuthGate({ children }) {
const [user, setUser] = useState(undefined); // undefined=loading, null=unauthed, obj=authed
const [initialPanel, setInitialPanel] = useState('login');
const [resetToken, setResetToken] = useState(null);
const [inviteCode, setInviteCode] = useState(null);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const inv = params.get('invite');
const rst = params.get('reset_token');
const ver = params.get('verify_token');
if (inv) { setInviteCode(inv); setInitialPanel('invite'); setUser(null); return; }
if (rst) { setResetToken(rst); setInitialPanel('reset'); setUser(null); return; }
if (ver) { setInitialPanel('login'); }
fetch('/api/auth/me', { cache: 'no-store', credentials: 'include' })
.then(r => r.ok ? r.json() : null)
.then(u => { if (u) { window.RC_Auth.currentUser = u; setUser(u); } else setUser(null); })
.catch(() => setUser(null));
}, []);
function handleAuth(u) {
window.RC_Auth.currentUser = u;
setUser(u);
}
function logout() {
fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }).catch(() => {});
window.RC_Auth.currentUser = null;
setUser(null);
}
if (user === undefined) return ;
if (user === null) return (
);
return (
<_AuthCtx.Provider value={{ user, logout }}>
{children}
);
}
function useAuth() { return useContext(_AuthCtx); }
window.RC_Auth = { currentUser: null, AuthGate, useAuth };
// Reload on 401 from api.jsx wrapper
window.RC_API.setOn401(() => {
window.RC_Auth.currentUser = null;
window.location.reload();
});