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:
2026-01-31 14:13:16 +01:00
parent 5aedade41d
commit 81db15333f
9 changed files with 387 additions and 23 deletions

View File

@@ -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
}