mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
Add admin-only comment resolution controls
This commit is contained in:
@@ -8,6 +8,7 @@ import { validateShareLinkAccess } from '@/lib/share-links';
|
|||||||
import { getShareSessionFromRequest } from '@/lib/share-session';
|
import { getShareSessionFromRequest } from '@/lib/share-session';
|
||||||
import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response';
|
import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response';
|
||||||
import { getGuestIdentityFromRequest } from '@/lib/guest-identity';
|
import { getGuestIdentityFromRequest } from '@/lib/guest-identity';
|
||||||
|
import { ProjectMemberRole, WorkspaceMemberRole } from '@prisma/client';
|
||||||
|
|
||||||
type RouteParams = { params: Promise<{ commentId: string }> };
|
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 userId = session?.user?.id ?? null;
|
||||||
const isOwner = userId === project.ownerId;
|
const isOwner = userId === project.ownerId;
|
||||||
const isAuthor = !!userId && comment.authorId === userId;
|
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 guestIdentityId = !userId ? getGuestIdentityFromRequest(request) : null;
|
||||||
const isGuestAuthor = !userId
|
const isGuestAuthor = !userId
|
||||||
&& !comment.authorId
|
&& !comment.authorId
|
||||||
@@ -164,9 +168,9 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
|||||||
&& guestIdentityId === comment.guestIdentityId;
|
&& guestIdentityId === comment.guestIdentityId;
|
||||||
const canEditOwnContent = isAuthor || isGuestAuthor;
|
const canEditOwnContent = isAuthor || isGuestAuthor;
|
||||||
|
|
||||||
// Check workspace membership for resolve permissions
|
// Check workspace role for resolve permissions.
|
||||||
let isWorkspaceMember = false;
|
let workspaceRole: WorkspaceMemberRole | 'OWNER' | null = null;
|
||||||
if (!isOwner && !isMember && userId) {
|
if (!isOwner && userId) {
|
||||||
const wsMember = await db.workspaceMember.findUnique({
|
const wsMember = await db.workspaceMember.findUnique({
|
||||||
where: {
|
where: {
|
||||||
workspaceId_userId: {
|
workspaceId_userId: {
|
||||||
@@ -179,8 +183,14 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
|||||||
where: { id: project.workspaceId },
|
where: { id: project.workspaceId },
|
||||||
select: { ownerId: true },
|
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) {
|
if (!userId && !isGuestAuthor) {
|
||||||
const shareSession = getShareSessionFromRequest(request, comment.version.video.id);
|
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
|
// Owner, author, members, or workspace members can resolve/unresolve
|
||||||
if (isResolved !== undefined && !isOwner && !canEditOwnContent && !isMember && !isWorkspaceMember) {
|
if (isResolved !== undefined && !canResolveComment) {
|
||||||
return apiErrors.forbidden('Access denied');
|
return apiErrors.forbidden('Only admins can resolve comments');
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateData: Record<string, unknown> = {};
|
const updateData: Record<string, unknown> = {};
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
|
|||||||
currentUserName: session?.user?.name || null,
|
currentUserName: session?.user?.name || null,
|
||||||
canDownload: access.hasAccess,
|
canDownload: access.hasAccess,
|
||||||
canManageTags: access.canEdit,
|
canManageTags: access.canEdit,
|
||||||
|
canResolveComments: access.canEdit,
|
||||||
});
|
});
|
||||||
|
|
||||||
return withCacheControl(response, 'private, no-cache');
|
return withCacheControl(response, 'private, no-cache');
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
|
|||||||
canComment: canCommentWithMembership || canCommentWithShareLink,
|
canComment: canCommentWithMembership || canCommentWithShareLink,
|
||||||
canDownload: canDownloadWithMembership || canDownloadWithShareLink,
|
canDownload: canDownloadWithMembership || canDownloadWithShareLink,
|
||||||
canManageTags: access.canEdit,
|
canManageTags: access.canEdit,
|
||||||
|
canResolveComments: access.canEdit,
|
||||||
});
|
});
|
||||||
|
|
||||||
return withCacheControl(response, 'private, no-cache');
|
return withCacheControl(response, 'private, no-cache');
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ interface VideoData {
|
|||||||
canComment?: boolean;
|
canComment?: boolean;
|
||||||
canDownload?: boolean;
|
canDownload?: boolean;
|
||||||
canManageTags?: boolean;
|
canManageTags?: boolean;
|
||||||
|
canResolveComments?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTime(seconds: number): string {
|
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
|
// Determine current user info for permission checks and comment display
|
||||||
const currentUserId = video?.currentUserId || null;
|
const currentUserId = video?.currentUserId || null;
|
||||||
const currentUserName = video?.currentUserName || null;
|
const currentUserName = video?.currentUserName || null;
|
||||||
|
const canResolveComments = !!video?.canResolveComments;
|
||||||
|
|
||||||
const apiBasePath = mode === 'dashboard'
|
const apiBasePath = mode === 'dashboard'
|
||||||
? `/api/projects/${propProjectId}/videos/${videoId}`
|
? `/api/projects/${propProjectId}/videos/${videoId}`
|
||||||
@@ -1959,6 +1961,11 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
|
|||||||
|
|
||||||
const handleResolveComment = useCallback(
|
const handleResolveComment = useCallback(
|
||||||
async (commentId: string, currentlyResolved: boolean) => {
|
async (commentId: string, currentlyResolved: boolean) => {
|
||||||
|
if (!video?.canResolveComments) {
|
||||||
|
toast.error('Only admins can resolve comments');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
isMutatingRef.current = true;
|
isMutatingRef.current = true;
|
||||||
setVideo((prev) => {
|
setVideo((prev) => {
|
||||||
if (!prev) return prev;
|
if (!prev) return prev;
|
||||||
@@ -2025,7 +2032,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
|
|||||||
isMutatingRef.current = false;
|
isMutatingRef.current = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[activeVersionId]
|
[activeVersionId, video?.canResolveComments]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleReplyComment = useCallback(async (parentId: string, voiceData?: { url: string; duration: number }, imageData?: { url: string }) => {
|
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)}
|
{formatTime(comment.timestamp)}
|
||||||
<ArrowUpRight className="h-3 w-3" />
|
<ArrowUpRight className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
|
{canResolveComments && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -3619,6 +3627,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
|
|||||||
<Circle className="h-4 w-4" />
|
<Circle className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
{canManageComment && (
|
{canManageComment && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
|
|||||||
Reference in New Issue
Block a user