scope job status and cancel to the owner or an admin

This commit is contained in:
Jarek Krochmalski committed 2026-09-06 17:03:47 +05:30
1 parent bad04e0056
commit 63850bf5ae
3 files changed
+130 -5

No files matched your search

+19 -1
View File
@@ -1,4 +1,5 @@
import { randomUUID } from 'crypto';
import { getRequestContext } from './request-context';
export interface JobLine {
event?: string; // 'result', 'progress', etc. — undefined for bare data lines
@@ -14,6 +15,8 @@ export interface Job {
updatedAt: number;
/** Set when a client requests cancellation; long-running jobs should poll this. */
cancelRequested?: boolean;
/** User id that created the job, captured from the request context. null when auth is disabled. */
ownerId: number | null;
}
const jobs = new Map<string, Job>();
@@ -24,12 +27,27 @@ export function createJob(): Job {
status: 'running',
lines: [],
createdAt: Date.now(),
updatedAt: Date.now()
updatedAt: Date.now(),
ownerId: getRequestContext()?.user?.id ?? null
};
jobs.set(job.id, job);
return job;
}
/**
* Whether the current request may read/cancel this job. Owner or admin only.
* When auth is disabled (or the job has no owner) access is open, matching the
* rest of the app's initial-setup behavior.
*/
export function canAccessJob(job: Job): boolean {
const ctx = getRequestContext();
if (!ctx || !ctx.authEnabled) return true;
if (job.ownerId === null) return true;
const user = ctx.user;
if (!user) return false;
return user.isAdmin || user.id === job.ownerId;
}
export function getJob(id: string): Job | undefined {
return jobs.get(id);
}
+15 -4
View File
@@ -1,22 +1,25 @@
import { json } from '@sveltejs/kit';
import { getJob, cancelJob } from '$lib/server/jobs';
import { getJob, cancelJob, canAccessJob } from '$lib/server/jobs';
import type { RequestHandler } from './$types';
/**
* GET /api/jobs/[id]
* Poll a job's status and accumulated lines.
* Returns all lines every time — client tracks its own cursor locally.
*
* Authenticated via the global hook. Only the user who created the job (or an
* admin) may read it; others get a 404 so a job's existence is not revealed.
*/
/**
* @openapi
* summary: Poll a background job's status and accumulated output lines (no auth — job ids are unguessable UUIDs)
* summary: Poll a background job's status and accumulated output lines (owner or admin only)
* path: id:string! Job id (UUID)
* resp-200: {id:string!, status:string!, lines:array<{event:string, data:{}}>!, result:{}}
* resp-404: Job not found
*/
export const GET: RequestHandler = async ({ params }) => {
const job = getJob(params.id);
if (!job) {
if (!job || !canAccessJob(job)) {
return json({ error: 'Job not found' }, { status: 404 });
}
@@ -33,12 +36,20 @@ export const GET: RequestHandler = async ({ params }) => {
* Request cancellation of a running job. The job's operation polls the flag
* between units of work and stops gracefully.
*
* Only the user who created the job (or an admin) may cancel it; others get a
* 404 so a job's existence is not revealed.
*
* @openapi
* summary: Request cancellation of a running background job (no auth — job ids are unguessable UUIDs)
* summary: Request cancellation of a running background job (owner or admin only)
* path: id:string! Job id (UUID)
* resp-200: {cancelled:boolean!}
* resp-404: Job not found
*/
export const DELETE: RequestHandler = async ({ params }) => {
const job = getJob(params.id);
if (!job || !canAccessJob(job)) {
return json({ error: 'Job not found' }, { status: 404 });
}
const cancelled = cancelJob(params.id);
return json({ cancelled });
};