fix: security hardening — block self-registration, SSE auth, audit logging fixes
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user