/* Mandera Portal — Auth (sessions + login) over MData. Roles: master (owner/admin), team (sub-master), client. */ const MA_SESSION = 'mandera_portal_session_v2'; const MAuth = { accounts() { return window.MData.get('accounts'); }, byEmail(email) { return MAuth.accounts().find((a) => a.email === (email || '').trim().toLowerCase()); }, login(email, pw) { const em = (email || '').trim().toLowerCase(); const matches = MAuth.accounts().filter((a) => a.email === em); if (!matches.length) return { error: 'Those details don’t match an account.' }; // If several accounts share an email, prefer the one whose password matches // (so a duplicate can never shadow the real account, e.g. a master). const a = matches.find((x) => x.password === pw) || matches[0]; if (a.password !== pw) return { error: 'Those details don’t match an account.' }; if (a.status === 'suspended') return { error: 'This account is suspended. Contact your Mandera lead.' }; localStorage.setItem(MA_SESSION, a.id); return { account: a }; }, current() { const id = localStorage.getItem(MA_SESSION); return MAuth.accounts().find((a) => a.id === id) || null; }, signOut() { localStorage.removeItem(MA_SESSION); }, markOnboarded() { const a = MAuth.current(); if (!a) return; window.MData.update('accounts', a.id, { onboarded: true }); }, /* passwords */ changePassword(id, current, next) { const a = MAuth.accounts().find((x) => x.id === id); if (!a) return { error: 'Account not found.' }; if (a.password !== current) return { error: 'Your current password is incorrect.' }; if (!next || next.length < 6) return { error: 'New password must be at least 6 characters.' }; window.MData.update('accounts', id, { password: next, mustSetPassword: false }); return { ok: true }; }, setOwnPassword(id, next) { if (!next || next.length < 6) return { error: 'Password must be at least 6 characters.' }; window.MData.update('accounts', id, { password: next, mustSetPassword: false }); return { ok: true }; }, resetByEmail(email, next) { const a = MAuth.byEmail(email); if (!a) return { error: 'No account uses that email.' }; if (!next || next.length < 6) return { error: 'New password must be at least 6 characters.' }; window.MData.update('accounts', a.id, { password: next, mustSetPassword: false }); return { ok: true, account: a }; }, /* account admin (used by master/team) */ create({ role, name, email, password, brand, clientId }) { const db = window.MData.load(); let cid = clientId || null; if (role === 'client' && !cid) { const existing = db.clients.find((c) => c.brand.toLowerCase() === (brand || '').trim().toLowerCase()); if (existing) cid = existing.id; else { const c = window.MData.add('clients', { brand: (brand || name).trim(), industry: '', contact: name, email: (email || '').trim().toLowerCase(), manager: 'Mandera', status: 'active' }); cid = c.id; } } // Clients must choose their own password on first sign-in. return window.MData.add('accounts', { role, name: name.trim(), email: (email || '').trim().toLowerCase(), password, status: 'active', onboarded: role !== 'client', clientId: cid, mustSetPassword: role === 'client' }); }, remove(id) { const a = window.MData.get('accounts').find((x) => x.id === id); window.MData.del('accounts', id); // If this was a client's sign-in and no other account is tied to that // client, remove the whole client workspace + data too — so deleting // the account genuinely deletes the client. if (a && a.role === 'client' && a.clientId) { const stillLinked = window.MData.get('accounts').some((x) => x.clientId === a.clientId); if (!stillLinked) window.MData.deleteClient(a.clientId); } }, suspend(id, on) { window.MData.update('accounts', id, { status: on ? 'suspended' : 'active' }); }, resetPassword(id, pw) { window.MData.update('accounts', id, { password: pw, mustSetPassword: true }); }, }; /* shared password input */ function PwInput({ value, onChange, placeholder, err }) { const [show, setShow] = React.useState(false); return (
); } /* ==================================================================== */ function Login({ go }) { const { Button, Input, Checkbox } = window.ManderaDesignSystem_8327ce; const [view, setView] = React.useState('signin'); // signin | forgot | setpw const [email, setEmail] = React.useState(''); const [pw, setPw] = React.useState(''); const [err, setErr] = React.useState(''); const [pending, setPending] = React.useState(null); // account that must set password const [np1, setNp1] = React.useState(''); const [np2, setNp2] = React.useState(''); const [notice, setNotice] = React.useState(''); const [checking, setChecking] = React.useState(false); const [sending, setSending] = React.useState(false); const proceed = (a) => { if (a.role === 'master' || a.role === 'team') { go('admin'); return; } go(a.onboarded ? 'app' : 'welcome'); }; const submitSignin = async (e) => { e.preventDefault(); let res = MAuth.login(email, pw); // The account may have been created on another device and not yet synced // to this browser — pull the latest from the server and try once more // before telling the user it doesn't exist. if (res.error && res.error.indexOf('suspended') === -1) { setChecking(true); try { await window.MData.sync(); } catch (_) {} setChecking(false); res = MAuth.login(email, pw); } if (res.error) { setErr(res.error); return; } setErr(''); if (res.account.mustSetPassword) { setPending(res.account); setNp1(''); setNp2(''); setView('setpw'); return; } proceed(res.account); }; const submitSetpw = (e) => { e.preventDefault(); if (np1 !== np2) { setErr('The two passwords don’t match.'); return; } const res = MAuth.setOwnPassword(pending.id, np1); if (res.error) { setErr(res.error); return; } setErr(''); proceed(MAuth.current() || pending); }; const submitForgot = async (e) => { e.preventDefault(); if (!email.trim()) { setErr('Enter your account email.'); return; } setErr(''); setSending(true); // Server generates a one-time token and emails a reset link. It always // responds the same way so the form can't reveal which emails exist. try { await fetch('../api/portal-mail.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'request_reset', email: email.trim() }) }); } catch (_) {} setSending(false); setNotice('If an account uses that email, we’ve sent a password reset link. Check your inbox (and spam folder).'); setPw(''); setView('signin'); }; const Panel = (
e.currentTarget.style.color = 'var(--text-strong)'} onMouseLeave={(e) => e.currentTarget.style.color = 'var(--text-muted)'}> Main website
Client Portal

Your work,
always in view.

Schedules, shoot and posting times, content and a direct line to your team — all in one place.

© 2026 Mandera Marketing Agency
); let formBody; if (view === 'signin') { formBody = (

Sign in

Use the details your Mandera contact sent you.

{notice &&
{notice}
} { setEmail(e.target.value); setErr(''); setNotice(''); }} iconLeft={} />
{ setPw(e.target.value); setErr(''); }} err={err} /> {err && {err}}
); } else if (view === 'setpw') { formBody = (

Choose your password

Welcome, {pending && pending.name.split(' ')[0]}. Set a password you’ll remember — you’ll use it every time you sign in.

{ setNp1(e.target.value); setErr(''); }} placeholder="At least 6 characters" err={err} />
{ setNp2(e.target.value); setErr(''); }} err={err} /> {err && {err}}
); } else { // forgot formBody = (

Reset password

Enter your account email and we’ll send you a secure link to set a new password. The link expires in 1 hour.

{ setEmail(e.target.value); setErr(''); }} iconLeft={} /> {err && {err}}
); } return ( ); } Object.assign(window, { MAuth, Login, PwInput });