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

@@ -42,7 +42,7 @@ msgstr "{label} ({count})"
msgid "{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
msgid "← 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/pages/Dump.tsx:359
#: src/pages/DumpEdit.tsx:433
#: src/pages/PlaylistDetail.tsx:908
#: src/pages/PlaylistDetail.tsx:911
#: src/pages/UserPublicProfile.tsx:1637
#: src/pages/UserPublicProfile.tsx:1707
msgid "Cancel"
msgstr "Cancel"
#: src/pages/PlaylistDetail.tsx:733
#: src/pages/PlaylistDetail.tsx:736
msgid "Cancel removal"
msgstr "Cancel removal"
@@ -278,7 +278,7 @@ msgstr "Default tab"
#: src/components/CommentThread.tsx:376
#: src/components/CommentThread.tsx:382
#: src/components/ConfirmModal.tsx:16
#: src/pages/PlaylistDetail.tsx:904
#: src/pages/PlaylistDetail.tsx:907
msgid "Delete"
msgstr "Delete"
@@ -288,7 +288,7 @@ msgid "Delete dump"
msgstr "Delete dump"
#: src/components/PlaylistCard.tsx:109
#: src/pages/PlaylistDetail.tsx:746
#: src/pages/PlaylistDetail.tsx:749
#: src/pages/UserPlaylists.tsx:465
msgid "Delete playlist"
msgstr "Delete playlist"
@@ -301,13 +301,13 @@ msgstr "Delete this comment?"
msgid "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
msgid "Delete this playlist? This cannot be undone."
msgstr "Delete this playlist? This cannot be undone."
#: src/components/PlaylistCreateForm.tsx:78
#: src/pages/PlaylistDetail.tsx:865
#: src/pages/PlaylistDetail.tsx:868
msgid "Description (optional)"
msgstr "Description (optional)"
@@ -353,7 +353,7 @@ msgstr "Earlier"
#: src/components/CommentThread.tsx:367
#: src/pages/Dump.tsx:459
#: src/pages/PlaylistDetail.tsx:613
#: src/pages/PlaylistDetail.tsx:616
msgid "Edit"
msgstr "Edit"
@@ -366,7 +366,7 @@ msgstr "Edit title"
#. placeholder {0}: relativeTime(playlist.updatedAt)
#: src/components/CommentThread.tsx:317
#: src/pages/Dump.tsx:412
#: src/pages/PlaylistDetail.tsx:652
#: src/pages/PlaylistDetail.tsx:655
msgid "edited {0}"
msgstr "edited {0}"
@@ -375,7 +375,7 @@ msgstr "edited {0}"
#. placeholder {0}: playlist.updatedAt.toLocaleString()
#: src/components/CommentThread.tsx:315
#: src/pages/Dump.tsx:410
#: src/pages/PlaylistDetail.tsx:649
#: src/pages/PlaylistDetail.tsx:652
msgid "Edited {0}"
msgstr "Edited {0}"
@@ -424,7 +424,7 @@ msgstr "Failed to post comment"
msgid "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:1709
msgid "Failed to save"
@@ -595,7 +595,7 @@ msgstr "Loading dump…"
msgid "Loading more…"
msgstr "Loading more…"
#: src/pages/PlaylistDetail.tsx:544
#: src/pages/PlaylistDetail.tsx:545
msgid "Loading playlist…"
msgstr "Loading playlist…"
@@ -688,7 +688,7 @@ msgstr "New password"
msgid "New playlist"
msgstr "New playlist"
#: src/pages/PlaylistDetail.tsx:668
#: src/pages/PlaylistDetail.tsx:671
msgid "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"
msgstr "Passwords do not match"
#: src/pages/PlaylistDetail.tsx:851
#: src/pages/PlaylistDetail.tsx:854
msgid "Playlist title"
msgstr "Playlist title"
@@ -825,22 +825,22 @@ msgstr "Posting…"
#: src/components/PlaylistCard.tsx:73
#: src/components/PlaylistMembershipPanel.tsx:55
#: src/pages/Dump.tsx:418
#: src/pages/PlaylistDetail.tsx:632
#: src/pages/PlaylistDetail.tsx:635
msgid "private"
msgstr "private"
#: src/components/form/SegmentedField.tsx:78
#: src/pages/PlaylistDetail.tsx:890
#: src/pages/PlaylistDetail.tsx:893
msgid "Private"
msgstr "Private"
#: src/components/PlaylistCard.tsx:72
#: src/pages/PlaylistDetail.tsx:631
#: src/pages/PlaylistDetail.tsx:634
msgid "public"
msgstr "public"
#: src/components/form/SegmentedField.tsx:77
#: src/pages/PlaylistDetail.tsx:883
#: src/pages/PlaylistDetail.tsx:886
msgid "Public"
msgstr "Public"
@@ -873,7 +873,7 @@ msgstr "Related"
msgid "Remove file"
msgstr "Remove file"
#: src/pages/PlaylistDetail.tsx:723
#: src/pages/PlaylistDetail.tsx:726
msgid "Remove from playlist"
msgstr "Remove from playlist"
@@ -918,7 +918,7 @@ msgstr "Role"
#: src/components/CommentThread.tsx:328
#: src/pages/Dump.tsx:351
#: src/pages/DumpEdit.tsx:436
#: src/pages/PlaylistDetail.tsx:915
#: src/pages/PlaylistDetail.tsx:918
#: src/pages/UserPublicProfile.tsx:1629
#: src/pages/UserPublicProfile.tsx:1699
msgid "Save"
@@ -927,7 +927,7 @@ msgstr "Save"
#: src/components/ChangePasswordModal.tsx:100
#: src/components/CommentThread.tsx:329
#: src/pages/Dump.tsx:350
#: src/pages/PlaylistDetail.tsx:911
#: src/pages/PlaylistDetail.tsx:914
#: src/pages/ResetPassword.tsx:124
#: src/pages/UserPublicProfile.tsx:1626
#: src/pages/UserPublicProfile.tsx:1696
@@ -1019,7 +1019,7 @@ msgstr "Title is required."
msgid "Today"
msgstr "Today"
#: src/pages/PlaylistDetail.tsx:735
#: src/pages/PlaylistDetail.tsx:738
msgid "Undo"
msgstr "Undo"

