Files
SRCmail/app/admin/change-password/page.tsx
T
Linus Rath 76b21147e4 feat: add plugin/theme harness and admin dashboard
Plugin & Theme System:
- Add plugin type definitions, permissions (30+), and validation constants
- Add IndexedDB storage layer for plugin code, theme CSS, and previews
- Add theme CSS sanitization, injection, and safety validation
- Add HookBus event system with 130+ hooks across 20 domains
- Add plugin ZIP extraction and manifest validation with JS security checks
- Add sandboxed PluginAPI factory with scoped storage, logging, and permission gating
- Add plugin loader with blob URL dynamic import and auto-disable circuit breaker
- Add 3 built-in themes (Nord, Catppuccin, Solarized)
- Add Zustand plugin store with install/uninstall/enable/disable lifecycle
- Add PluginSlot, PluginSlotRenderer, and PluginErrorBoundary components
- Add plugins and themes settings UI panels
- Integrate plugin slots into email viewer, composer, navigation rail, sidebar, and context menu
- Extend theme store with custom theme installation and activation

Admin Dashboard:
- Add admin authentication with scrypt password hashing and AES-256-GCM sessions
- Add rate-limited login (5 attempts/15min per IP)
- Add config manager with admin override > env var > default priority
- Add settings policy system with feature gates and per-setting restrictions
- Add audit logging with rotation
- Add admin API routes (login, logout, config, policy, audit, password change)
- Add admin UI pages (login, dashboard, config, policy, audit)
- Add policy store for client-side feature gate enforcement
- Wire admin password initialization into server instrumentation

Tests:
- Add 139 tests across 10 test files covering all plugin/theme modules
2026-03-25 00:44:03 +01:00

117 lines
4.1 KiB
TypeScript

'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Lock } from 'lucide-react';
export default function ChangePasswordPage() {
const router = useRouter();
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState('');
const [success, setSuccess] = useState(false);
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError('');
setSuccess(false);
if (newPassword.length < 8) {
setError('New password must be at least 8 characters.');
return;
}
if (newPassword !== confirmPassword) {
setError('New passwords do not match.');
return;
}
setLoading(true);
const res = await fetch('/api/admin/change-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ currentPassword, newPassword }),
});
if (res.ok) {
setSuccess(true);
setCurrentPassword('');
setNewPassword('');
setConfirmPassword('');
setTimeout(() => router.push('/admin'), 2000);
} else {
const data = await res.json().catch(() => ({}));
setError(data.error || 'Failed to change password.');
}
setLoading(false);
}
return (
<div className="max-w-md mx-auto space-y-6">
<div>
<h1 className="text-2xl font-semibold text-foreground">Change Password</h1>
<p className="text-sm text-muted-foreground mt-1">Update your admin password.</p>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-1.5">
<label className="text-sm font-medium text-foreground">Current Password</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<input
type="password"
value={currentPassword}
onChange={e => setCurrentPassword(e.target.value)}
required
className="w-full h-9 pl-9 pr-3 rounded-md border border-input bg-background text-sm text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
autoComplete="current-password"
/>
</div>
</div>
<div className="space-y-1.5">
<label className="text-sm font-medium text-foreground">New Password</label>
<input
type="password"
value={newPassword}
onChange={e => setNewPassword(e.target.value)}
required
minLength={8}
className="w-full h-9 px-3 rounded-md border border-input bg-background text-sm text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
autoComplete="new-password"
/>
</div>
<div className="space-y-1.5">
<label className="text-sm font-medium text-foreground">Confirm New Password</label>
<input
type="password"
value={confirmPassword}
onChange={e => setConfirmPassword(e.target.value)}
required
minLength={8}
className="w-full h-9 px-3 rounded-md border border-input bg-background text-sm text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
autoComplete="new-password"
/>
</div>
{error && (
<p className="text-sm text-red-500">{error}</p>
)}
{success && (
<p className="text-sm text-green-600">Password changed. Redirecting...</p>
)}
<button
type="submit"
disabled={loading}
className="w-full h-9 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 transition-colors disabled:opacity-50"
>
{loading ? 'Changing...' : 'Change Password'}
</button>
</form>
</div>
);
}