Files
MOPC-Portal/src/app/(public)/email/change-password/page.tsx

387 lines
14 KiB
TypeScript
Raw Normal View History

'use client'
import { useState } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Label } from '@/components/ui/label'
import { AlertCircle, CheckCircle2, Eye, EyeOff, Loader2, Mail, Lock, Monitor, Smartphone } from 'lucide-react'
type Step = 'verify' | 'change' | 'success'
const MAIL_DOMAIN = 'monaco-opc.com'
const MAIL_SERVER = 'mail.monaco-opc.com'
export default function ChangeEmailPasswordPage() {
const [step, setStep] = useState<Step>('verify')
const [email, setEmail] = useState('')
const [currentPassword, setCurrentPassword] = useState('')
const [newPassword, setNewPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [showCurrentPassword, setShowCurrentPassword] = useState(false)
const [showNewPassword, setShowNewPassword] = useState(false)
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
async function handleVerify(e: React.FormEvent) {
e.preventDefault()
setError('')
const emailLower = email.toLowerCase().trim()
if (!emailLower.endsWith(`@${MAIL_DOMAIN}`)) {
setError(`Email must be an @${MAIL_DOMAIN} address.`)
return
}
setLoading(true)
try {
const res = await fetch('/api/email/verify-credentials', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: emailLower, password: currentPassword }),
})
const data = await res.json()
if (res.status === 429) {
setError(data.error || 'Too many attempts. Please try again later.')
return
}
if (!data.valid) {
setError(data.error || 'Invalid email or password.')
return
}
setStep('change')
} catch {
setError('Connection error. Please try again.')
} finally {
setLoading(false)
}
}
async function handleChangePassword(e: React.FormEvent) {
e.preventDefault()
setError('')
if (newPassword.length < 8) {
setError('Password must be at least 8 characters.')
return
}
if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(newPassword)) {
setError('Password must contain at least one uppercase letter, one lowercase letter, and one number.')
return
}
if (newPassword !== confirmPassword) {
setError('Passwords do not match.')
return
}
setLoading(true)
try {
const res = await fetch('/api/email/change-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: email.toLowerCase().trim(),
currentPassword,
newPassword,
}),
})
const data = await res.json()
if (!res.ok) {
setError(data.error || 'Failed to change password.')
return
}
setStep('success')
} catch {
setError('Connection error. Please try again.')
} finally {
setLoading(false)
}
}
return (
<div className="mx-auto max-w-lg">
<div className="mb-8 text-center">
<Mail className="mx-auto h-12 w-12 text-[#053d57] mb-4" />
<h1 className="text-heading font-semibold text-[#053d57]">Email Account</h1>
<p className="text-muted-foreground mt-2">
Change your @{MAIL_DOMAIN} email password
</p>
</div>
{step === 'verify' && (
<Card>
<CardHeader>
<CardTitle>Verify Your Identity</CardTitle>
<CardDescription>
Enter your email address and current password to continue.
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleVerify} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email Address</Label>
<Input
id="email"
type="email"
placeholder={`yourname@${MAIL_DOMAIN}`}
value={email}
onChange={(e) => setEmail(e.target.value)}
required
autoComplete="email"
/>
</div>
<div className="space-y-2">
<Label htmlFor="current-password">Current Password</Label>
<div className="relative">
<Input
id="current-password"
type={showCurrentPassword ? 'text' : 'password'}
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
required
autoComplete="current-password"
/>
<button
type="button"
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
tabIndex={-1}
>
{showCurrentPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
</div>
{error && (
<div className="flex items-center gap-2 text-sm text-destructive">
<AlertCircle className="h-4 w-4 shrink-0" />
<span>{error}</span>
</div>
)}
<Button type="submit" className="w-full" disabled={loading}>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Verifying...
</>
) : (
'Continue'
)}
</Button>
</form>
</CardContent>
</Card>
)}
{step === 'change' && (
<Card>
<CardHeader>
<CardTitle>Set New Password</CardTitle>
<CardDescription>
Choose a new password for <strong>{email.toLowerCase().trim()}</strong>.
Must be at least 8 characters with uppercase, lowercase, and a number.
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleChangePassword} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="new-password">New Password</Label>
<div className="relative">
<Input
id="new-password"
type={showNewPassword ? 'text' : 'password'}
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
autoComplete="new-password"
minLength={8}
/>
<button
type="button"
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
onClick={() => setShowNewPassword(!showNewPassword)}
tabIndex={-1}
>
{showNewPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="confirm-password">Confirm New Password</Label>
<div className="relative">
<Input
id="confirm-password"
type={showConfirmPassword ? 'text' : 'password'}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
autoComplete="new-password"
minLength={8}
/>
<button
type="button"
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
tabIndex={-1}
>
{showConfirmPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
</div>
{error && (
<div className="flex items-center gap-2 text-sm text-destructive">
<AlertCircle className="h-4 w-4 shrink-0" />
<span>{error}</span>
</div>
)}
<div className="flex gap-3">
<Button
type="button"
variant="outline"
onClick={() => {
setStep('verify')
setNewPassword('')
setConfirmPassword('')
setError('')
}}
>
Back
</Button>
<Button type="submit" className="flex-1" disabled={loading}>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Changing Password...
</>
) : (
'Change Password'
)}
</Button>
</div>
</form>
</CardContent>
</Card>
)}
{step === 'success' && (
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="flex flex-col items-center text-center space-y-3">
<CheckCircle2 className="h-12 w-12 text-green-600" />
<h2 className="text-xl font-semibold">Password Changed Successfully</h2>
<p className="text-muted-foreground">
Your password for <strong>{email.toLowerCase().trim()}</strong> has been updated.
Use your new password to sign in to your email.
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Lock className="h-5 w-5" />
Mail Client Setup
</CardTitle>
<CardDescription>
Use these settings to add your email account to any mail app.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid gap-4 sm:grid-cols-2">
<div className="rounded-lg border p-4 space-y-3">
<h3 className="font-semibold text-sm uppercase tracking-wide text-muted-foreground">
Incoming Mail (IMAP)
</h3>
<dl className="space-y-2 text-sm">
<div>
<dt className="text-muted-foreground">Server</dt>
<dd className="font-mono font-medium">{MAIL_SERVER}</dd>
</div>
<div>
<dt className="text-muted-foreground">Port</dt>
<dd className="font-mono font-medium">993</dd>
</div>
<div>
<dt className="text-muted-foreground">Security</dt>
<dd className="font-mono font-medium">SSL/TLS</dd>
</div>
<div>
<dt className="text-muted-foreground">Username</dt>
<dd className="font-mono font-medium text-xs break-all">{email.toLowerCase().trim()}</dd>
</div>
</dl>
</div>
<div className="rounded-lg border p-4 space-y-3">
<h3 className="font-semibold text-sm uppercase tracking-wide text-muted-foreground">
Outgoing Mail (SMTP)
</h3>
<dl className="space-y-2 text-sm">
<div>
<dt className="text-muted-foreground">Server</dt>
<dd className="font-mono font-medium">{MAIL_SERVER}</dd>
</div>
<div>
<dt className="text-muted-foreground">Port</dt>
<dd className="font-mono font-medium">587</dd>
</div>
<div>
<dt className="text-muted-foreground">Security</dt>
<dd className="font-mono font-medium">STARTTLS</dd>
</div>
<div>
<dt className="text-muted-foreground">Username</dt>
<dd className="font-mono font-medium text-xs break-all">{email.toLowerCase().trim()}</dd>
</div>
</dl>
</div>
</div>
<div className="rounded-lg bg-muted/50 p-4 space-y-3">
<h3 className="font-semibold text-sm flex items-center gap-2">
<Smartphone className="h-4 w-4" />
Mobile Apps
</h3>
<ul className="text-sm space-y-1 text-muted-foreground">
<li><strong>iPhone/iPad:</strong> Settings &gt; Mail &gt; Accounts &gt; Add Account &gt; Other</li>
<li><strong>Gmail App:</strong> Settings &gt; Add Account &gt; Other</li>
<li><strong>Outlook App:</strong> Settings &gt; Add Email Account</li>
</ul>
</div>
<div className="rounded-lg bg-muted/50 p-4 space-y-3">
<h3 className="font-semibold text-sm flex items-center gap-2">
<Monitor className="h-4 w-4" />
Desktop Apps
</h3>
<ul className="text-sm space-y-1 text-muted-foreground">
<li><strong>Apple Mail:</strong> Mail &gt; Add Account &gt; Other Mail Account</li>
<li><strong>Outlook:</strong> File &gt; Add Account</li>
<li><strong>Thunderbird:</strong> Account Settings &gt; Account Actions &gt; Add Mail Account</li>
</ul>
</div>
</CardContent>
</Card>
</div>
)}
</div>
)
}