fix: applicant portal — document uploads, round filtering, auth hardening

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 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 13:29:39 +01:00
parent 1103d42439
commit a39e27f6ff
8 changed files with 192 additions and 37 deletions

View File

@@ -15,7 +15,19 @@ const LOCKOUT_DURATION_MS = 15 * 60 * 1000 // 15 minutes
export const { handlers, auth, signIn, signOut } = NextAuth({
...authConfig,
adapter: PrismaAdapter(prisma),
adapter: {
...PrismaAdapter(prisma),
async useVerificationToken({ identifier, token }: { identifier: string; token: string }) {
try {
return await prisma.verificationToken.delete({
where: { identifier_token: { identifier, token } },
})
} catch (e) {
if ((e as { code?: string }).code === 'P2025') return null
throw e
}
},
},
providers: [
// Email provider for magic links (used for first login and password reset)
EmailProvider({
@@ -129,7 +141,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
},
})
if (!user || user.status === 'SUSPENDED' || !user.passwordHash) {
if (!user || user.status === 'SUSPENDED') {
// Track failed attempt (don't reveal whether user exists)
const current = failedAttempts.get(email) || { count: 0, lockedUntil: 0 }
current.count++
@@ -139,19 +151,24 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
}
failedAttempts.set(email, current)
// Log failed login
// Log failed login — real security event
await prisma.auditLog.create({
data: {
userId: null,
action: 'LOGIN_FAILED',
entityType: 'User',
detailsJson: { email, reason: !user ? 'user_not_found' : user.status === 'SUSPENDED' ? 'suspended' : 'no_password' },
detailsJson: { email, reason: !user ? 'user_not_found' : 'suspended' },
},
}).catch(() => {})
return null
}
if (!user.passwordHash) {
// Magic-link user tried credentials form — expected, not a security event
return null
}
// Verify password
const isValid = await verifyPassword(password, user.passwordHash)
if (!isValid) {