v3: added playlist moderation permission
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 43s

This commit is contained in:
khannurien
2026-06-28 06:37:23 +00:00
parent 4fb5caa146
commit 551e29c564
8 changed files with 99 additions and 66 deletions

View File

@@ -2,11 +2,16 @@ import { type AuthPayload, type Role } from "../model/interfaces.ts";
// Named capabilities checked at enforcement points. Extend this union as new
// gated capabilities are added (e.g. "invite:manage").
export type Permission = "dump:moderate" | "comment:moderate" | "user:manage";
export type Permission =
| "dump:moderate"
| "comment:moderate"
| "playlist:moderate"
| "user:manage";
const ALL_PERMISSIONS: readonly Permission[] = [
"dump:moderate",
"comment:moderate",
"playlist:moderate",
"user:manage",
];
@@ -14,7 +19,7 @@ const ALL_PERMISSIONS: readonly Permission[] = [
// and future); `moderator` gets the content-moderation permissions only.
export const ROLE_PERMISSIONS: Record<Role, readonly Permission[]> = {
admin: ALL_PERMISSIONS,
moderator: ["dump:moderate", "comment:moderate"],
moderator: ["dump:moderate", "comment:moderate", "playlist:moderate"],
user: [],
};

View File

@@ -8,6 +8,7 @@ import {
isUpdatePlaylistRequest,
} from "../model/interfaces.ts";
import { authMiddleware, type AuthState } from "../middleware/auth.ts";
import { can } from "../lib/permissions.ts";
import {
addDumpToPlaylist,
createPlaylist,
@@ -72,13 +73,18 @@ router.put("/:playlistId", authMiddleware, async (ctx) => {
ctx.params.playlistId,
body,
ctx.state.user.userId,
can(ctx.state.user, "playlist:moderate"),
);
ctx.response.body = { success: true, data: playlist };
});
// DELETE /api/playlists/:playlistId
router.delete("/:playlistId", authMiddleware, (ctx) => {
deletePlaylist(ctx.params.playlistId, ctx.state.user.userId);
deletePlaylist(
ctx.params.playlistId,
ctx.state.user.userId,
can(ctx.state.user, "playlist:moderate"),
);
ctx.response.status = 204;
});
@@ -88,6 +94,7 @@ router.post("/:playlistId/dumps/:dumpId", authMiddleware, (ctx) => {
ctx.params.playlistId,
ctx.params.dumpId,
ctx.state.user.userId,
can(ctx.state.user, "playlist:moderate"),
);
ctx.response.status = 204;
});
@@ -98,6 +105,7 @@ router.delete("/:playlistId/dumps/:dumpId", authMiddleware, (ctx) => {
ctx.params.playlistId,
ctx.params.dumpId,
ctx.state.user.userId,
can(ctx.state.user, "playlist:moderate"),
);
ctx.response.status = 204;
});
@@ -129,6 +137,7 @@ router.post("/:playlistId/image", authMiddleware, async (ctx) => {
ctx.params.playlistId,
mime,
ctx.state.user.userId,
can(ctx.state.user, "playlist:moderate"),
);
const filePath = `${PLAYLIST_IMAGES_DIR}/${playlist.id}`;
await Deno.mkdir(PLAYLIST_IMAGES_DIR, { recursive: true });
@@ -166,7 +175,12 @@ router.put("/:playlistId/order", authMiddleware, async (ctx) => {
"Invalid request",
);
}
reorderPlaylist(ctx.params.playlistId, body.dumpIds, ctx.state.user.userId);
reorderPlaylist(
ctx.params.playlistId,
body.dumpIds,
ctx.state.user.userId,
can(ctx.state.user, "playlist:moderate"),
);
ctx.response.body = { success: true, data: null };
});

View File

@@ -159,10 +159,11 @@ export function updatePlaylist(
playlistId: string,
req: UpdatePlaylistRequest,
requestingUserId: string,
canModerate: boolean,
): Playlist {
const playlist = getPlaylistById(playlistId);
if (playlist.userId !== requestingUserId) {
if (playlist.userId !== requestingUserId && !canModerate) {
throw new APIException(APIErrorCode.UNAUTHORIZED, 403, "Forbidden");
}
@@ -212,10 +213,11 @@ export function updatePlaylist(
export function deletePlaylist(
playlistId: string,
requestingUserId: string,
canModerate: boolean,
): void {
const playlist = getPlaylistById(playlistId);
if (playlist.userId !== requestingUserId) {
if (playlist.userId !== requestingUserId && !canModerate) {
throw new APIException(APIErrorCode.UNAUTHORIZED, 403, "Forbidden");
}
@@ -227,9 +229,10 @@ export function setPlaylistImage(
playlistId: string,
imageMime: string,
requestingUserId: string,
canModerate: boolean,
): Playlist {
const playlist = getPlaylistById(playlistId);
if (playlist.userId !== requestingUserId) {
if (playlist.userId !== requestingUserId && !canModerate) {
throw new APIException(APIErrorCode.UNAUTHORIZED, 403, "Forbidden");
}
db.prepare(`UPDATE playlists SET image_mime = ? WHERE id = ?;`).run(
@@ -245,10 +248,11 @@ export function addDumpToPlaylist(
playlistId: string,
dumpId: string,
requestingUserId: string,
canModerate: boolean,
): void {
const playlist = getPlaylistById(playlistId);
if (playlist.userId !== requestingUserId) {
if (playlist.userId !== requestingUserId && !canModerate) {
throw new APIException(APIErrorCode.UNAUTHORIZED, 403, "Forbidden");
}
@@ -298,10 +302,11 @@ export function removeDumpFromPlaylist(
playlistId: string,
dumpId: string,
requestingUserId: string,
canModerate: boolean,
): void {
const playlist = getPlaylistById(playlistId);
if (playlist.userId !== requestingUserId) {
if (playlist.userId !== requestingUserId && !canModerate) {
throw new APIException(APIErrorCode.UNAUTHORIZED, 403, "Forbidden");
}
@@ -317,10 +322,11 @@ export function reorderPlaylist(
playlistId: string,
dumpIds: string[],
requestingUserId: string,
canModerate: boolean,
): void {
const playlist = getPlaylistById(playlistId);
if (playlist.userId !== requestingUserId) {
if (playlist.userId !== requestingUserId && !canModerate) {
throw new APIException(APIErrorCode.UNAUTHORIZED, 403, "Forbidden");
}