From a39e27f6ff775764f04e7f56574d348453789453 Mon Sep 17 00:00:00 2001
From: Matt
Date: Wed, 4 Mar 2026 13:29:39 +0100
Subject: [PATCH] =?UTF-8?q?fix:=20applicant=20portal=20=E2=80=94=20documen?=
=?UTF-8?q?t=20uploads,=20round=20filtering,=20auth=20hardening?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Fix round-specific document uploads (submittedAt no longer blocks uploads),
add view/download buttons for existing files, enforce active-round-only for
uploads/deletes. Harden auth layout and set-password page. Filter applicant
portal rounds by award track membership.
Co-Authored-By: Claude Opus 4.6
---
.../(applicant)/applicant/documents/page.tsx | 63 +++++++++++--
src/app/(applicant)/applicant/page.tsx | 20 ++++-
src/app/(applicant)/applicant/team/page.tsx | 5 +-
src/app/(auth)/layout.tsx | 5 +-
src/app/(auth)/set-password/page.tsx | 12 +--
src/lib/auth.ts | 25 +++++-
src/lib/storage/index.ts | 10 ++-
src/server/routers/applicant.ts | 89 +++++++++++++++++--
8 files changed, 192 insertions(+), 37 deletions(-)
diff --git a/src/app/(applicant)/applicant/documents/page.tsx b/src/app/(applicant)/applicant/documents/page.tsx
index 8b53d45..eed3343 100644
--- a/src/app/(applicant)/applicant/documents/page.tsx
+++ b/src/app/(applicant)/applicant/documents/page.tsx
@@ -3,6 +3,7 @@
import { useSession } from 'next-auth/react'
import { trpc } from '@/lib/trpc/client'
import { Badge } from '@/components/ui/badge'
+import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
@@ -20,6 +21,7 @@ import {
Video,
File,
Download,
+ Eye,
} from 'lucide-react'
const fileTypeIcons: Record = {
@@ -42,6 +44,34 @@ const fileTypeLabels: Record = {
SUPPORTING_DOC: 'Supporting Document',
}
+function FileActionButtons({ bucket, objectKey, fileName }: { bucket: string; objectKey: string; fileName: string }) {
+ const { data: viewData } = trpc.file.getDownloadUrl.useQuery(
+ { bucket, objectKey, forDownload: false },
+ { staleTime: 10 * 60 * 1000 }
+ )
+ const { data: dlData } = trpc.file.getDownloadUrl.useQuery(
+ { bucket, objectKey, forDownload: true, fileName },
+ { staleTime: 10 * 60 * 1000 }
+ )
+ const viewUrl = typeof viewData === 'string' ? viewData : viewData?.url
+ const dlUrl = typeof dlData === 'string' ? dlData : dlData?.url
+
+ return (
+
+ )
+}
+
export default function ApplicantDocumentsPage() {
const { status: sessionStatus } = useSession()
const isAuthenticated = sessionStatus === 'authenticated'
@@ -82,7 +112,7 @@ export default function ApplicantDocumentsPage() {
)
}
- const { project, openRounds } = data
+ const { project, openRounds, isRejected } = data
const isDraft = !project.submittedAt
return (
@@ -98,8 +128,20 @@ export default function ApplicantDocumentsPage() {
+ {/* Rejected banner */}
+ {isRejected && (
+
+
+
+
+ Your project was not selected to advance. Documents are view-only.
+
+
+
+ )}
+
{/* Per-round upload sections */}
- {openRounds.length > 0 && (
+ {!isRejected && openRounds.length > 0 && (
{openRounds.map((round: { id: string; name: string; windowCloseAt?: string | Date | null }) => {
const now = new Date()
@@ -163,18 +205,18 @@ export default function ApplicantDocumentsPage() {
{project.files.map((file) => {
const Icon = fileTypeIcons[file.fileType] || File
- const fileRecord = file as typeof file & { isLate?: boolean; roundId?: string | null }
+ const fileRecord = file as typeof file & { isLate?: boolean; roundId?: string | null; bucket?: string; objectKey?: string }
return (
-
-
-
+
+
+
-
{file.fileName}
+
{file.fileName}
{fileRecord.isLate && (
@@ -189,6 +231,13 @@ export default function ApplicantDocumentsPage() {
+ {fileRecord.bucket && fileRecord.objectKey && (
+
+ )}
)
})}
diff --git a/src/app/(applicant)/applicant/page.tsx b/src/app/(applicant)/applicant/page.tsx
index b6b0509..847193a 100644
--- a/src/app/(applicant)/applicant/page.tsx
+++ b/src/app/(applicant)/applicant/page.tsx
@@ -111,7 +111,7 @@ export default function ApplicantDashboardPage() {
)
}
- const { project, timeline, currentStatus, openRounds, hasPassedIntake } = data
+ const { project, timeline, currentStatus, openRounds, hasPassedIntake, isRejected } = data
const programYear = project.program?.year
const programName = project.program?.name
const totalEvaluations = evaluations?.reduce((sum, r) => sum + r.evaluationCount, 0) ?? 0
@@ -221,8 +221,23 @@ export default function ApplicantDashboardPage() {
+ {/* Rejected banner */}
+ {isRejected && (
+
+
+
+
+
+ Your project was not selected to advance. Your project space is now read-only.
+
+
+
+
+ )}
+
{/* Quick actions */}
-
+ {!isRejected && (
+
@@ -266,6 +281,7 @@ export default function ApplicantDashboardPage() {
)}
+ )}
{/* Document Completeness */}
{docCompleteness && docCompleteness.length > 0 && (
diff --git a/src/app/(applicant)/applicant/team/page.tsx b/src/app/(applicant)/applicant/team/page.tsx
index 953e163..dd0255f 100644
--- a/src/app/(applicant)/applicant/team/page.tsx
+++ b/src/app/(applicant)/applicant/team/page.tsx
@@ -123,6 +123,7 @@ export default function ApplicantProjectPage() {
const project = dashboardData?.project
const projectId = project?.id
const isIntakeOpen = dashboardData?.isIntakeOpen ?? false
+ const isRejected = dashboardData?.isRejected ?? false
const { data: teamData, isLoading: teamLoading, refetch } = trpc.applicant.getTeamMembers.useQuery(
{ projectId: projectId! },
@@ -398,7 +399,7 @@ export default function ApplicantProjectPage() {
Everyone on this list can view and collaborate on this project.
- {isTeamLead && (
+ {isTeamLead && !isRejected && (
- {isTeamLead && member.role !== 'LEAD' && teamData.submittedBy?.id !== member.userId && (
+ {isTeamLead && !isRejected && member.role !== 'LEAD' && teamData.submittedBy?.id !== member.userId && (