From f27ea71d269086ad11f038431688d93ebdc8c322 Mon Sep 17 00:00:00 2001 From: canghai118 <3394863+canghai118@users.noreply.github.com> Date: Thu, 21 May 2026 17:35:23 +0800 Subject: [PATCH 1/3] fix(post): suppress initial focus ring in PostDetailModal Prevent Radix Dialog auto-focus so opening via a non-focusable trigger (e.g. an
card) doesn't paint a focus-visible ring on the first tabbable element. --- app/components/post/PostDetailModal.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/app/components/post/PostDetailModal.vue b/app/components/post/PostDetailModal.vue index ef92300..9473303 100644 --- a/app/components/post/PostDetailModal.vue +++ b/app/components/post/PostDetailModal.vue @@ -30,6 +30,7 @@ watch(open, (isOpen) => { Date: Thu, 21 May 2026 18:46:27 +0800 Subject: [PATCH 2/3] fix(post): restrict post deletion to moderators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the author self-delete bypass in the admin delete handler so the endpoint matches the rest of /api/admin/posts/* (moderator-only). Authors keep their edit path via PATCH /api/posts/:id; deletion is a community- level action because shipped posts carry others' upvotes, comments, and status changes. Split canDelete in usePermission by resource type — posts moderator-only, comments still author-or-moderator (conversational). --- app/composables/usePermission.ts | 11 ++++++++--- server/api/admin/posts/[id].delete.ts | 14 +++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/app/composables/usePermission.ts b/app/composables/usePermission.ts index 2716127..0ac269e 100644 --- a/app/composables/usePermission.ts +++ b/app/composables/usePermission.ts @@ -7,8 +7,7 @@ type ResourceType = 'post' | 'comment' // - moderator → `requireOrgPermission({ feedlog: ['moderate'] })` // // Ownership is a row-level check: `authorId === session.user.id`. Edit/delete -// is allowed when the caller is the author OR a moderator. Comments follow -// the same rule. +// rules differ between resource types — see canEdit / canDelete below. export function usePermission(authorId: Ref | ComputedRef, type: ResourceType) { const { data: session } = useAuthSession() const ctx = useOrgContext() @@ -38,7 +37,13 @@ export function usePermission(authorId: Ref | ComputedRef isOwner.value || canModerate.value) + const canDelete = computed(() => { + // Posts: moderator-only. Once shipped, a post belongs to the community + // (others' upvotes / comments / status). Authors can edit but not delete. + if (type === 'post') return canModerate.value + // Comments are conversational — author can delete their own. + return isOwner.value || canModerate.value + }) const showMenu = computed(() => canEdit.value || canDelete.value) diff --git a/server/api/admin/posts/[id].delete.ts b/server/api/admin/posts/[id].delete.ts index 84fa73a..2380d93 100644 --- a/server/api/admin/posts/[id].delete.ts +++ b/server/api/admin/posts/[id].delete.ts @@ -1,16 +1,16 @@ import { and, eq, sql } from 'drizzle-orm' import { post, comment, commentLike, vote } from '#layers/feedlog/server/db/schemas' -// DELETE /api/admin/posts/:id — Hard delete. Author can delete own; -// moderators can delete anyone's (escalates via feedlog:moderate). The -// "admin" path is here for the cascade clean-up logic, not for the gate. +// DELETE /api/admin/posts/:id — Moderator-only hard delete. Once a post is +// public it belongs to the community (others' upvotes/comments/status); the +// author has no self-delete path, only edit via PATCH /api/posts/:id. export default defineEventHandler(async (event) => { - const { session, orgId } = await requireOrgMember(event) + const { orgId } = await requireOrgPermission(event, { feedlog: ['moderate'] }) const id = getRouterParam(event, 'id')! const db = useDB() const [existing] = await db - .select({ authorId: post.authorId, mergedTo: post.mergedTo }) + .select({ mergedTo: post.mergedTo }) .from(post) .where(and(eq(post.id, id), eq(post.orgId, orgId))) .limit(1) @@ -21,10 +21,6 @@ export default defineEventHandler(async (event) => { throw createError({ statusCode: 400, message: 'Cannot delete a merged post. Unmerge it first.' }) } - if (existing.authorId !== session.user.id) { - await requireOrgPermission(event, { feedlog: ['moderate'] }) - } - // Delete associated data (no DB foreign keys, cascade at application level) // 1. Delete all comment likes for this post await db.delete(commentLike).where( From 4e477fa7534ff47e85832f6eb10880fd05edb266 Mon Sep 17 00:00:00 2001 From: canghai118 <3394863+canghai118@users.noreply.github.com> Date: Fri, 22 May 2026 18:11:07 +0800 Subject: [PATCH 3/3] fix(ui): unify Delete dropdown hover style with Edit Custom --accent is salmon, so focus:bg-accent + focus:text-destructive produced red-on-orange. Drop focus:text-destructive and let the default focus:text-accent-foreground (white) take over on hover. --- app/components/comment/CommentItem.vue | 2 +- app/components/post/PostDetail.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/comment/CommentItem.vue b/app/components/comment/CommentItem.vue index 3e4a95d..92f4e15 100644 --- a/app/components/comment/CommentItem.vue +++ b/app/components/comment/CommentItem.vue @@ -135,7 +135,7 @@ function initials(name: string | null) { Edit - + Delete diff --git a/app/components/post/PostDetail.vue b/app/components/post/PostDetail.vue index d9f4fdb..03c7da7 100644 --- a/app/components/post/PostDetail.vue +++ b/app/components/post/PostDetail.vue @@ -381,7 +381,7 @@ async function handleShare() { Edit - Delete + Delete