Phase 1 scoped only projects/assets/bins and left recorders, sequences,
imports, comments carrying TODO(authz) markers. A scoped editor/viewer could
still read and mutate those across every project. This closes the gap using
the existing authz.js helpers — no open TODO(authz) markers remain.
- recorders: param('id') resolves project + view baseline, requireRecorderEdit
on PATCH/DELETE/start/stop, GET / filtered by accessibleProjectIds, POST /
asserts edit on the target project (null project = admin-only)
- sequences: same param pattern + requireSequenceEdit on PUT/:id,/clips,conform
and DELETE; GET//POST/ assert on the query/body project
- imports: POST /youtube asserts edit on the body projectId
- comments: router.use guard resolves project via the asset (view to read, edit
to write); also fixes the author bug (req.session.userId -> req.user.id, which
was always NULL so comments had no recorded author)
- capture: intentionally any-logged-in (shared hardware, asset scoped on
registration) — TODO replaced with a rationale note
Security fixes from review of this change:
- recorders POST /:id/start: a per-take projectId override could route a live
asset into a project the caller lacks edit on — now asserts edit on the
override target
- sequences PUT /:id/clips: spliced asset_ids weren't checked, so an editor
could pull in (and via GET /:id leak signed proxy URLs for) assets from a
project they can't access — now every clip asset must belong to the
sequence's project; pre-transaction queries moved inside try/catch so a DB
error returns 500 instead of hanging the request
- tests: recorders-access, sequences-access (incl. cross-project clip guard),
comments-access (incl. author-id regression)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
65 lines
1.9 KiB
JavaScript
65 lines
1.9 KiB
JavaScript
// authz: intentionally any-logged-in (no per-project scoping). This is a thin
|
|
// proxy to shared capture hardware with no project_id of its own; the resulting
|
|
// asset is scoped when it's registered via the /assets route. Gated by the
|
|
// global requireAuth in index.js, like the rest of /api/v1.
|
|
import express from 'express';
|
|
|
|
const router = express.Router();
|
|
|
|
const CAPTURE_URL = process.env.CAPTURE_URL || 'http://capture:3001';
|
|
|
|
async function proxyRequest(method, path, body = null) {
|
|
const options = {
|
|
method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
signal: AbortSignal.timeout(8000),
|
|
};
|
|
if (body) options.body = JSON.stringify(body);
|
|
|
|
const response = await fetch(`${CAPTURE_URL}${path}`, options);
|
|
const text = await response.text();
|
|
|
|
let data;
|
|
try {
|
|
data = JSON.parse(text);
|
|
} catch {
|
|
// Capture service returned non-JSON (HTML error page, plain text, etc.)
|
|
data = { message: text.slice(0, 300) || '(empty response)' };
|
|
}
|
|
|
|
return { status: response.status, data };
|
|
}
|
|
|
|
// POST /start
|
|
router.post('/start', async (req, res, next) => {
|
|
try {
|
|
const { status, data } = await proxyRequest('POST', '/start', req.body);
|
|
res.status(status).json(data);
|
|
} catch (err) { next(err); }
|
|
});
|
|
|
|
// POST /stop
|
|
router.post('/stop', async (req, res, next) => {
|
|
try {
|
|
const { status, data } = await proxyRequest('POST', '/stop', req.body);
|
|
res.status(status).json(data);
|
|
} catch (err) { next(err); }
|
|
});
|
|
|
|
// GET /status
|
|
router.get('/status', async (req, res, next) => {
|
|
try {
|
|
const { status, data } = await proxyRequest('GET', '/status');
|
|
res.status(status).json(data);
|
|
} catch (err) { next(err); }
|
|
});
|
|
|
|
// GET /devices
|
|
router.get('/devices', async (req, res, next) => {
|
|
try {
|
|
const { status, data } = await proxyRequest('GET', '/devices');
|
|
res.status(status).json(data);
|
|
} catch (err) { next(err); }
|
|
});
|
|
|
|
export default router;
|