Add admin-only comment resolution controls

This commit is contained in:
Yusuf İpek
2026-02-23 18:21:10 +03:00
parent fe7235052e
commit dd884a325c
4 changed files with 43 additions and 22 deletions
+17 -7
View File
@@ -8,6 +8,7 @@ import { validateShareLinkAccess } from '@/lib/share-links';
import { getShareSessionFromRequest } from '@/lib/share-session';
import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response';
import { getGuestIdentityFromRequest } from '@/lib/guest-identity';
import { ProjectMemberRole, WorkspaceMemberRole } from '@prisma/client';
type RouteParams = { params: Promise<{ commentId: string }> };
@@ -156,7 +157,10 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
const userId = session?.user?.id ?? null;
const isOwner = userId === project.ownerId;
const isAuthor = !!userId && comment.authorId === userId;
const isMember = !!userId && project.members.some((member) => member.userId === userId);
const projectMembership = userId
? project.members.find((member) => member.userId === userId) ?? null
: null;
const isProjectAdmin = projectMembership?.role === ProjectMemberRole.ADMIN;
const guestIdentityId = !userId ? getGuestIdentityFromRequest(request) : null;
const isGuestAuthor = !userId
&& !comment.authorId
@@ -164,9 +168,9 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
&& guestIdentityId === comment.guestIdentityId;
const canEditOwnContent = isAuthor || isGuestAuthor;
// Check workspace membership for resolve permissions
let isWorkspaceMember = false;
if (!isOwner && !isMember && userId) {
// Check workspace role for resolve permissions.
let workspaceRole: WorkspaceMemberRole | 'OWNER' | null = null;
if (!isOwner && userId) {
const wsMember = await db.workspaceMember.findUnique({
where: {
workspaceId_userId: {
@@ -179,8 +183,14 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
where: { id: project.workspaceId },
select: { ownerId: true },
});
isWorkspaceMember = !!wsMember || wsOwner?.ownerId === userId;
if (wsOwner?.ownerId === userId) {
workspaceRole = 'OWNER';
} else if (wsMember) {
workspaceRole = wsMember.role;
}
}
const isWorkspaceAdmin = workspaceRole === 'OWNER' || workspaceRole === WorkspaceMemberRole.ADMIN;
const canResolveComment = isOwner || isProjectAdmin || isWorkspaceAdmin;
if (!userId && !isGuestAuthor) {
const shareSession = getShareSessionFromRequest(request, comment.version.video.id);
@@ -205,8 +215,8 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
}
// Owner, author, members, or workspace members can resolve/unresolve
if (isResolved !== undefined && !isOwner && !canEditOwnContent && !isMember && !isWorkspaceMember) {
return apiErrors.forbidden('Access denied');
if (isResolved !== undefined && !canResolveComment) {
return apiErrors.forbidden('Only admins can resolve comments');
}
const updateData: Record<string, unknown> = {};
@@ -108,6 +108,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
currentUserName: session?.user?.name || null,
canDownload: access.hasAccess,
canManageTags: access.canEdit,
canResolveComments: access.canEdit,
});
return withCacheControl(response, 'private, no-cache');
+1
View File
@@ -191,6 +191,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
canComment: canCommentWithMembership || canCommentWithShareLink,
canDownload: canDownloadWithMembership || canDownloadWithShareLink,
canManageTags: access.canEdit,
canResolveComments: access.canEdit,
});
return withCacheControl(response, 'private, no-cache');
+10 -1
View File
@@ -167,6 +167,7 @@ interface VideoData {
canComment?: boolean;
canDownload?: boolean;
canManageTags?: boolean;
canResolveComments?: boolean;
}
function formatTime(seconds: number): string {
@@ -511,6 +512,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
// Determine current user info for permission checks and comment display
const currentUserId = video?.currentUserId || null;
const currentUserName = video?.currentUserName || null;
const canResolveComments = !!video?.canResolveComments;
const apiBasePath = mode === 'dashboard'
? `/api/projects/${propProjectId}/videos/${videoId}`
@@ -1959,6 +1961,11 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
const handleResolveComment = useCallback(
async (commentId: string, currentlyResolved: boolean) => {
if (!video?.canResolveComments) {
toast.error('Only admins can resolve comments');
return;
}
isMutatingRef.current = true;
setVideo((prev) => {
if (!prev) return prev;
@@ -2025,7 +2032,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
isMutatingRef.current = false;
}
},
[activeVersionId]
[activeVersionId, video?.canResolveComments]
);
const handleReplyComment = useCallback(async (parentId: string, voiceData?: { url: string; duration: number }, imageData?: { url: string }) => {
@@ -3605,6 +3612,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
{formatTime(comment.timestamp)}
<ArrowUpRight className="h-3 w-3" />
</button>
{canResolveComments && (
<Button
variant="ghost"
size="icon"
@@ -3619,6 +3627,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
<Circle className="h-4 w-4" />
)}
</Button>
)}
{canManageComment && (
<DropdownMenu>
<DropdownMenuTrigger asChild>