fix: security hardening — block self-registration, SSE auth, audit logging fixes
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled

Security fixes:
- Block self-registration via magic link (PrismaAdapter createUser throws)
- Magic links only sent to existing ACTIVE users (prevents enumeration)
- signIn callback rejects non-existent users (defense-in-depth)
- Change schema default role from JURY_MEMBER to APPLICANT
- Add authentication to live-voting SSE stream endpoint
- Fix false FILE_OPENED/FILE_DOWNLOADED audit events on page load
  (remove purpose from eagerly pre-fetched URL queries)

Bug fixes:
- Fix impersonation skeleton screen on applicant dashboard
- Fix onboarding redirect loop in auth layout

Observer dashboard redesign (Steps 1-6):
- Clickable round pipeline with selected round highlighting
- Round-type-specific dashboard panels (intake, filtering, evaluation,
  submission, mentoring, live final, deliberation)
- Enhanced activity feed with server-side humanization
- Previous round comparison section
- New backend queries for round-specific analytics

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 20:18:50 +01:00
parent 13f125af28
commit 875c2e8f48
23 changed files with 2126 additions and 410 deletions

View File

@@ -27,6 +27,10 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
},
adapter: {
...PrismaAdapter(prisma),
// Block auto-creation of users via magic link — only pre-created users can log in
createUser: () => {
throw new Error('Self-registration is not allowed. Please contact an administrator.')
},
async useVerificationToken({ identifier, token }: { identifier: string; token: string }) {
try {
return await prisma.verificationToken.delete({
@@ -39,7 +43,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
},
},
providers: [
// Email provider for magic links (used for first login and password reset)
// Email provider for magic links (only for existing active users)
EmailProvider({
// Server config required by NextAuth validation but not used —
// sendVerificationRequest below fully overrides email sending via getTransporter()
@@ -54,6 +58,16 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
from: process.env.EMAIL_FROM || 'MOPC Platform <noreply@monaco-opc.com>',
maxAge: parseInt(process.env.MAGIC_LINK_EXPIRY || '900'), // 15 minutes
sendVerificationRequest: async ({ identifier: email, url }) => {
// Only send magic links to existing, ACTIVE users
const existingUser = await prisma.user.findUnique({
where: { email: email.toLowerCase().trim() },
select: { status: true },
})
if (!existingUser || existingUser.status !== 'ACTIVE') {
// Silently skip — don't reveal whether the email exists (prevents enumeration)
console.log(`[auth] Magic link requested for non-active/unknown email: ${email}`)
return
}
await sendMagicLinkEmail(email, url)
},
}),
@@ -355,7 +369,12 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
},
})
if (dbUser?.status === 'SUSPENDED') {
// Block non-existent users (defense-in-depth against adapter auto-creation)
if (!dbUser) {
return false
}
if (dbUser.status === 'SUSPENDED') {
return false // Block suspended users
}
@@ -363,12 +382,10 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
// The completeOnboarding mutation sets status to ACTIVE.
// Add user data for JWT callback
if (dbUser) {
user.id = dbUser.id
user.role = dbUser.role
user.roles = dbUser.roles.length ? dbUser.roles : [dbUser.role]
user.mustSetPassword = dbUser.mustSetPassword || !dbUser.passwordHash
}
user.id = dbUser.id
user.role = dbUser.role
user.roles = dbUser.roles.length ? dbUser.roles : [dbUser.role]
user.mustSetPassword = dbUser.mustSetPassword || !dbUser.passwordHash
}
// Update last login time on actual sign-in