build-container / build (push) Canceled after 4m16s
- Mobile-first PWA (phone tab bar with More menu, iPad/desktop rail) - Passkey (WebAuthn) sign-in gates the whole app; first passkey via setup code - Files: Seafile browse/upload/new doc/rename/move/share link/preview/delete - In-app OnlyOffice editing with JWT-signed configs and save-back to Seafile - Plex music player via plex.tv sign-in and a server-side proxy - Calendar agenda from private ICS links (recurrence + time zones) - Search, Home dashboard, webmail deep links, audiobooks frame - Dockerfile, compose, and a Gitea Actions workflow that builds the image Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
37 lines
2.2 KiB
JavaScript
37 lines
2.2 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) {
|
|
const { cid, options } = await authPost('/api/auth/register/options', { setupCode });
|
|
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() {
|
|
const { cid, options } = await authPost('/api/auth/login/options');
|
|
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(),
|
|
} });
|
|
}
|