Fix S3/SMTP connectivity and add one-click invite flow
- Fix MinIO port parsing bug: use protocol-appropriate defaults (443/80) instead of hardcoded 9000 fallback, enabling public URL endpoint - Remove unused SMTP server config from NextAuth EmailProvider to prevent connection errors (sendVerificationRequest is fully overridden) - Replace extra_hosts with DNS config (8.8.8.8) so container resolves mail.monaco-opc.com to public IP instead of host loopback - Add invite token auth: single-click accept-invite flow replacing broken two-email invitation process - Auto-send invitation emails on bulk user creation - Update email template expiry text from 24 hours to 7 days Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,28 +19,65 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
||||
providers: [
|
||||
// Email provider for magic links (used for first login and password reset)
|
||||
EmailProvider({
|
||||
server: {
|
||||
host: process.env.SMTP_HOST,
|
||||
port: Number(process.env.SMTP_PORT || 587),
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
},
|
||||
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 }) => {
|
||||
await sendMagicLinkEmail(email, url)
|
||||
},
|
||||
}),
|
||||
// Credentials provider for email/password login
|
||||
// Credentials provider for email/password login and invite token auth
|
||||
CredentialsProvider({
|
||||
name: 'credentials',
|
||||
credentials: {
|
||||
email: { label: 'Email', type: 'email' },
|
||||
password: { label: 'Password', type: 'password' },
|
||||
inviteToken: { label: 'Invite Token', type: 'text' },
|
||||
},
|
||||
async authorize(credentials) {
|
||||
// Handle invite token authentication
|
||||
if (credentials?.inviteToken) {
|
||||
const token = credentials.inviteToken as string
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { inviteToken: token },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
role: true,
|
||||
status: true,
|
||||
inviteTokenExpiresAt: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (!user || user.status !== 'INVITED') {
|
||||
return null
|
||||
}
|
||||
|
||||
if (user.inviteTokenExpiresAt && user.inviteTokenExpiresAt < new Date()) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Clear token, activate user, mark as needing password
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
inviteToken: null,
|
||||
inviteTokenExpiresAt: null,
|
||||
status: 'ACTIVE',
|
||||
mustSetPassword: true,
|
||||
lastLoginAt: new Date(),
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
mustSetPassword: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (!credentials?.email || !credentials?.password) {
|
||||
return null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user