v3: added playlist moderation permission
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 43s
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 43s
This commit is contained in:
@@ -2,11 +2,16 @@ import { type AuthPayload, type Role } from "../model/interfaces.ts";
|
|||||||
|
|
||||||
// Named capabilities checked at enforcement points. Extend this union as new
|
// Named capabilities checked at enforcement points. Extend this union as new
|
||||||
// gated capabilities are added (e.g. "invite:manage").
|
// 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[] = [
|
const ALL_PERMISSIONS: readonly Permission[] = [
|
||||||
"dump:moderate",
|
"dump:moderate",
|
||||||
"comment:moderate",
|
"comment:moderate",
|
||||||
|
"playlist:moderate",
|
||||||
"user:manage",
|
"user:manage",
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -14,7 +19,7 @@ const ALL_PERMISSIONS: readonly Permission[] = [
|
|||||||
// and future); `moderator` gets the content-moderation permissions only.
|
// and future); `moderator` gets the content-moderation permissions only.
|
||||||
export const ROLE_PERMISSIONS: Record<Role, readonly Permission[]> = {
|
export const ROLE_PERMISSIONS: Record<Role, readonly Permission[]> = {
|
||||||
admin: ALL_PERMISSIONS,
|
admin: ALL_PERMISSIONS,
|
||||||
moderator: ["dump:moderate", "comment:moderate"],
|
moderator: ["dump:moderate", "comment:moderate", "playlist:moderate"],
|
||||||
user: [],
|
user: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
isUpdatePlaylistRequest,
|
isUpdatePlaylistRequest,
|
||||||
} from "../model/interfaces.ts";
|
} from "../model/interfaces.ts";
|
||||||
import { authMiddleware, type AuthState } from "../middleware/auth.ts";
|
import { authMiddleware, type AuthState } from "../middleware/auth.ts";
|
||||||
|
import { can } from "../lib/permissions.ts";
|
||||||
import {
|
import {
|
||||||
addDumpToPlaylist,
|
addDumpToPlaylist,
|
||||||
createPlaylist,
|
createPlaylist,
|
||||||
@@ -72,13 +73,18 @@ router.put("/:playlistId", authMiddleware, async (ctx) => {
|
|||||||
ctx.params.playlistId,
|
ctx.params.playlistId,
|
||||||
body,
|
body,
|
||||||
ctx.state.user.userId,
|
ctx.state.user.userId,
|
||||||
|
can(ctx.state.user, "playlist:moderate"),
|
||||||
);
|
);
|
||||||
ctx.response.body = { success: true, data: playlist };
|
ctx.response.body = { success: true, data: playlist };
|
||||||
});
|
});
|
||||||
|
|
||||||
// DELETE /api/playlists/:playlistId
|
// DELETE /api/playlists/:playlistId
|
||||||
router.delete("/:playlistId", authMiddleware, (ctx) => {
|
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;
|
ctx.response.status = 204;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -88,6 +94,7 @@ router.post("/:playlistId/dumps/:dumpId", authMiddleware, (ctx) => {
|
|||||||
ctx.params.playlistId,
|
ctx.params.playlistId,
|
||||||
ctx.params.dumpId,
|
ctx.params.dumpId,
|
||||||
ctx.state.user.userId,
|
ctx.state.user.userId,
|
||||||
|
can(ctx.state.user, "playlist:moderate"),
|
||||||
);
|
);
|
||||||
ctx.response.status = 204;
|
ctx.response.status = 204;
|
||||||
});
|
});
|
||||||
@@ -98,6 +105,7 @@ router.delete("/:playlistId/dumps/:dumpId", authMiddleware, (ctx) => {
|
|||||||
ctx.params.playlistId,
|
ctx.params.playlistId,
|
||||||
ctx.params.dumpId,
|
ctx.params.dumpId,
|
||||||
ctx.state.user.userId,
|
ctx.state.user.userId,
|
||||||
|
can(ctx.state.user, "playlist:moderate"),
|
||||||
);
|
);
|
||||||
ctx.response.status = 204;
|
ctx.response.status = 204;
|
||||||
});
|
});
|
||||||
@@ -129,6 +137,7 @@ router.post("/:playlistId/image", authMiddleware, async (ctx) => {
|
|||||||
ctx.params.playlistId,
|
ctx.params.playlistId,
|
||||||
mime,
|
mime,
|
||||||
ctx.state.user.userId,
|
ctx.state.user.userId,
|
||||||
|
can(ctx.state.user, "playlist:moderate"),
|
||||||
);
|
);
|
||||||
const filePath = `${PLAYLIST_IMAGES_DIR}/${playlist.id}`;
|
const filePath = `${PLAYLIST_IMAGES_DIR}/${playlist.id}`;
|
||||||
await Deno.mkdir(PLAYLIST_IMAGES_DIR, { recursive: true });
|
await Deno.mkdir(PLAYLIST_IMAGES_DIR, { recursive: true });
|
||||||
@@ -166,7 +175,12 @@ router.put("/:playlistId/order", authMiddleware, async (ctx) => {
|
|||||||
"Invalid request",
|
"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 };
|
ctx.response.body = { success: true, data: null };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -159,10 +159,11 @@ export function updatePlaylist(
|
|||||||
playlistId: string,
|
playlistId: string,
|
||||||
req: UpdatePlaylistRequest,
|
req: UpdatePlaylistRequest,
|
||||||
requestingUserId: string,
|
requestingUserId: string,
|
||||||
|
canModerate: boolean,
|
||||||
): Playlist {
|
): Playlist {
|
||||||
const playlist = getPlaylistById(playlistId);
|
const playlist = getPlaylistById(playlistId);
|
||||||
|
|
||||||
if (playlist.userId !== requestingUserId) {
|
if (playlist.userId !== requestingUserId && !canModerate) {
|
||||||
throw new APIException(APIErrorCode.UNAUTHORIZED, 403, "Forbidden");
|
throw new APIException(APIErrorCode.UNAUTHORIZED, 403, "Forbidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,10 +213,11 @@ export function updatePlaylist(
|
|||||||
export function deletePlaylist(
|
export function deletePlaylist(
|
||||||
playlistId: string,
|
playlistId: string,
|
||||||
requestingUserId: string,
|
requestingUserId: string,
|
||||||
|
canModerate: boolean,
|
||||||
): void {
|
): void {
|
||||||
const playlist = getPlaylistById(playlistId);
|
const playlist = getPlaylistById(playlistId);
|
||||||
|
|
||||||
if (playlist.userId !== requestingUserId) {
|
if (playlist.userId !== requestingUserId && !canModerate) {
|
||||||
throw new APIException(APIErrorCode.UNAUTHORIZED, 403, "Forbidden");
|
throw new APIException(APIErrorCode.UNAUTHORIZED, 403, "Forbidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,9 +229,10 @@ export function setPlaylistImage(
|
|||||||
playlistId: string,
|
playlistId: string,
|
||||||
imageMime: string,
|
imageMime: string,
|
||||||
requestingUserId: string,
|
requestingUserId: string,
|
||||||
|
canModerate: boolean,
|
||||||
): Playlist {
|
): Playlist {
|
||||||
const playlist = getPlaylistById(playlistId);
|
const playlist = getPlaylistById(playlistId);
|
||||||
if (playlist.userId !== requestingUserId) {
|
if (playlist.userId !== requestingUserId && !canModerate) {
|
||||||
throw new APIException(APIErrorCode.UNAUTHORIZED, 403, "Forbidden");
|
throw new APIException(APIErrorCode.UNAUTHORIZED, 403, "Forbidden");
|
||||||
}
|
}
|
||||||
db.prepare(`UPDATE playlists SET image_mime = ? WHERE id = ?;`).run(
|
db.prepare(`UPDATE playlists SET image_mime = ? WHERE id = ?;`).run(
|
||||||
@@ -245,10 +248,11 @@ export function addDumpToPlaylist(
|
|||||||
playlistId: string,
|
playlistId: string,
|
||||||
dumpId: string,
|
dumpId: string,
|
||||||
requestingUserId: string,
|
requestingUserId: string,
|
||||||
|
canModerate: boolean,
|
||||||
): void {
|
): void {
|
||||||
const playlist = getPlaylistById(playlistId);
|
const playlist = getPlaylistById(playlistId);
|
||||||
|
|
||||||
if (playlist.userId !== requestingUserId) {
|
if (playlist.userId !== requestingUserId && !canModerate) {
|
||||||
throw new APIException(APIErrorCode.UNAUTHORIZED, 403, "Forbidden");
|
throw new APIException(APIErrorCode.UNAUTHORIZED, 403, "Forbidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,10 +302,11 @@ export function removeDumpFromPlaylist(
|
|||||||
playlistId: string,
|
playlistId: string,
|
||||||
dumpId: string,
|
dumpId: string,
|
||||||
requestingUserId: string,
|
requestingUserId: string,
|
||||||
|
canModerate: boolean,
|
||||||
): void {
|
): void {
|
||||||
const playlist = getPlaylistById(playlistId);
|
const playlist = getPlaylistById(playlistId);
|
||||||
|
|
||||||
if (playlist.userId !== requestingUserId) {
|
if (playlist.userId !== requestingUserId && !canModerate) {
|
||||||
throw new APIException(APIErrorCode.UNAUTHORIZED, 403, "Forbidden");
|
throw new APIException(APIErrorCode.UNAUTHORIZED, 403, "Forbidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,10 +322,11 @@ export function reorderPlaylist(
|
|||||||
playlistId: string,
|
playlistId: string,
|
||||||
dumpIds: string[],
|
dumpIds: string[],
|
||||||
requestingUserId: string,
|
requestingUserId: string,
|
||||||
|
canModerate: boolean,
|
||||||
): void {
|
): void {
|
||||||
const playlist = getPlaylistById(playlistId);
|
const playlist = getPlaylistById(playlistId);
|
||||||
|
|
||||||
if (playlist.userId !== requestingUserId) {
|
if (playlist.userId !== requestingUserId && !canModerate) {
|
||||||
throw new APIException(APIErrorCode.UNAUTHORIZED, 403, "Forbidden");
|
throw new APIException(APIErrorCode.UNAUTHORIZED, 403, "Forbidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ msgstr "{label} ({count})"
|
|||||||
msgid "{visibleCount, plural, one {# comment} other {# comments}}"
|
msgid "{visibleCount, plural, one {# comment} other {# comments}}"
|
||||||
msgstr "{visibleCount, plural, one {# comment} other {# comments}}"
|
msgstr "{visibleCount, plural, one {# comment} other {# comments}}"
|
||||||
|
|
||||||
#: src/pages/PlaylistDetail.tsx:560
|
#: src/pages/PlaylistDetail.tsx:561
|
||||||
#: src/pages/UserPublicProfile.tsx:732
|
#: src/pages/UserPublicProfile.tsx:732
|
||||||
msgid "← Back"
|
msgid "← Back"
|
||||||
msgstr "← Back"
|
msgstr "← Back"
|
||||||
@@ -180,13 +180,13 @@ msgstr "Can't connect to the live updates server. Upvotes and notifications may
|
|||||||
#: src/components/form/FormActions.tsx:32
|
#: src/components/form/FormActions.tsx:32
|
||||||
#: src/pages/Dump.tsx:359
|
#: src/pages/Dump.tsx:359
|
||||||
#: src/pages/DumpEdit.tsx:433
|
#: src/pages/DumpEdit.tsx:433
|
||||||
#: src/pages/PlaylistDetail.tsx:908
|
#: src/pages/PlaylistDetail.tsx:911
|
||||||
#: src/pages/UserPublicProfile.tsx:1637
|
#: src/pages/UserPublicProfile.tsx:1637
|
||||||
#: src/pages/UserPublicProfile.tsx:1707
|
#: src/pages/UserPublicProfile.tsx:1707
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "Cancel"
|
msgstr "Cancel"
|
||||||
|
|
||||||
#: src/pages/PlaylistDetail.tsx:733
|
#: src/pages/PlaylistDetail.tsx:736
|
||||||
msgid "Cancel removal"
|
msgid "Cancel removal"
|
||||||
msgstr "Cancel removal"
|
msgstr "Cancel removal"
|
||||||
|
|
||||||
@@ -278,7 +278,7 @@ msgstr "Default tab"
|
|||||||
#: src/components/CommentThread.tsx:376
|
#: src/components/CommentThread.tsx:376
|
||||||
#: src/components/CommentThread.tsx:382
|
#: src/components/CommentThread.tsx:382
|
||||||
#: src/components/ConfirmModal.tsx:16
|
#: src/components/ConfirmModal.tsx:16
|
||||||
#: src/pages/PlaylistDetail.tsx:904
|
#: src/pages/PlaylistDetail.tsx:907
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr "Delete"
|
msgstr "Delete"
|
||||||
|
|
||||||
@@ -288,7 +288,7 @@ msgid "Delete dump"
|
|||||||
msgstr "Delete dump"
|
msgstr "Delete dump"
|
||||||
|
|
||||||
#: src/components/PlaylistCard.tsx:109
|
#: src/components/PlaylistCard.tsx:109
|
||||||
#: src/pages/PlaylistDetail.tsx:746
|
#: src/pages/PlaylistDetail.tsx:749
|
||||||
#: src/pages/UserPlaylists.tsx:465
|
#: src/pages/UserPlaylists.tsx:465
|
||||||
msgid "Delete playlist"
|
msgid "Delete playlist"
|
||||||
msgstr "Delete playlist"
|
msgstr "Delete playlist"
|
||||||
@@ -301,13 +301,13 @@ msgstr "Delete this comment?"
|
|||||||
msgid "Delete this dump? This cannot be undone."
|
msgid "Delete this dump? This cannot be undone."
|
||||||
msgstr "Delete this dump? This cannot be undone."
|
msgstr "Delete this dump? This cannot be undone."
|
||||||
|
|
||||||
#: src/pages/PlaylistDetail.tsx:745
|
#: src/pages/PlaylistDetail.tsx:748
|
||||||
#: src/pages/UserPlaylists.tsx:464
|
#: src/pages/UserPlaylists.tsx:464
|
||||||
msgid "Delete this playlist? This cannot be undone."
|
msgid "Delete this playlist? This cannot be undone."
|
||||||
msgstr "Delete this playlist? This cannot be undone."
|
msgstr "Delete this playlist? This cannot be undone."
|
||||||
|
|
||||||
#: src/components/PlaylistCreateForm.tsx:78
|
#: src/components/PlaylistCreateForm.tsx:78
|
||||||
#: src/pages/PlaylistDetail.tsx:865
|
#: src/pages/PlaylistDetail.tsx:868
|
||||||
msgid "Description (optional)"
|
msgid "Description (optional)"
|
||||||
msgstr "Description (optional)"
|
msgstr "Description (optional)"
|
||||||
|
|
||||||
@@ -353,7 +353,7 @@ msgstr "Earlier"
|
|||||||
|
|
||||||
#: src/components/CommentThread.tsx:367
|
#: src/components/CommentThread.tsx:367
|
||||||
#: src/pages/Dump.tsx:459
|
#: src/pages/Dump.tsx:459
|
||||||
#: src/pages/PlaylistDetail.tsx:613
|
#: src/pages/PlaylistDetail.tsx:616
|
||||||
msgid "Edit"
|
msgid "Edit"
|
||||||
msgstr "Edit"
|
msgstr "Edit"
|
||||||
|
|
||||||
@@ -366,7 +366,7 @@ msgstr "Edit title"
|
|||||||
#. placeholder {0}: relativeTime(playlist.updatedAt)
|
#. placeholder {0}: relativeTime(playlist.updatedAt)
|
||||||
#: src/components/CommentThread.tsx:317
|
#: src/components/CommentThread.tsx:317
|
||||||
#: src/pages/Dump.tsx:412
|
#: src/pages/Dump.tsx:412
|
||||||
#: src/pages/PlaylistDetail.tsx:652
|
#: src/pages/PlaylistDetail.tsx:655
|
||||||
msgid "edited {0}"
|
msgid "edited {0}"
|
||||||
msgstr "edited {0}"
|
msgstr "edited {0}"
|
||||||
|
|
||||||
@@ -375,7 +375,7 @@ msgstr "edited {0}"
|
|||||||
#. placeholder {0}: playlist.updatedAt.toLocaleString()
|
#. placeholder {0}: playlist.updatedAt.toLocaleString()
|
||||||
#: src/components/CommentThread.tsx:315
|
#: src/components/CommentThread.tsx:315
|
||||||
#: src/pages/Dump.tsx:410
|
#: src/pages/Dump.tsx:410
|
||||||
#: src/pages/PlaylistDetail.tsx:649
|
#: src/pages/PlaylistDetail.tsx:652
|
||||||
msgid "Edited {0}"
|
msgid "Edited {0}"
|
||||||
msgstr "Edited {0}"
|
msgstr "Edited {0}"
|
||||||
|
|
||||||
@@ -424,7 +424,7 @@ msgstr "Failed to post comment"
|
|||||||
msgid "Failed to post reply"
|
msgid "Failed to post reply"
|
||||||
msgstr "Failed to post reply"
|
msgstr "Failed to post reply"
|
||||||
|
|
||||||
#: src/pages/PlaylistDetail.tsx:896
|
#: src/pages/PlaylistDetail.tsx:899
|
||||||
#: src/pages/UserPublicProfile.tsx:1640
|
#: src/pages/UserPublicProfile.tsx:1640
|
||||||
#: src/pages/UserPublicProfile.tsx:1709
|
#: src/pages/UserPublicProfile.tsx:1709
|
||||||
msgid "Failed to save"
|
msgid "Failed to save"
|
||||||
@@ -595,7 +595,7 @@ msgstr "Loading dump…"
|
|||||||
msgid "Loading more…"
|
msgid "Loading more…"
|
||||||
msgstr "Loading more…"
|
msgstr "Loading more…"
|
||||||
|
|
||||||
#: src/pages/PlaylistDetail.tsx:544
|
#: src/pages/PlaylistDetail.tsx:545
|
||||||
msgid "Loading playlist…"
|
msgid "Loading playlist…"
|
||||||
msgstr "Loading playlist…"
|
msgstr "Loading playlist…"
|
||||||
|
|
||||||
@@ -688,7 +688,7 @@ msgstr "New password"
|
|||||||
msgid "New playlist"
|
msgid "New playlist"
|
||||||
msgstr "New playlist"
|
msgstr "New playlist"
|
||||||
|
|
||||||
#: src/pages/PlaylistDetail.tsx:668
|
#: src/pages/PlaylistDetail.tsx:671
|
||||||
msgid "No dumps in this playlist yet."
|
msgid "No dumps in this playlist yet."
|
||||||
msgstr "No dumps in this playlist yet."
|
msgstr "No dumps in this playlist yet."
|
||||||
|
|
||||||
@@ -785,7 +785,7 @@ msgstr "Password updated"
|
|||||||
msgid "Passwords do not match"
|
msgid "Passwords do not match"
|
||||||
msgstr "Passwords do not match"
|
msgstr "Passwords do not match"
|
||||||
|
|
||||||
#: src/pages/PlaylistDetail.tsx:851
|
#: src/pages/PlaylistDetail.tsx:854
|
||||||
msgid "Playlist title"
|
msgid "Playlist title"
|
||||||
msgstr "Playlist title"
|
msgstr "Playlist title"
|
||||||
|
|
||||||
@@ -825,22 +825,22 @@ msgstr "Posting…"
|
|||||||
#: src/components/PlaylistCard.tsx:73
|
#: src/components/PlaylistCard.tsx:73
|
||||||
#: src/components/PlaylistMembershipPanel.tsx:55
|
#: src/components/PlaylistMembershipPanel.tsx:55
|
||||||
#: src/pages/Dump.tsx:418
|
#: src/pages/Dump.tsx:418
|
||||||
#: src/pages/PlaylistDetail.tsx:632
|
#: src/pages/PlaylistDetail.tsx:635
|
||||||
msgid "private"
|
msgid "private"
|
||||||
msgstr "private"
|
msgstr "private"
|
||||||
|
|
||||||
#: src/components/form/SegmentedField.tsx:78
|
#: src/components/form/SegmentedField.tsx:78
|
||||||
#: src/pages/PlaylistDetail.tsx:890
|
#: src/pages/PlaylistDetail.tsx:893
|
||||||
msgid "Private"
|
msgid "Private"
|
||||||
msgstr "Private"
|
msgstr "Private"
|
||||||
|
|
||||||
#: src/components/PlaylistCard.tsx:72
|
#: src/components/PlaylistCard.tsx:72
|
||||||
#: src/pages/PlaylistDetail.tsx:631
|
#: src/pages/PlaylistDetail.tsx:634
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr "public"
|
msgstr "public"
|
||||||
|
|
||||||
#: src/components/form/SegmentedField.tsx:77
|
#: src/components/form/SegmentedField.tsx:77
|
||||||
#: src/pages/PlaylistDetail.tsx:883
|
#: src/pages/PlaylistDetail.tsx:886
|
||||||
msgid "Public"
|
msgid "Public"
|
||||||
msgstr "Public"
|
msgstr "Public"
|
||||||
|
|
||||||
@@ -873,7 +873,7 @@ msgstr "Related"
|
|||||||
msgid "Remove file"
|
msgid "Remove file"
|
||||||
msgstr "Remove file"
|
msgstr "Remove file"
|
||||||
|
|
||||||
#: src/pages/PlaylistDetail.tsx:723
|
#: src/pages/PlaylistDetail.tsx:726
|
||||||
msgid "Remove from playlist"
|
msgid "Remove from playlist"
|
||||||
msgstr "Remove from playlist"
|
msgstr "Remove from playlist"
|
||||||
|
|
||||||
@@ -918,7 +918,7 @@ msgstr "Role"
|
|||||||
#: src/components/CommentThread.tsx:328
|
#: src/components/CommentThread.tsx:328
|
||||||
#: src/pages/Dump.tsx:351
|
#: src/pages/Dump.tsx:351
|
||||||
#: src/pages/DumpEdit.tsx:436
|
#: src/pages/DumpEdit.tsx:436
|
||||||
#: src/pages/PlaylistDetail.tsx:915
|
#: src/pages/PlaylistDetail.tsx:918
|
||||||
#: src/pages/UserPublicProfile.tsx:1629
|
#: src/pages/UserPublicProfile.tsx:1629
|
||||||
#: src/pages/UserPublicProfile.tsx:1699
|
#: src/pages/UserPublicProfile.tsx:1699
|
||||||
msgid "Save"
|
msgid "Save"
|
||||||
@@ -927,7 +927,7 @@ msgstr "Save"
|
|||||||
#: src/components/ChangePasswordModal.tsx:100
|
#: src/components/ChangePasswordModal.tsx:100
|
||||||
#: src/components/CommentThread.tsx:329
|
#: src/components/CommentThread.tsx:329
|
||||||
#: src/pages/Dump.tsx:350
|
#: src/pages/Dump.tsx:350
|
||||||
#: src/pages/PlaylistDetail.tsx:911
|
#: src/pages/PlaylistDetail.tsx:914
|
||||||
#: src/pages/ResetPassword.tsx:124
|
#: src/pages/ResetPassword.tsx:124
|
||||||
#: src/pages/UserPublicProfile.tsx:1626
|
#: src/pages/UserPublicProfile.tsx:1626
|
||||||
#: src/pages/UserPublicProfile.tsx:1696
|
#: src/pages/UserPublicProfile.tsx:1696
|
||||||
@@ -1019,7 +1019,7 @@ msgstr "Title is required."
|
|||||||
msgid "Today"
|
msgid "Today"
|
||||||
msgstr "Today"
|
msgstr "Today"
|
||||||
|
|
||||||
#: src/pages/PlaylistDetail.tsx:735
|
#: src/pages/PlaylistDetail.tsx:738
|
||||||
msgid "Undo"
|
msgid "Undo"
|
||||||
msgstr "Undo"
|
msgstr "Undo"
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -42,7 +42,7 @@ msgstr "{label} ({count})"
|
|||||||
msgid "{visibleCount, plural, one {# comment} other {# comments}}"
|
msgid "{visibleCount, plural, one {# comment} other {# comments}}"
|
||||||
msgstr "{visibleCount, plural, one {# commentaire} other {# commentaires}}"
|
msgstr "{visibleCount, plural, one {# commentaire} other {# commentaires}}"
|
||||||
|
|
||||||
#: src/pages/PlaylistDetail.tsx:560
|
#: src/pages/PlaylistDetail.tsx:561
|
||||||
#: src/pages/UserPublicProfile.tsx:732
|
#: src/pages/UserPublicProfile.tsx:732
|
||||||
msgid "← Back"
|
msgid "← Back"
|
||||||
msgstr "← Retour"
|
msgstr "← Retour"
|
||||||
@@ -180,13 +180,13 @@ msgstr "Impossible de se connecter au serveur de mises à jour en direct. Les vo
|
|||||||
#: src/components/form/FormActions.tsx:32
|
#: src/components/form/FormActions.tsx:32
|
||||||
#: src/pages/Dump.tsx:359
|
#: src/pages/Dump.tsx:359
|
||||||
#: src/pages/DumpEdit.tsx:433
|
#: src/pages/DumpEdit.tsx:433
|
||||||
#: src/pages/PlaylistDetail.tsx:908
|
#: src/pages/PlaylistDetail.tsx:911
|
||||||
#: src/pages/UserPublicProfile.tsx:1637
|
#: src/pages/UserPublicProfile.tsx:1637
|
||||||
#: src/pages/UserPublicProfile.tsx:1707
|
#: src/pages/UserPublicProfile.tsx:1707
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "Annuler"
|
msgstr "Annuler"
|
||||||
|
|
||||||
#: src/pages/PlaylistDetail.tsx:733
|
#: src/pages/PlaylistDetail.tsx:736
|
||||||
msgid "Cancel removal"
|
msgid "Cancel removal"
|
||||||
msgstr "Annuler la suppression"
|
msgstr "Annuler la suppression"
|
||||||
|
|
||||||
@@ -278,7 +278,7 @@ msgstr "Onglet par défaut"
|
|||||||
#: src/components/CommentThread.tsx:376
|
#: src/components/CommentThread.tsx:376
|
||||||
#: src/components/CommentThread.tsx:382
|
#: src/components/CommentThread.tsx:382
|
||||||
#: src/components/ConfirmModal.tsx:16
|
#: src/components/ConfirmModal.tsx:16
|
||||||
#: src/pages/PlaylistDetail.tsx:904
|
#: src/pages/PlaylistDetail.tsx:907
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr "Supprimer"
|
msgstr "Supprimer"
|
||||||
|
|
||||||
@@ -288,7 +288,7 @@ msgid "Delete dump"
|
|||||||
msgstr "Supprimer la reco"
|
msgstr "Supprimer la reco"
|
||||||
|
|
||||||
#: src/components/PlaylistCard.tsx:109
|
#: src/components/PlaylistCard.tsx:109
|
||||||
#: src/pages/PlaylistDetail.tsx:746
|
#: src/pages/PlaylistDetail.tsx:749
|
||||||
#: src/pages/UserPlaylists.tsx:465
|
#: src/pages/UserPlaylists.tsx:465
|
||||||
msgid "Delete playlist"
|
msgid "Delete playlist"
|
||||||
msgstr "Supprimer la collection"
|
msgstr "Supprimer la collection"
|
||||||
@@ -301,13 +301,13 @@ msgstr "Supprimer ce commentaire ?"
|
|||||||
msgid "Delete this dump? This cannot be undone."
|
msgid "Delete this dump? This cannot be undone."
|
||||||
msgstr "Supprimer cette reco ? Cette action est irréversible."
|
msgstr "Supprimer cette reco ? Cette action est irréversible."
|
||||||
|
|
||||||
#: src/pages/PlaylistDetail.tsx:745
|
#: src/pages/PlaylistDetail.tsx:748
|
||||||
#: src/pages/UserPlaylists.tsx:464
|
#: src/pages/UserPlaylists.tsx:464
|
||||||
msgid "Delete this playlist? This cannot be undone."
|
msgid "Delete this playlist? This cannot be undone."
|
||||||
msgstr "Supprimer cette collection ? Cette action est irréversible."
|
msgstr "Supprimer cette collection ? Cette action est irréversible."
|
||||||
|
|
||||||
#: src/components/PlaylistCreateForm.tsx:78
|
#: src/components/PlaylistCreateForm.tsx:78
|
||||||
#: src/pages/PlaylistDetail.tsx:865
|
#: src/pages/PlaylistDetail.tsx:868
|
||||||
msgid "Description (optional)"
|
msgid "Description (optional)"
|
||||||
msgstr "Description (facultatif)"
|
msgstr "Description (facultatif)"
|
||||||
|
|
||||||
@@ -353,7 +353,7 @@ msgstr "Plus tôt"
|
|||||||
|
|
||||||
#: src/components/CommentThread.tsx:367
|
#: src/components/CommentThread.tsx:367
|
||||||
#: src/pages/Dump.tsx:459
|
#: src/pages/Dump.tsx:459
|
||||||
#: src/pages/PlaylistDetail.tsx:613
|
#: src/pages/PlaylistDetail.tsx:616
|
||||||
msgid "Edit"
|
msgid "Edit"
|
||||||
msgstr "Modifier"
|
msgstr "Modifier"
|
||||||
|
|
||||||
@@ -366,7 +366,7 @@ msgstr "Modifier le titre"
|
|||||||
#. placeholder {0}: relativeTime(playlist.updatedAt)
|
#. placeholder {0}: relativeTime(playlist.updatedAt)
|
||||||
#: src/components/CommentThread.tsx:317
|
#: src/components/CommentThread.tsx:317
|
||||||
#: src/pages/Dump.tsx:412
|
#: src/pages/Dump.tsx:412
|
||||||
#: src/pages/PlaylistDetail.tsx:652
|
#: src/pages/PlaylistDetail.tsx:655
|
||||||
msgid "edited {0}"
|
msgid "edited {0}"
|
||||||
msgstr "modifié {0}"
|
msgstr "modifié {0}"
|
||||||
|
|
||||||
@@ -375,7 +375,7 @@ msgstr "modifié {0}"
|
|||||||
#. placeholder {0}: playlist.updatedAt.toLocaleString()
|
#. placeholder {0}: playlist.updatedAt.toLocaleString()
|
||||||
#: src/components/CommentThread.tsx:315
|
#: src/components/CommentThread.tsx:315
|
||||||
#: src/pages/Dump.tsx:410
|
#: src/pages/Dump.tsx:410
|
||||||
#: src/pages/PlaylistDetail.tsx:649
|
#: src/pages/PlaylistDetail.tsx:652
|
||||||
msgid "Edited {0}"
|
msgid "Edited {0}"
|
||||||
msgstr "Modifié le {0}"
|
msgstr "Modifié le {0}"
|
||||||
|
|
||||||
@@ -424,7 +424,7 @@ msgstr "Impossible de publier le commentaire"
|
|||||||
msgid "Failed to post reply"
|
msgid "Failed to post reply"
|
||||||
msgstr "Impossible de publier la réponse"
|
msgstr "Impossible de publier la réponse"
|
||||||
|
|
||||||
#: src/pages/PlaylistDetail.tsx:896
|
#: src/pages/PlaylistDetail.tsx:899
|
||||||
#: src/pages/UserPublicProfile.tsx:1640
|
#: src/pages/UserPublicProfile.tsx:1640
|
||||||
#: src/pages/UserPublicProfile.tsx:1709
|
#: src/pages/UserPublicProfile.tsx:1709
|
||||||
msgid "Failed to save"
|
msgid "Failed to save"
|
||||||
@@ -595,7 +595,7 @@ msgstr "Chargement de la reco…"
|
|||||||
msgid "Loading more…"
|
msgid "Loading more…"
|
||||||
msgstr "Chargement…"
|
msgstr "Chargement…"
|
||||||
|
|
||||||
#: src/pages/PlaylistDetail.tsx:544
|
#: src/pages/PlaylistDetail.tsx:545
|
||||||
msgid "Loading playlist…"
|
msgid "Loading playlist…"
|
||||||
msgstr "Chargement de la collection…"
|
msgstr "Chargement de la collection…"
|
||||||
|
|
||||||
@@ -688,7 +688,7 @@ msgstr "Nouveau mot de passe"
|
|||||||
msgid "New playlist"
|
msgid "New playlist"
|
||||||
msgstr "Nouvelle collection"
|
msgstr "Nouvelle collection"
|
||||||
|
|
||||||
#: src/pages/PlaylistDetail.tsx:668
|
#: src/pages/PlaylistDetail.tsx:671
|
||||||
msgid "No dumps in this playlist yet."
|
msgid "No dumps in this playlist yet."
|
||||||
msgstr "Aucune reco dans cette collection pour l'instant."
|
msgstr "Aucune reco dans cette collection pour l'instant."
|
||||||
|
|
||||||
@@ -785,7 +785,7 @@ msgstr "Mot de passe mis à jour"
|
|||||||
msgid "Passwords do not match"
|
msgid "Passwords do not match"
|
||||||
msgstr "Les mots de passe ne correspondent pas"
|
msgstr "Les mots de passe ne correspondent pas"
|
||||||
|
|
||||||
#: src/pages/PlaylistDetail.tsx:851
|
#: src/pages/PlaylistDetail.tsx:854
|
||||||
msgid "Playlist title"
|
msgid "Playlist title"
|
||||||
msgstr "Titre de la collection"
|
msgstr "Titre de la collection"
|
||||||
|
|
||||||
@@ -825,22 +825,22 @@ msgstr "Publication…"
|
|||||||
#: src/components/PlaylistCard.tsx:73
|
#: src/components/PlaylistCard.tsx:73
|
||||||
#: src/components/PlaylistMembershipPanel.tsx:55
|
#: src/components/PlaylistMembershipPanel.tsx:55
|
||||||
#: src/pages/Dump.tsx:418
|
#: src/pages/Dump.tsx:418
|
||||||
#: src/pages/PlaylistDetail.tsx:632
|
#: src/pages/PlaylistDetail.tsx:635
|
||||||
msgid "private"
|
msgid "private"
|
||||||
msgstr "privé"
|
msgstr "privé"
|
||||||
|
|
||||||
#: src/components/form/SegmentedField.tsx:78
|
#: src/components/form/SegmentedField.tsx:78
|
||||||
#: src/pages/PlaylistDetail.tsx:890
|
#: src/pages/PlaylistDetail.tsx:893
|
||||||
msgid "Private"
|
msgid "Private"
|
||||||
msgstr "Privé"
|
msgstr "Privé"
|
||||||
|
|
||||||
#: src/components/PlaylistCard.tsx:72
|
#: src/components/PlaylistCard.tsx:72
|
||||||
#: src/pages/PlaylistDetail.tsx:631
|
#: src/pages/PlaylistDetail.tsx:634
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr "public"
|
msgstr "public"
|
||||||
|
|
||||||
#: src/components/form/SegmentedField.tsx:77
|
#: src/components/form/SegmentedField.tsx:77
|
||||||
#: src/pages/PlaylistDetail.tsx:883
|
#: src/pages/PlaylistDetail.tsx:886
|
||||||
msgid "Public"
|
msgid "Public"
|
||||||
msgstr "Public"
|
msgstr "Public"
|
||||||
|
|
||||||
@@ -873,7 +873,7 @@ msgstr "Connexe"
|
|||||||
msgid "Remove file"
|
msgid "Remove file"
|
||||||
msgstr "Supprimer le fichier"
|
msgstr "Supprimer le fichier"
|
||||||
|
|
||||||
#: src/pages/PlaylistDetail.tsx:723
|
#: src/pages/PlaylistDetail.tsx:726
|
||||||
msgid "Remove from playlist"
|
msgid "Remove from playlist"
|
||||||
msgstr "Retirer de la collection"
|
msgstr "Retirer de la collection"
|
||||||
|
|
||||||
@@ -918,7 +918,7 @@ msgstr "Rôle"
|
|||||||
#: src/components/CommentThread.tsx:328
|
#: src/components/CommentThread.tsx:328
|
||||||
#: src/pages/Dump.tsx:351
|
#: src/pages/Dump.tsx:351
|
||||||
#: src/pages/DumpEdit.tsx:436
|
#: src/pages/DumpEdit.tsx:436
|
||||||
#: src/pages/PlaylistDetail.tsx:915
|
#: src/pages/PlaylistDetail.tsx:918
|
||||||
#: src/pages/UserPublicProfile.tsx:1629
|
#: src/pages/UserPublicProfile.tsx:1629
|
||||||
#: src/pages/UserPublicProfile.tsx:1699
|
#: src/pages/UserPublicProfile.tsx:1699
|
||||||
msgid "Save"
|
msgid "Save"
|
||||||
@@ -927,7 +927,7 @@ msgstr "Enregistrer"
|
|||||||
#: src/components/ChangePasswordModal.tsx:100
|
#: src/components/ChangePasswordModal.tsx:100
|
||||||
#: src/components/CommentThread.tsx:329
|
#: src/components/CommentThread.tsx:329
|
||||||
#: src/pages/Dump.tsx:350
|
#: src/pages/Dump.tsx:350
|
||||||
#: src/pages/PlaylistDetail.tsx:911
|
#: src/pages/PlaylistDetail.tsx:914
|
||||||
#: src/pages/ResetPassword.tsx:124
|
#: src/pages/ResetPassword.tsx:124
|
||||||
#: src/pages/UserPublicProfile.tsx:1626
|
#: src/pages/UserPublicProfile.tsx:1626
|
||||||
#: src/pages/UserPublicProfile.tsx:1696
|
#: src/pages/UserPublicProfile.tsx:1696
|
||||||
@@ -1019,7 +1019,7 @@ msgstr "Un titre est requis."
|
|||||||
msgid "Today"
|
msgid "Today"
|
||||||
msgstr "Aujourd'hui"
|
msgstr "Aujourd'hui"
|
||||||
|
|
||||||
#: src/pages/PlaylistDetail.tsx:735
|
#: src/pages/PlaylistDetail.tsx:738
|
||||||
msgid "Undo"
|
msgid "Undo"
|
||||||
msgstr "Annuler"
|
msgstr "Annuler"
|
||||||
|
|
||||||
@@ -1068,7 +1068,7 @@ msgstr "Utilisateur"
|
|||||||
|
|
||||||
#: src/pages/UserPublicProfile.tsx:1246
|
#: src/pages/UserPublicProfile.tsx:1246
|
||||||
msgid "User management"
|
msgid "User management"
|
||||||
msgstr "Gestion des utilisateurs"
|
msgstr "Gestion utilisateur"
|
||||||
|
|
||||||
#: src/components/UserMenu.tsx:37
|
#: src/components/UserMenu.tsx:37
|
||||||
msgid "User menu"
|
msgid "User menu"
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
deserializePlaylistWithDumps,
|
deserializePlaylistWithDumps,
|
||||||
} from "../model.ts";
|
} from "../model.ts";
|
||||||
import { playlistUrl } from "../utils/urls.ts";
|
import { playlistUrl } from "../utils/urls.ts";
|
||||||
|
import { can } from "../utils/permissions.ts";
|
||||||
import { useAuth } from "../hooks/useAuth.ts";
|
import { useAuth } from "../hooks/useAuth.ts";
|
||||||
import { useWS } from "../hooks/useWS.ts";
|
import { useWS } from "../hooks/useWS.ts";
|
||||||
import { relativeTime } from "../utils/relativeTime.ts";
|
import { relativeTime } from "../utils/relativeTime.ts";
|
||||||
@@ -566,6 +567,8 @@ export function PlaylistDetail() {
|
|||||||
|
|
||||||
const { playlist } = state;
|
const { playlist } = state;
|
||||||
const isOwner = !!user && user.id === playlist.userId;
|
const isOwner = !!user && user.id === playlist.userId;
|
||||||
|
// Owners edit their own playlists; moderators may edit any (playlist:moderate).
|
||||||
|
const canEdit = isOwner || can(user, "playlist:moderate");
|
||||||
|
|
||||||
// Active dumps in playlist order; fading dumps appended so they stay visible
|
// Active dumps in playlist order; fading dumps appended so they stay visible
|
||||||
const activeDumps = playlist.dumps.filter((d) => activeDumpIds.has(d.id));
|
const activeDumps = playlist.dumps.filter((d) => activeDumpIds.has(d.id));
|
||||||
@@ -604,7 +607,7 @@ export function PlaylistDetail() {
|
|||||||
isPublic={playlist.isPublic}
|
isPublic={playlist.isPublic}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isOwner && (
|
{canEdit && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="playlist-edit-btn"
|
className="playlist-edit-btn"
|
||||||
@@ -671,7 +674,7 @@ export function PlaylistDetail() {
|
|||||||
: (
|
: (
|
||||||
<div
|
<div
|
||||||
className="playlist-dump-list"
|
className="playlist-dump-list"
|
||||||
onDragOver={isOwner ? (e) => e.preventDefault() : undefined}
|
onDragOver={canEdit ? (e) => e.preventDefault() : undefined}
|
||||||
>
|
>
|
||||||
{visibleDumps.map((dump) => {
|
{visibleDumps.map((dump) => {
|
||||||
const isActive = activeDumpIds.has(dump.id);
|
const isActive = activeDumpIds.has(dump.id);
|
||||||
@@ -692,16 +695,16 @@ export function PlaylistDetail() {
|
|||||||
? " playlist-dump-item--drag-over"
|
? " playlist-dump-item--drag-over"
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
draggable={isOwner && isActive}
|
draggable={canEdit && isActive}
|
||||||
onDragStart={isOwner && isActive
|
onDragStart={canEdit && isActive
|
||||||
? () => handleDragStart(activeIndex)
|
? () => handleDragStart(activeIndex)
|
||||||
: undefined}
|
: undefined}
|
||||||
onDragOver={isOwner && isActive
|
onDragOver={canEdit && isActive
|
||||||
? (e) => handleDragOver(e, activeIndex)
|
? (e) => handleDragOver(e, activeIndex)
|
||||||
: undefined}
|
: undefined}
|
||||||
onDragEnd={isOwner ? handleDragEnd : undefined}
|
onDragEnd={canEdit ? handleDragEnd : undefined}
|
||||||
>
|
>
|
||||||
{isOwner && isActive && (
|
{canEdit && isActive && (
|
||||||
<span className="drag-handle" aria-hidden>⠿</span>
|
<span className="drag-handle" aria-hidden>⠿</span>
|
||||||
)}
|
)}
|
||||||
<DumpCard
|
<DumpCard
|
||||||
@@ -714,7 +717,7 @@ export function PlaylistDetail() {
|
|||||||
className={cardCls}
|
className={cardCls}
|
||||||
isOwner={!!user && user.id === dump.userId}
|
isOwner={!!user && user.id === dump.userId}
|
||||||
/>
|
/>
|
||||||
{isOwner && (isActive
|
{canEdit && (isActive
|
||||||
? (
|
? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
import type { PublicUser, Role } from "../model.ts";
|
import type { PublicUser, Role } from "../model.ts";
|
||||||
|
|
||||||
// Mirror of api/lib/permissions.ts. Keep the two in sync.
|
// Mirror of api/lib/permissions.ts. Keep the two in sync.
|
||||||
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[] = [
|
const ALL_PERMISSIONS: readonly Permission[] = [
|
||||||
"dump:moderate",
|
"dump:moderate",
|
||||||
"comment:moderate",
|
"comment:moderate",
|
||||||
|
"playlist:moderate",
|
||||||
"user:manage",
|
"user:manage",
|
||||||
];
|
];
|
||||||
|
|
||||||
const ROLE_PERMISSIONS: Record<Role, readonly Permission[]> = {
|
const ROLE_PERMISSIONS: Record<Role, readonly Permission[]> = {
|
||||||
admin: ALL_PERMISSIONS,
|
admin: ALL_PERMISSIONS,
|
||||||
moderator: ["dump:moderate", "comment:moderate"],
|
moderator: ["dump:moderate", "comment:moderate", "playlist:moderate"],
|
||||||
user: [],
|
user: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user