/* 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 (
Schedules, shoot and posting times, content and a direct line to your team — all in one place.