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:
Matt
2026-04-28 13:33:18 +02:00
parent dd48db5eea
commit 2e7b545a1b
11 changed files with 699 additions and 30 deletions

View File

@@ -11,6 +11,7 @@ import {
} from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton'
import { MentorChat } from '@/components/shared/mentor-chat'
import { WorkspaceFilesPanel } from '@/components/mentor/workspace-files-panel'
import {
MessageSquare,
UserCircle,
@@ -133,6 +134,14 @@ export default function ApplicantMentorPage() {
</CardContent>
</Card>
)}
{/* Files */}
{dashboardData?.project?.mentorAssignment?.id && (
<WorkspaceFilesPanel
mentorAssignmentId={dashboardData.project.mentorAssignment.id}
asApplicant
/>
)}
</div>
)
}

View File

@@ -9,6 +9,7 @@ import { Skeleton } from '@/components/ui/skeleton'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { WorkspaceChat } from '@/components/mentor/workspace-chat'
import { FilePromotionPanel } from '@/components/mentor/file-promotion-panel'
import { WorkspaceFilesPanel } from '@/components/mentor/workspace-files-panel'
import { ArrowLeft, MessageSquare, FileText, Upload } from 'lucide-react'
import { toast } from 'sonner'
@@ -102,20 +103,16 @@ export default function MentorWorkspaceDetailPage() {
</TabsContent>
<TabsContent value="files" className="mt-6">
<Card>
<CardHeader>
<CardTitle>Workspace Files</CardTitle>
<CardDescription>
Files shared in the mentor workspace
</CardDescription>
</CardHeader>
<CardContent className="text-center py-8">
<FileText className="h-12 w-12 text-muted-foreground/50 mx-auto mb-3" />
<p className="text-sm text-muted-foreground">
File listing feature coming soon
</p>
</CardContent>
</Card>
{assignment ? (
<WorkspaceFilesPanel mentorAssignmentId={assignment.id} />
) : (
<Card>
<CardContent className="text-center py-8">
<FileText className="h-12 w-12 text-muted-foreground/50 mx-auto mb-3" />
<p className="text-sm text-muted-foreground">Loading workspace</p>
</CardContent>
</Card>
)}
</TabsContent>
<TabsContent value="promotion" className="mt-6">