File diff suppressed because one or more lines are too long

View File

@@ -42,7 +42,7 @@ msgstr "{label} ({count})"
msgid "{visibleCount, plural, one {# comment} other {# comments}}"
msgstr "{visibleCount, plural, one {# commentaire} other {# commentaires}}"
#: src/pages/PlaylistDetail.tsx:560
#: src/pages/PlaylistDetail.tsx:561
#: src/pages/UserPublicProfile.tsx:732
msgid "← Back"
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/pages/Dump.tsx:359
#: src/pages/DumpEdit.tsx:433
#: src/pages/PlaylistDetail.tsx:908
#: src/pages/PlaylistDetail.tsx:911
#: src/pages/UserPublicProfile.tsx:1637
#: src/pages/UserPublicProfile.tsx:1707
msgid "Cancel"
msgstr "Annuler"
#: src/pages/PlaylistDetail.tsx:733
#: src/pages/PlaylistDetail.tsx:736
msgid "Cancel removal"
msgstr "Annuler la suppression"
@@ -278,7 +278,7 @@ msgstr "Onglet par défaut"
#: src/components/CommentThread.tsx:376
#: src/components/CommentThread.tsx:382
#: src/components/ConfirmModal.tsx:16
#: src/pages/PlaylistDetail.tsx:904
#: src/pages/PlaylistDetail.tsx:907
msgid "Delete"
msgstr "Supprimer"
@@ -288,7 +288,7 @@ msgid "Delete dump"
msgstr "Supprimer la reco"
#: src/components/PlaylistCard.tsx:109
#: src/pages/PlaylistDetail.tsx:746
#: src/pages/PlaylistDetail.tsx:749
#: src/pages/UserPlaylists.tsx:465
msgid "Delete playlist"
msgstr "Supprimer la collection"
@@ -301,13 +301,13 @@ msgstr "Supprimer ce commentaire ?"
msgid "Delete this dump? This cannot be undone."
msgstr "Supprimer cette reco ? Cette action est irréversible."
#: src/pages/PlaylistDetail.tsx:745
#: src/pages/PlaylistDetail.tsx:748
#: src/pages/UserPlaylists.tsx:464
msgid "Delete this playlist? This cannot be undone."
msgstr "Supprimer cette collection ? Cette action est irréversible."
#: src/components/PlaylistCreateForm.tsx:78
#: src/pages/PlaylistDetail.tsx:865
#: src/pages/PlaylistDetail.tsx:868
msgid "Description (optional)"
msgstr "Description (facultatif)"
@@ -353,7 +353,7 @@ msgstr "Plus tôt"
#: src/components/CommentThread.tsx:367
#: src/pages/Dump.tsx:459
#: src/pages/PlaylistDetail.tsx:613
#: src/pages/PlaylistDetail.tsx:616
msgid "Edit"
msgstr "Modifier"
@@ -366,7 +366,7 @@ msgstr "Modifier le titre"
#. placeholder {0}: relativeTime(playlist.updatedAt)
#: src/components/CommentThread.tsx:317
#: src/pages/Dump.tsx:412
#: src/pages/PlaylistDetail.tsx:652
#: src/pages/PlaylistDetail.tsx:655
msgid "edited {0}"
msgstr "modifié {0}"
@@ -375,7 +375,7 @@ msgstr "modifié {0}"
#. placeholder {0}: playlist.updatedAt.toLocaleString()
#: src/components/CommentThread.tsx:315
#: src/pages/Dump.tsx:410
#: src/pages/PlaylistDetail.tsx:649
#: src/pages/PlaylistDetail.tsx:652
msgid "Edited {0}"
msgstr "Modifié le {0}"
@@ -424,7 +424,7 @@ msgstr "Impossible de publier le commentaire"
msgid "Failed to post reply"
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:1709
msgid "Failed to save"
@@ -595,7 +595,7 @@ msgstr "Chargement de la reco…"
msgid "Loading more…"
msgstr "Chargement…"
#: src/pages/PlaylistDetail.tsx:544
#: src/pages/PlaylistDetail.tsx:545
msgid "Loading playlist…"
msgstr "Chargement de la collection…"
@@ -688,7 +688,7 @@ msgstr "Nouveau mot de passe"
msgid "New playlist"
msgstr "Nouvelle collection"
#: src/pages/PlaylistDetail.tsx:668
#: src/pages/PlaylistDetail.tsx:671
msgid "No dumps in this playlist yet."
msgstr "Aucune reco dans cette collection pour l'instant."
@@ -785,7 +785,7 @@ msgstr "Mot de passe mis à jour"
msgid "Passwords do not match"
msgstr "Les mots de passe ne correspondent pas"
#: src/pages/PlaylistDetail.tsx:851
#: src/pages/PlaylistDetail.tsx:854
msgid "Playlist title"
msgstr "Titre de la collection"
@@ -825,22 +825,22 @@ msgstr "Publication…"
#: src/components/PlaylistCard.tsx:73
#: src/components/PlaylistMembershipPanel.tsx:55
#: src/pages/Dump.tsx:418
#: src/pages/PlaylistDetail.tsx:632
#: src/pages/PlaylistDetail.tsx:635
msgid "private"
msgstr "privé"
#: src/components/form/SegmentedField.tsx:78
#: src/pages/PlaylistDetail.tsx:890
#: src/pages/PlaylistDetail.tsx:893
msgid "Private"
msgstr "Privé"
#: src/components/PlaylistCard.tsx:72
#: src/pages/PlaylistDetail.tsx:631
#: src/pages/PlaylistDetail.tsx:634
msgid "public"
msgstr "public"
#: src/components/form/SegmentedField.tsx:77
#: src/pages/PlaylistDetail.tsx:883
#: src/pages/PlaylistDetail.tsx:886
msgid "Public"
msgstr "Public"
@@ -873,7 +873,7 @@ msgstr "Connexe"
msgid "Remove file"
msgstr "Supprimer le fichier"
#: src/pages/PlaylistDetail.tsx:723
#: src/pages/PlaylistDetail.tsx:726
msgid "Remove from playlist"
msgstr "Retirer de la collection"
@@ -918,7 +918,7 @@ msgstr "Rôle"
#: src/components/CommentThread.tsx:328
#: src/pages/Dump.tsx:351
#: src/pages/DumpEdit.tsx:436
#: src/pages/PlaylistDetail.tsx:915
#: src/pages/PlaylistDetail.tsx:918
#: src/pages/UserPublicProfile.tsx:1629
#: src/pages/UserPublicProfile.tsx:1699
msgid "Save"
@@ -927,7 +927,7 @@ msgstr "Enregistrer"
#: src/components/ChangePasswordModal.tsx:100
#: src/components/CommentThread.tsx:329
#: src/pages/Dump.tsx:350
#: src/pages/PlaylistDetail.tsx:911
#: src/pages/PlaylistDetail.tsx:914
#: src/pages/ResetPassword.tsx:124
#: src/pages/UserPublicProfile.tsx:1626
#: src/pages/UserPublicProfile.tsx:1696
@@ -1019,7 +1019,7 @@ msgstr "Un titre est requis."
msgid "Today"
msgstr "Aujourd'hui"
#: src/pages/PlaylistDetail.tsx:735
#: src/pages/PlaylistDetail.tsx:738
msgid "Undo"
msgstr "Annuler"
@@ -1068,7 +1068,7 @@ msgstr "Utilisateur"
#: src/pages/UserPublicProfile.tsx:1246
msgid "User management"
msgstr "Gestion des utilisateurs"
msgstr "Gestion utilisateur"
#: src/components/UserMenu.tsx:37
msgid "User menu"

