Let the coordinator set a workspace's picture
CI / check (push) Canceled after 0s

update_workspace now takes an image media id (or null to remove the
picture). updateWorkspace writes image_media_id through
resolveWorkspacePicture, which only accepts the caller's own image and
deletes the picture it replaced, so a bot can never show someone else's
file or drop a picture it is still using.
This commit is contained in:
2026-10-22 21:10:06 +01:00
parent 34667581b6
commit a5eb2b6a95
6 changed files with 221 additions and 10 deletions
@@ -0,0 +1,66 @@
import { describe, expect, test } from "bun:test";
import AppError from "./app-error";
import resolveWorkspacePicture from "./resolve-workspace-picture";
const image = (id: number) => ({ id, media_type: "image" });
describe("resolveWorkspacePicture", () => {
test("leaves the picture alone when nothing is asked for", () => {
expect(resolveWorkspacePicture({ current: 4 })).toBeUndefined();
});
test("sets a new image of the caller's", () => {
expect(
resolveWorkspacePicture({ requested: 9, current: 4, media: image(9) }),
).toBe(9);
});
test("clears the picture with null", () => {
expect(
resolveWorkspacePicture({ requested: null, current: 4 }),
).toBeNull();
});
test("naming the picture it already has changes nothing, so it is never deleted", () => {
expect(
resolveWorkspacePicture({ requested: 4, current: 4 }),
).toBeUndefined();
expect(
resolveWorkspacePicture({ requested: "4", current: 4, media: image(4) }),
).toBeUndefined();
});
test("refuses media that isn't the caller's", () => {
expect(() =>
resolveWorkspacePicture({ requested: 9, current: null, media: null }),
).toThrow(AppError);
});
test("refuses anything that isn't an image", () => {
expect(() =>
resolveWorkspacePicture({
requested: 9,
current: null,
media: { id: 9, media_type: "document" },
}),
).toThrow(`A workspace picture has to be an image.`);
});
test("refuses an id that names a different row than the one looked up", () => {
expect(() =>
resolveWorkspacePicture({
requested: 9,
current: null,
media: image(10),
}),
).toThrow(AppError);
});
test("refuses an id that isn't a positive integer", () => {
for (const requested of [0, -3, 1.5, "abc"]) {
expect(() =>
resolveWorkspacePicture({ requested, current: null }),
).toThrow(AppError);
}
});
});
+51
View File
@@ -0,0 +1,51 @@
import AppError from "@/src/utils/app-error";
type Params = {
/**
* The `workspace_media_id` the caller asked for: a new picture, or
* null to remove the one it has. Undefined leaves it alone.
*/
requested?: number | string | null;
/**
* The picture the workspace has now.
*/
current?: number | string | null;
/**
* The media row the requested id names, when it was found for the
* caller (`grabOwnPictureMedia`); required to set a new picture.
*/
media?: { id?: number | string | null; media_type?: string | null } | null;
};
/**
* What `updateWorkspace` should write into `workspaces.image_media_id`:
* undefined to leave it as it is, null to clear it, or the new media id.
*
* A new picture has to be the caller's own image: the resolver refuses an
* id that isn't an image, and the media lookup refuses one that isn't
* theirs. Handing back the picture it already has changes nothing, so
* saying "use this picture" twice never deletes it.
*/
export default function resolveWorkspacePicture({ requested, current, media }: Params) {
if (requested === undefined) return undefined;
if (requested === null) return null;
const id = Number(requested);
if (!Number.isInteger(id) || id <= 0) {
throw new AppError(`Pick an image for the workspace.`);
}
if (current != null && Number(current) == id) return undefined;
if (!media || Number(media.id) != id) {
throw new AppError(`That picture isn't one of yours. Upload it again.`);
}
if (media.media_type != "image") {
throw new AppError(`A workspace picture has to be an image.`);
}
return id;
}