Add email password change feature and fix nginx config
- Add public page at /email/change-password for Poste.io mailbox password management - Add API routes for SMTP credential verification and Poste.io password change - Rewrite nginx config as HTTP-only (certbot --nginx will add SSL) - Add Poste.io admin API env vars to docker-compose and env templates Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
127
src/app/api/email/change-password/route.ts
Normal file
127
src/app/api/email/change-password/route.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import nodemailer from 'nodemailer'
|
||||
import { checkRateLimit } from '@/lib/rate-limit'
|
||||
|
||||
const MAIL_DOMAIN = process.env.POSTE_MAIL_DOMAIN || 'monaco-opc.com'
|
||||
const SMTP_HOST = process.env.SMTP_HOST || 'localhost'
|
||||
const SMTP_PORT = parseInt(process.env.SMTP_PORT || '587')
|
||||
const POSTE_API_URL = process.env.POSTE_API_URL || 'https://mail.monaco-opc.com'
|
||||
const POSTE_ADMIN_EMAIL = process.env.POSTE_ADMIN_EMAIL || ''
|
||||
const POSTE_ADMIN_PASSWORD = process.env.POSTE_ADMIN_PASSWORD || ''
|
||||
|
||||
const PASSWORD_MIN_LENGTH = 8
|
||||
const PASSWORD_REGEX = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/
|
||||
|
||||
function validateNewPassword(password: string): string | null {
|
||||
if (password.length < PASSWORD_MIN_LENGTH) {
|
||||
return `Password must be at least ${PASSWORD_MIN_LENGTH} characters.`
|
||||
}
|
||||
if (!PASSWORD_REGEX.test(password)) {
|
||||
return 'Password must contain at least one uppercase letter, one lowercase letter, and one number.'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest): Promise<NextResponse> {
|
||||
const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || 'unknown'
|
||||
const rateLimit = checkRateLimit(`email-change:${ip}`, 3, 15 * 60 * 1000)
|
||||
|
||||
if (!rateLimit.success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Too many attempts. Please try again later.' },
|
||||
{ status: 429 }
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { email, currentPassword, newPassword } = body as {
|
||||
email: string
|
||||
currentPassword: string
|
||||
newPassword: string
|
||||
}
|
||||
|
||||
if (!email || !currentPassword || !newPassword) {
|
||||
return NextResponse.json(
|
||||
{ error: 'All fields are required.' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const emailLower = email.toLowerCase().trim()
|
||||
|
||||
if (!emailLower.endsWith(`@${MAIL_DOMAIN}`)) {
|
||||
return NextResponse.json(
|
||||
{ error: `Email must be an @${MAIL_DOMAIN} address.` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const passwordError = validateNewPassword(newPassword)
|
||||
if (passwordError) {
|
||||
return NextResponse.json({ error: passwordError }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!POSTE_ADMIN_EMAIL || !POSTE_ADMIN_PASSWORD) {
|
||||
console.error('Poste.io admin credentials not configured')
|
||||
return NextResponse.json(
|
||||
{ error: 'Email service is not configured. Contact an administrator.' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
|
||||
// Re-verify current credentials via SMTP
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: SMTP_HOST,
|
||||
port: SMTP_PORT,
|
||||
secure: SMTP_PORT === 465,
|
||||
auth: {
|
||||
user: emailLower,
|
||||
pass: currentPassword,
|
||||
},
|
||||
connectionTimeout: 10000,
|
||||
greetingTimeout: 10000,
|
||||
})
|
||||
|
||||
try {
|
||||
await transporter.verify()
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: 'Current password is incorrect.' },
|
||||
{ status: 401 }
|
||||
)
|
||||
} finally {
|
||||
transporter.close()
|
||||
}
|
||||
|
||||
// Change password via Poste.io Admin API
|
||||
const apiUrl = `${POSTE_API_URL}/admin/api/v1/boxes/${encodeURIComponent(emailLower)}`
|
||||
const authHeader = 'Basic ' + Buffer.from(`${POSTE_ADMIN_EMAIL}:${POSTE_ADMIN_PASSWORD}`).toString('base64')
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': authHeader,
|
||||
},
|
||||
body: JSON.stringify({ passwordPlaintext: newPassword }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Poste.io API error:', response.status, await response.text())
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to change password. Please try again or contact an administrator.' },
|
||||
{ status: 502 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (err) {
|
||||
console.error('Password change error:', err)
|
||||
return NextResponse.json(
|
||||
{ error: 'An unexpected error occurred.' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user