View File

@@ -25,6 +25,7 @@ import {
deserializePlaylistWithDumps,
} from "../model.ts";
import { playlistUrl } from "../utils/urls.ts";
import { can } from "../utils/permissions.ts";
import { useAuth } from "../hooks/useAuth.ts";
import { useWS } from "../hooks/useWS.ts";
import { relativeTime } from "../utils/relativeTime.ts";
@@ -566,6 +567,8 @@ export function PlaylistDetail() {
const { playlist } = state;
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
const activeDumps = playlist.dumps.filter((d) => activeDumpIds.has(d.id));
@@ -604,7 +607,7 @@ export function PlaylistDetail() {
isPublic={playlist.isPublic}
/>
)}
{isOwner && (
{canEdit && (
<button
type="button"
className="playlist-edit-btn"
@@ -671,7 +674,7 @@ export function PlaylistDetail() {
: (
<div
className="playlist-dump-list"
onDragOver={isOwner ? (e) => e.preventDefault() : undefined}
onDragOver={canEdit ? (e) => e.preventDefault() : undefined}
>
{visibleDumps.map((dump) => {
const isActive = activeDumpIds.has(dump.id);
@@ -692,16 +695,16 @@ export function PlaylistDetail() {
? " playlist-dump-item--drag-over"
: ""
}`}
draggable={isOwner && isActive}
onDragStart={isOwner && isActive
draggable={canEdit && isActive}
onDragStart={canEdit && isActive
? () => handleDragStart(activeIndex)
: undefined}
onDragOver={isOwner && isActive
onDragOver={canEdit && isActive
? (e) => handleDragOver(e, activeIndex)
: undefined}
onDragEnd={isOwner ? handleDragEnd : undefined}
onDragEnd={canEdit ? handleDragEnd : undefined}
>
{isOwner && isActive && (
{canEdit && isActive && (
<span className="drag-handle" aria-hidden></span>
)}
<DumpCard
@@ -714,7 +717,7 @@ export function PlaylistDetail() {
className={cardCls}
isOwner={!!user && user.id === dump.userId}
/>
{isOwner && (isActive
{canEdit && (isActive
? (
<button
type="button"

View File

@@ -1,17 +1,22 @@
import type { PublicUser, Role } from "../model.ts";
// 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[] = [
"dump:moderate",
"comment:moderate",
"playlist:moderate",
"user:manage",
];
const ROLE_PERMISSIONS: Record<Role, readonly Permission[]> = {
admin: ALL_PERMISSIONS,
moderator: ["dump:moderate", "comment:moderate"],
moderator: ["dump:moderate", "comment:moderate", "playlist:moderate"],
user: [],
};