dragonflight/services/mam-api/src/routes/capture.js

66 lines
1.9 KiB
JavaScript
Raw Normal View History

feat(mam-api): finish per-project authz on the deferred routers 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>
2026-05-29 23:48:02 -04:00
// 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;