Spaces:
Paused
Paused
Download src/api/iserv-notes.js from Luca448/APP-Backend: direct link, hf CLI and curl.
- Browser
- Download file 3.43 kB
-
https://huggingface.co/spaces/Luca448/APP-Backend/resolve/main/src/api/iserv-notes.js
- Command line
-
hf download hf://spaces/Luca448/APP-Backend/src/api/iserv-notes.js
-
curl -L -o iserv-notes.js https://huggingface.co/spaces/Luca448/APP-Backend/resolve/main/src/api/iserv-notes.js
3.43 kB
| // GET /api/notes/iserv/tasks and GET /api/notes/iserv/file?path=: the notes app's own view of IServ (tasks with | |
| // status, description and provided files). IServ credentials come from the Space secrets (ISERV_BASE_URL, | |
| // ISERV_USERNAME, ISERV_PASSWORD), never from the request. Unlike the other /api/notes routes these FAIL CLOSED: | |
| // without a NOTES_ACCESS_TOKEN of at least 16 characters nothing is served. Errors carry a code only (the messages | |
| // of Puppeteer and IServ quote the school domain and task text). Nothing is logged. | |
| import { timingSafeEqual } from 'node:crypto'; | |
| import { downloadIServFile, fetchIServOverview } from './iserv.js'; | |
| const CACHE_MS = 3 * 60_000; // several opens of the app must not log in to IServ each time | |
| /** A path on the school server: /iserv/…, no scheme or host, no dot segments, no double slash. */ | |
| export function isSafeIServPath(path) { | |
| return typeof path === 'string' && path.startsWith('/iserv/') && path.length <= 2000 && !/[\\\s]|\.\.|\/\//.test(path); | |
| } | |
| function keyOk(req) { | |
| const expected = Buffer.from(process.env.NOTES_ACCESS_TOKEN || ''); | |
| const offered = [req.headers['x-app-key'], (req.headers.authorization || '').replace(/^Bearer\s+/i, '')]; | |
| return offered.some((o) => o && Buffer.byteLength(o) === expected.length && timingSafeEqual(Buffer.from(o), expected)); | |
| } | |
| /** true if the request may continue; otherwise the answer is already sent. */ | |
| function allowed(req, res) { | |
| res.set('Cache-Control', 'no-store'); | |
| if ((process.env.NOTES_ACCESS_TOKEN || '').length < 16) return void res.status(503).json({ error: 'config' }); | |
| if (!keyOk(req)) return void res.status(401).json({ error: 'unauthorized' }); | |
| const { ISERV_BASE_URL: url, ISERV_USERNAME: user, ISERV_PASSWORD: pass } = process.env; | |
| if (!url || !user || !pass || !url.startsWith('https://')) return void res.status(503).json({ error: 'config' }); | |
| return true; | |
| } | |
| export function registerIServNotes(app, { overview = fetchIServOverview, file = downloadIServFile, now = Date.now } = {}) { | |
| let running = Promise.resolve(); // one IServ session at a time | |
| let cached = null; // { at, tasks }, memory only | |
| const credentials = () => [process.env.ISERV_BASE_URL, process.env.ISERV_USERNAME, process.env.ISERV_PASSWORD]; | |
| const queued = (run) => { | |
| const job = running.then(run, run); | |
| running = job.catch(() => {}); | |
| return job; | |
| }; | |
| app.get('/api/notes/iserv/tasks', async (req, res) => { | |
| if (!allowed(req, res)) return; | |
| try { | |
| const { at, tasks } = await queued(async () => { | |
| if (cached && now() - cached.at < CACHE_MS) return cached; | |
| cached = { at: now(), tasks: await overview(...credentials()) }; | |
| return cached; | |
| }); | |
| res.json({ tasks, fetchedAt: new Date(at).toISOString() }); | |
| } catch { | |
| res.status(502).json({ error: 'unreachable' }); | |
| } | |
| }); | |
| app.get('/api/notes/iserv/file', async (req, res) => { | |
| if (!allowed(req, res)) return; | |
| const path = req.query.path; | |
| if (!isSafeIServPath(path)) return res.status(400).json({ error: 'bad_path' }); | |
| try { | |
| const { contentType, buffer } = await queued(() => file(...credentials(), path)); | |
| res.set('Content-Type', contentType).send(buffer); | |
| } catch (e) { | |
| const code = e?.code === 'too_large' || e?.code === 'refused' ? e.code : 'unreachable'; | |
| res.status(code === 'too_large' ? 413 : 502).json({ error: code }); | |
| } | |
| }); | |
| } | |