Initial BizOne portal setup
This commit is contained in:
177
frontend/src/components/reset-password-card.tsx
Normal file
177
frontend/src/components/reset-password-card.tsx
Normal file
@ -0,0 +1,177 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
type Props = {
|
||||
token: string;
|
||||
languageSwitcher: ReactNode;
|
||||
resetRequest: {
|
||||
email: string;
|
||||
name: string;
|
||||
expiresAt: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
function passwordChecks(password: string) {
|
||||
return {
|
||||
minLength: password.length >= 8,
|
||||
hasNumber: /\d/.test(password),
|
||||
hasSpecial: /[^A-Za-z0-9]/.test(password),
|
||||
};
|
||||
}
|
||||
|
||||
function formatExpiry(value: string | null) {
|
||||
if (!value) return 'Unavailable';
|
||||
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
}).format(new Date(value));
|
||||
}
|
||||
|
||||
export function ResetPasswordCard({ token, resetRequest, languageSwitcher }: Props) {
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [pending, setPending] = useState(false);
|
||||
|
||||
const checks = useMemo(() => passwordChecks(password), [password]);
|
||||
const strengthCount = [checks.minLength, checks.hasNumber, checks.hasSpecial].filter(Boolean).length;
|
||||
const strengthLabel = strengthCount <= 1 ? 'Weak' : strengthCount === 2 ? 'Medium' : 'Strong';
|
||||
|
||||
async function submit() {
|
||||
setError('');
|
||||
setMessage('');
|
||||
if (password !== confirmPassword) {
|
||||
setError('Password confirmation does not match.');
|
||||
return;
|
||||
}
|
||||
|
||||
setPending(true);
|
||||
try {
|
||||
const response = await fetch(`/api/auth/password-reset/${token}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password }),
|
||||
});
|
||||
const payload = await response.json();
|
||||
if (!response.ok) {
|
||||
setError(payload.message || 'Failed to reset password');
|
||||
return;
|
||||
}
|
||||
|
||||
setMessage('Password updated. You can now log in with your new password.');
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="auth-page auth-page-enterprise auth-page-login">
|
||||
<div className="auth-enterprise-glow auth-enterprise-glow-right" />
|
||||
<div className="auth-enterprise-glow auth-enterprise-glow-left" />
|
||||
<div className="auth-page-symbol auth-page-symbol-top">
|
||||
<span className="material-symbols-outlined">password</span>
|
||||
</div>
|
||||
<div className="auth-page-symbol auth-page-symbol-bottom">
|
||||
<span className="material-symbols-outlined">verified_user</span>
|
||||
</div>
|
||||
|
||||
<section className="auth-container auth-container-login">
|
||||
<div className="auth-login-toolbar">
|
||||
<div className="auth-login-locale">{languageSwitcher}</div>
|
||||
</div>
|
||||
|
||||
<header className="auth-brand">
|
||||
<div className="auth-brand-mark">
|
||||
<span className="material-symbols-outlined">vpn_key</span>
|
||||
</div>
|
||||
<div className="auth-brand-copy">
|
||||
<h1>BizOne</h1>
|
||||
<p>Create a new password and secure the admin account before returning to the dashboard.</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section className="auth-card auth-card-enterprise">
|
||||
<div className="two-factor-copy auth-public-copy">
|
||||
<h2>Reset Your Password</h2>
|
||||
<p>
|
||||
Hello, {resetRequest.name}. Choose a new password for <strong>{resetRequest.email}</strong>.
|
||||
</p>
|
||||
<p className="auth-public-meta">Reset link expires: {formatExpiry(resetRequest.expiresAt)}</p>
|
||||
</div>
|
||||
|
||||
<div className="invite-form">
|
||||
<label className="invite-field">
|
||||
<span>New Password</span>
|
||||
<div className="invite-password-wrap">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
placeholder="Enter your password"
|
||||
/>
|
||||
<button type="button" onClick={() => setShowPassword((current) => !current)}>
|
||||
<span className="material-symbols-outlined">{showPassword ? 'visibility_off' : 'visibility'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="invite-field">
|
||||
<span>Confirm New Password</span>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(event) => setConfirmPassword(event.target.value)}
|
||||
placeholder="Repeat your password"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="invite-strength">
|
||||
<div className="invite-strength-head">
|
||||
<span>Password Strength</span>
|
||||
<strong>{strengthLabel}</strong>
|
||||
</div>
|
||||
<div className="invite-strength-bars">
|
||||
{[0, 1, 2, 3].map((index) => (
|
||||
<span key={index} className={index < strengthCount + 1 ? 'is-active' : ''} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="invite-checklist">
|
||||
<div className={checks.minLength ? 'is-done' : ''}>
|
||||
<span className="material-symbols-outlined">{checks.minLength ? 'check_circle' : 'circle'}</span>
|
||||
Minimum 8 characters
|
||||
</div>
|
||||
<div className={checks.hasNumber ? 'is-done' : ''}>
|
||||
<span className="material-symbols-outlined">{checks.hasNumber ? 'check_circle' : 'circle'}</span>
|
||||
At least one number
|
||||
</div>
|
||||
<div className={checks.hasSpecial ? 'is-done' : ''}>
|
||||
<span className="material-symbols-outlined">{checks.hasSpecial ? 'check_circle' : 'circle'}</span>
|
||||
One special character (@, #, $, etc.)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" className="invite-submit-button" onClick={submit} disabled={pending}>
|
||||
{pending ? 'Updating...' : 'Update Password'}
|
||||
</button>
|
||||
|
||||
{error ? <p className="form-error">{error}</p> : null}
|
||||
{message ? <p className="form-success">{message}</p> : null}
|
||||
</div>
|
||||
|
||||
<div className="invite-footer auth-inline-footer">
|
||||
<span>Need help?</span>
|
||||
<Link href="/login">Back to login</Link>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user