feat: mentor workspace files end-to-end with secure presign
Adds generateMentorObjectKey helper producing <projectName>/mentorship/<timestamp>-<file>. Replaces the client-supplied bucket/objectKey on workspaceUploadFile with an HMAC-signed upload token that binds bucket, objectKey, uploader, and a 1h expiry — paths can no longer be forged from the client. Adds workspaceGetUploadUrl, workspaceGetFiles, workspaceGetFileDownloadUrl, workspaceDeleteFile procedures with mentor-or-team-member auth. Builds <WorkspaceFilesPanel> and wires it into the mentor workspace Files tab and the applicant /applicant/mentor page. Replaces the file-promotion-panel mock array with a real workspaceGetFiles query. Tests cover token sign/verify (5), key construction (5), and end-to-end procedure flow including auth + tampered tokens (7). Spec: docs/superpowers/specs/2026-04-28-mentor-round-readiness-design.md §F.1 Plan: docs/superpowers/plans/2026-04-28-pr2-mentor-workspace-files.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -312,3 +312,47 @@ export async function promoteFile(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List files for a workspace, newest first, with comment counts and uploader.
|
||||
*/
|
||||
export async function getFiles(
|
||||
workspaceId: string,
|
||||
prisma: PrismaClient,
|
||||
) {
|
||||
return prisma.mentorFile.findMany({
|
||||
where: { mentorAssignmentId: workspaceId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
uploadedBy: { select: { id: true, name: true, email: true } },
|
||||
_count: { select: { comments: true } },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a file. Caller must be either the uploader OR the assigned mentor.
|
||||
* Removes the MinIO object and the DB row + cascade-deletes comments.
|
||||
*/
|
||||
export async function deleteFile(
|
||||
params: { mentorFileId: string; userId: string },
|
||||
prisma: PrismaClient,
|
||||
removeStorageObject: (bucket: string, key: string) => Promise<void>,
|
||||
): 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')
|
||||
}
|
||||
try {
|
||||
await removeStorageObject(file.bucket, file.objectKey)
|
||||
} catch (err) {
|
||||
console.error('[mentor-workspace] failed to delete storage object', file.objectKey, err)
|
||||
}
|
||||
await prisma.mentorFile.delete({ where: { id: params.mentorFileId } })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user