feat(mentor-workspace): re-scope files from assignment to project for team-wide visibility
- MentorFile.projectId is the new access boundary; mentorAssignmentId stays as informational audit FK (nullable). - uploadFile derives projectId from the assignment; getFiles takes projectId directly; deleteFile/addFileComment auth checks any mentor on the project OR a project team member. - HMAC upload token now binds to projectId (in addition to assignmentId). - promoteFile reads file.projectId directly (no more mentorAssignment null navigation). - Removes 3 placeholder NOT_FOUND guards added in Task 4.
This commit is contained in:
@@ -152,6 +152,11 @@ export async function markRead(
|
||||
|
||||
/**
|
||||
* Record a file upload in a workspace.
|
||||
*
|
||||
* `workspaceId` is the originating MentorAssignment id (kept on the row as an
|
||||
* audit-trail FK). We derive the project id from that assignment so the file
|
||||
* is bound to the project — meaning any co-mentor on the project can see/use
|
||||
* it, and the row survives if this particular assignment is later dropped.
|
||||
*/
|
||||
export async function uploadFile(
|
||||
params: {
|
||||
@@ -180,6 +185,7 @@ export async function uploadFile(
|
||||
|
||||
return prisma.mentorFile.create({
|
||||
data: {
|
||||
projectId: assignment.projectId,
|
||||
mentorAssignmentId: params.workspaceId,
|
||||
uploadedByUserId: params.uploadedByUserId,
|
||||
fileName: params.fileName,
|
||||
@@ -238,9 +244,6 @@ export async function promoteFile(
|
||||
try {
|
||||
const file = await prisma.mentorFile.findUnique({
|
||||
where: { id: params.mentorFileId },
|
||||
include: {
|
||||
mentorAssignment: { select: { projectId: true } },
|
||||
},
|
||||
})
|
||||
|
||||
if (!file) {
|
||||
@@ -265,7 +268,7 @@ export async function promoteFile(
|
||||
// Create promotion event
|
||||
await tx.submissionPromotionEvent.create({
|
||||
data: {
|
||||
projectId: file.mentorAssignment.projectId,
|
||||
projectId: file.projectId,
|
||||
roundId: params.roundId,
|
||||
slotKey: params.slotKey,
|
||||
sourceType: 'MENTOR_FILE',
|
||||
@@ -281,7 +284,7 @@ export async function promoteFile(
|
||||
entityId: params.mentorFileId,
|
||||
actorId: params.promotedById,
|
||||
detailsJson: {
|
||||
projectId: file.mentorAssignment.projectId,
|
||||
projectId: file.projectId,
|
||||
roundId: params.roundId,
|
||||
slotKey: params.slotKey,
|
||||
fileName: file.fileName,
|
||||
@@ -297,7 +300,7 @@ export async function promoteFile(
|
||||
entityType: 'MentorFile',
|
||||
entityId: params.mentorFileId,
|
||||
detailsJson: {
|
||||
projectId: file.mentorAssignment.projectId,
|
||||
projectId: file.projectId,
|
||||
slotKey: params.slotKey,
|
||||
},
|
||||
})
|
||||
@@ -314,14 +317,17 @@ export async function promoteFile(
|
||||
}
|
||||
|
||||
/**
|
||||
* List files for a workspace, newest first, with comment counts and uploader.
|
||||
* List files for a project, newest first, with comment counts and uploader.
|
||||
* Project-scoped: every mentor assigned to the project (and every team member)
|
||||
* sees the same file list, even if some files were uploaded under a now-dropped
|
||||
* assignment.
|
||||
*/
|
||||
export async function getFiles(
|
||||
workspaceId: string,
|
||||
projectId: string,
|
||||
prisma: PrismaClient,
|
||||
) {
|
||||
return prisma.mentorFile.findMany({
|
||||
where: { mentorAssignmentId: workspaceId },
|
||||
where: { projectId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
uploadedBy: { select: { id: true, name: true, email: true } },
|
||||
@@ -331,8 +337,10 @@ export async function getFiles(
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a file. Caller must be either the uploader OR the assigned mentor.
|
||||
* Removes the MinIO object and the DB row + cascade-deletes comments.
|
||||
* Delete a file. Caller must be either the uploader, OR any mentor currently
|
||||
* assigned (not dropped) to the file's project, OR a team member of the
|
||||
* file's project. Removes the MinIO object and the DB row + cascade-deletes
|
||||
* comments.
|
||||
*/
|
||||
export async function deleteFile(
|
||||
params: { mentorFileId: string; userId: string },
|
||||
@@ -341,13 +349,30 @@ export async function deleteFile(
|
||||
): Promise<void> {
|
||||
const file = await prisma.mentorFile.findUnique({
|
||||
where: { id: params.mentorFileId },
|
||||
include: { mentorAssignment: { select: { mentorId: true } } },
|
||||
})
|
||||
if (!file) throw new Error('File not found')
|
||||
const isUploader = file.uploadedByUserId === params.userId
|
||||
const isMentor = file.mentorAssignment.mentorId === params.userId
|
||||
if (!isUploader && !isMentor) {
|
||||
throw new Error('Only the uploader or the assigned mentor can delete this file')
|
||||
let isAuthorized = isUploader
|
||||
if (!isAuthorized) {
|
||||
const mentorAssignment = await prisma.mentorAssignment.findFirst({
|
||||
where: { projectId: file.projectId, mentorId: params.userId, droppedAt: null },
|
||||
select: { id: true },
|
||||
})
|
||||
if (mentorAssignment) {
|
||||
isAuthorized = true
|
||||
}
|
||||
}
|
||||
if (!isAuthorized) {
|
||||
const teamMembership = await prisma.teamMember.findFirst({
|
||||
where: { projectId: file.projectId, userId: params.userId },
|
||||
select: { id: true },
|
||||
})
|
||||
if (teamMembership) {
|
||||
isAuthorized = true
|
||||
}
|
||||
}
|
||||
if (!isAuthorized) {
|
||||
throw new Error('Only the uploader, an assigned mentor, or a team member can delete this file')
|
||||
}
|
||||
try {
|
||||
await removeStorageObject(file.bucket, file.objectKey)
|
||||
|
||||
Reference in New Issue
Block a user