Files
hub/public/webauthn.js
T
nmemmertandClaude Sonnet 5.5 301e8017d9
build-container / build (push) Successful in 3m52s
Parent controls for children, name-tile sign-in, restricted Learn search, simple mode
- Sign-in page shows name tiles on remembered devices; tap a name, then PIN or passkey
- Music and Movies are separate on the server; Plex can be locked to a child's managed profile
- Activity log, weekly summary and alerts to parents; per-area and weekend limits
- Requests from children (Ask button) answered in Settings; bonus screen time from rewards
- Restricted Learn search (Simple English Wikipedia, blocked words) replaces web search for children
- Simple mode (big-button home) for young children or grandparents
- Child usage kept in memory instead of read from disk on every request
- Tests and README updated

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 16:49:48 -04:00

37 lines
2.3 KiB
JavaScript

// Shared passkey helpers (sign-in page + Settings).
const b64u = {
enc: buf => btoa(String.fromCharCode(...new Uint8Array(buf))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
dec: s => Uint8Array.from(atob(s.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - s.length % 4) % 4)), c => c.charCodeAt(0)),
};
async function authPost(path, body, method = 'POST') {
const r = await fetch(path, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) });
const j = await r.json().catch(() => ({}));
if (!r.ok) throw new Error(j.error || 'Request failed (' + r.status + ')');
return j;
}
async function passkeyRegister(setupCode, name, inviteCode) {
const { cid, options } = await authPost('/api/auth/register/options', { setupCode, inviteCode });
options.challenge = b64u.dec(options.challenge);
options.user.id = b64u.dec(options.user.id);
(options.excludeCredentials || []).forEach(c => { c.id = b64u.dec(c.id); });
const cred = await navigator.credentials.create({ publicKey: options });
await authPost('/api/auth/register/verify', { cid, name, response: {
id: cred.id, rawId: b64u.enc(cred.rawId), type: cred.type,
response: { clientDataJSON: b64u.enc(cred.response.clientDataJSON), attestationObject: b64u.enc(cred.response.attestationObject),
transports: cred.response.getTransports ? cred.response.getTransports() : [] },
clientExtensionResults: cred.getClientExtensionResults(), authenticatorAttachment: cred.authenticatorAttachment,
} });
}
async function passkeyLogin(uid) {
const { cid, options } = await authPost('/api/auth/login/options', uid ? { uid } : undefined);
options.challenge = b64u.dec(options.challenge);
(options.allowCredentials || []).forEach(c => { c.id = b64u.dec(c.id); });
const a = await navigator.credentials.get({ publicKey: options });
await authPost('/api/auth/login/verify', { cid, response: {
id: a.id, rawId: b64u.enc(a.rawId), type: a.type,
response: { clientDataJSON: b64u.enc(a.response.clientDataJSON), authenticatorData: b64u.enc(a.response.authenticatorData),
signature: b64u.enc(a.response.signature), userHandle: a.response.userHandle ? b64u.enc(a.response.userHandle) : undefined },
clientExtensionResults: a.getClientExtensionResults(),
} });
}