Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/components/comment/CommentItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ function initials(name: string | null) {
<Icon name="lucide:pencil" size="14" />
Edit
</DropdownMenuItem>
<DropdownMenuItem v-if="canDelete" class="text-xs font-bold gap-2 text-destructive focus:text-destructive" @click="handleDelete">
<DropdownMenuItem v-if="canDelete" class="text-xs font-bold gap-2 text-destructive" @click="handleDelete">
<Icon name="lucide:trash-2" size="14" />
Delete
</DropdownMenuItem>
Expand Down
2 changes: 1 addition & 1 deletion app/components/post/PostDetail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ async function handleShare() {
</DropdownMenuTrigger>
<DropdownMenuContent align="end" class="min-w-[120px]">
<DropdownMenuItem v-if="canEditPost" class="text-xs font-bold gap-2" @click="startEditPost"><Icon name="lucide:pencil" size="14" /> Edit</DropdownMenuItem>
<DropdownMenuItem v-if="canDeletePost" class="text-xs font-bold gap-2 text-destructive focus:text-destructive" @click="handleDeletePost"><Icon name="lucide:trash-2" size="14" /> Delete</DropdownMenuItem>
<DropdownMenuItem v-if="canDeletePost" class="text-xs font-bold gap-2 text-destructive" @click="handleDeletePost"><Icon name="lucide:trash-2" size="14" /> Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
Expand Down
1 change: 1 addition & 0 deletions app/components/post/PostDetailModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ watch(open, (isOpen) => {
<Dialog v-model:open="open" :modal="true">
<DialogContent
:show-close-button="false"
@open-auto-focus.prevent
@pointer-down-outside="preventShadcnDialogClose"
@escape-key-down="preventShadcnDialogClose"
class="!max-w-[1100px] h-[90vh] !p-0 !gap-0 overflow-hidden border-border bg-background !rounded-2xl flex flex-col"
Expand Down
11 changes: 8 additions & 3 deletions app/composables/usePermission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | undefined> | ComputedRef<string | undefined>, type: ResourceType) {
const { data: session } = useAuthSession()
const ctx = useOrgContext()
Expand Down Expand Up @@ -38,7 +37,13 @@ export function usePermission(authorId: Ref<string | undefined> | ComputedRef<st
return isOwner.value
})

const canDelete = computed(() => 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)

Expand Down
14 changes: 5 additions & 9 deletions server/api/admin/posts/[id].delete.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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(
Expand Down
Loading