Family devices: shared-screen family board with a who-is-this profile picker
build-container / build (push) Successful in 4m30s
build-container / build (push) Successful in 4m30s
- Admin can make a screen a family device; it opens on a family board - Name tiles with KID labels; kids tap, others enter a PIN (lockout after 5 wrong tries) - Name label with Switch user, auto-return to the board after 5 idle minutes - Short 30-minute sessions on family devices - Settings section for PIN and turning a device on or off - Test, README and roadmap updated Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
058f410a58
commit
eef1adaa73
21 files changed
+300
-14
No files matched your search
@@ -30,6 +30,7 @@ const cal = require('./cal');
|
||||
const tasks = require('./tasks');
|
||||
const settings = require('./settings');
|
||||
const inbox = require('./inbox');
|
||||
const board = require('./board');
|
||||
const search = require('./search');
|
||||
const users = require('./users');
|
||||
const admin = require('./admin');
|
||||
@@ -127,13 +128,14 @@ function serveStatic(req, res) {
|
||||
let p = decodeURIComponent(new URL(req.url, 'http://x').pathname);
|
||||
if (p === '/') p = '/index.html';
|
||||
if (p === '/login') p = '/login.html';
|
||||
if (p === '/board') p = '/board.html';
|
||||
const file = path.join(PUBLIC, path.normalize(p));
|
||||
if (!file.startsWith(PUBLIC)) { res.writeHead(403); return res.end(); }
|
||||
fs.readFile(file, (err, data) => {
|
||||
if (err) { res.writeHead(404); return res.end('Not found'); }
|
||||
// The pages link to /app.js?v=<build> etc. A new release changes the address, so no proxy cache ("Cache Assets" in
|
||||
// Nginx Proxy Manager, a CDN, the browser) can hand out an old copy.
|
||||
if (p === '/index.html' || p === '/login.html') data = Buffer.from(String(data).replace(/(src|href)="(\/[\w-]+\.js|\/style\.css)"/g, `$1="$2?v=${BUILD}"`));
|
||||
if (p === '/index.html' || p === '/login.html' || p === '/board.html') data = Buffer.from(String(data).replace(/(src|href)="(\/[\w-]+\.js|\/style\.css)"/g, `$1="$2?v=${BUILD}"`));
|
||||
const headers = { 'content-type': TYPES[path.extname(file)] || 'application/octet-stream' };
|
||||
if (p.startsWith('/icons/')) headers['cache-control'] = 'public, max-age=604800'; // icons and splash screens rarely change
|
||||
else { // everything else: the browser may keep a copy but must check with us first, so a new version is never missed
|
||||
@@ -603,20 +605,21 @@ async function handleRequest(req, res) {
|
||||
if (pathname === '/healthz') { res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }); return res.end('{"ok":true}'); }
|
||||
if (crossSite(req) && pathname !== '/api/oo/callback') return forbidden(res, 'Cross-site request refused');
|
||||
if (pathname.startsWith('/api/auth/')) return auth.handle(req, res);
|
||||
if (pathname === '/api/board' || pathname.startsWith('/api/board/') || pathname === '/api/device' || pathname.startsWith('/api/device/')) return board.handle(req, res); // family devices: the board and picking a profile (each checks who is asking)
|
||||
if (pathname === '/api/mail/callback' && mail.outlookOn()) return mail.callback(req, res); // Microsoft sends the person back here; the one-time state says who they are
|
||||
if (pathname.startsWith('/cal/feed/')) return cal.feed(req, res); // subscribe feed: protected by its own secret token, not the session
|
||||
// Everything else needs a passkey session, except the sign-in page and its assets,
|
||||
// and OnlyOffice's save callback (which carries its own signed token).
|
||||
const user = auth.currentUser(req);
|
||||
if (user) auth.refresh(req, res, user);
|
||||
const open = ['/login', '/login.js', '/webauthn.js', '/style.css', '/icon.svg', '/manifest.webmanifest', '/sw.js', '/api/oo/callback'];
|
||||
const open = ['/login', '/login.js', '/board', '/board.js', '/webauthn.js', '/style.css', '/icon.svg', '/manifest.webmanifest', '/sw.js', '/api/oo/callback'];
|
||||
if (!user) {
|
||||
if (open.includes(pathname) || pathname.startsWith('/icons/')) {
|
||||
if (req.method === 'POST' && req.url.startsWith('/api/oo/callback')) return OO_OK ? ooCallback(req, res) : json(res, 403, { error: 1 });
|
||||
return serveStatic(req, res);
|
||||
}
|
||||
if (/^\/(api|sf|plex)\//.test(pathname)) { res.writeHead(401, { 'content-type': 'application/json', 'x-hub-auth': 'required' }); return res.end('{"error":"Sign in required"}'); }
|
||||
res.writeHead(302, { location: '/login' }); return res.end();
|
||||
res.writeHead(302, { location: auth.deviceOf(req) ? '/board' : '/login' }); return res.end();
|
||||
}
|
||||
// From here on, everything runs "as" this person: their own folder, and only the tabs they were given.
|
||||
return users.runAs(user, async () => {
|
||||
|
||||
Reference in new issue
Block a user