admin can delete status changes from post timeline

This commit is contained in:
2026-03-22 07:41:59 +02:00
parent 6b110a6d90
commit 393001c07c
3 changed files with 47 additions and 0 deletions

View File

@@ -509,6 +509,26 @@ export default async function adminPostRoutes(app: FastifyInstance) {
} }
); );
// delete a status change entry
app.delete<{ Params: { id: string } }>(
"/admin/status-changes/:id",
{ preHandler: [app.requireAdmin], config: { rateLimit: { max: 20, timeWindow: "1 minute" } } },
async (req, reply) => {
const sc = await prisma.statusChange.findUnique({ where: { id: req.params.id }, select: { id: true, postId: true, toStatus: true } });
if (!sc) {
reply.status(404).send({ error: "Status change not found" });
return;
}
// delete related notifications
await prisma.notification.deleteMany({
where: { postId: sc.postId, type: "status_changed" },
});
await prisma.statusChange.delete({ where: { id: sc.id } });
req.log.info({ adminId: req.adminId, statusChangeId: sc.id }, "status change deleted");
reply.status(204).send();
}
);
app.post<{ Params: { id: string }; Body: z.infer<typeof rollbackBody> }>( app.post<{ Params: { id: string }; Body: z.infer<typeof rollbackBody> }>(
"/admin/posts/:id/rollback", "/admin/posts/:id/rollback",
{ preHandler: [app.requireAdmin], config: { rateLimit: { max: 10, timeWindow: "1 minute" } } }, { preHandler: [app.requireAdmin], config: { rateLimit: { max: 10, timeWindow: "1 minute" } } },

View File

@@ -51,6 +51,7 @@ export default function Timeline({
onReply, onReply,
onShowEditHistory, onShowEditHistory,
onLockComment, onLockComment,
onDeleteStatusChange,
}: { }: {
entries: TimelineEntry[] entries: TimelineEntry[]
onReact?: (entryId: string, emoji: string) => void onReact?: (entryId: string, emoji: string) => void
@@ -61,6 +62,7 @@ export default function Timeline({
onReply?: (entry: TimelineEntry) => void onReply?: (entry: TimelineEntry) => void
onShowEditHistory?: (entryId: string) => void onShowEditHistory?: (entryId: string) => void
onLockComment?: (entryId: string) => void onLockComment?: (entryId: string) => void
onDeleteStatusChange?: (entryId: string) => void
}) { }) {
return ( return (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
@@ -76,6 +78,7 @@ export default function Timeline({
onReply={onReply} onReply={onReply}
onShowEditHistory={onShowEditHistory} onShowEditHistory={onShowEditHistory}
onLockComment={onLockComment} onLockComment={onLockComment}
onDeleteStatusChange={onDeleteStatusChange}
index={i} index={i}
/> />
))} ))}
@@ -93,6 +96,7 @@ function TimelineItem({
onReply, onReply,
onShowEditHistory, onShowEditHistory,
onLockComment, onLockComment,
onDeleteStatusChange,
index, index,
}: { }: {
entry: TimelineEntry entry: TimelineEntry
@@ -104,6 +108,7 @@ function TimelineItem({
onReply?: (entry: TimelineEntry) => void onReply?: (entry: TimelineEntry) => void
onShowEditHistory?: (entryId: string) => void onShowEditHistory?: (entryId: string) => void
onLockComment?: (entryId: string) => void onLockComment?: (entryId: string) => void
onDeleteStatusChange?: (entryId: string) => void
index: number index: number
}) { }) {
const confirm = useConfirm() const confirm = useConfirm()
@@ -138,6 +143,19 @@ function TimelineItem({
<time dateTime={entry.createdAt} style={{ color: 'var(--text-tertiary)', fontSize: 'var(--text-xs)' }}> <time dateTime={entry.createdAt} style={{ color: 'var(--text-tertiary)', fontSize: 'var(--text-xs)' }}>
{new Date(entry.createdAt).toLocaleDateString()} {new Date(entry.createdAt).toLocaleDateString()}
</time> </time>
{isCurrentAdmin && onDeleteStatusChange && (
<button
onClick={async () => {
if (!await confirm('Remove this status change?')) return
onDeleteStatusChange(entry.id)
}}
className="action-btn"
aria-label="Remove status change"
style={{ color: 'var(--text-tertiary)', padding: '2px 4px', borderRadius: 'var(--radius-sm)', minHeight: 24 }}
>
<IconTrash size={12} stroke={2} />
</button>
)}
</div> </div>
{entry.reason && ( {entry.reason && (
<div <div

View File

@@ -931,6 +931,15 @@ export default function PostDetail() {
onReply={post.isThreadLocked ? undefined : handleReply} onReply={post.isThreadLocked ? undefined : handleReply}
onShowEditHistory={showCommentHistory} onShowEditHistory={showCommentHistory}
onLockComment={admin.isAdmin ? toggleCommentEditLock : undefined} onLockComment={admin.isAdmin ? toggleCommentEditLock : undefined}
onDeleteStatusChange={admin.isAdmin ? async (id) => {
try {
await api.delete(`/admin/status-changes/${id}`)
toast.success('Status change removed')
fetchPost()
} catch {
toast.error('Failed to remove status change')
}
} : undefined}
/> />
</div> </div>
)} )}