- Created ResetPassword.tsx with: - Password reset form with strength indicator - Key validation on load - Show/hide password toggle - Success/error states - Redirect to login on success - Updated EmailManager.php: - Changed reset_link from wp-login.php to SPA route - Format: /wp-admin/admin.php?page=woonoow#/reset-password?key=KEY&login=LOGIN - Added AuthController API methods: - validate_reset_key: Validates reset key before showing form - reset_password: Performs actual password reset - Registered new REST routes in Routes.php: - POST /auth/validate-reset-key - POST /auth/reset-password Password reset emails now link to the SPA instead of native WordPress.
256 lines
11 KiB
TypeScript
256 lines
11 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useSearchParams, useNavigate } from 'react-router-dom';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
|
import { Loader2, CheckCircle, AlertCircle, Eye, EyeOff, Lock } from 'lucide-react';
|
|
import { __ } from '@/lib/i18n';
|
|
|
|
export default function ResetPassword() {
|
|
const [searchParams] = useSearchParams();
|
|
const navigate = useNavigate();
|
|
|
|
const key = searchParams.get('key') || '';
|
|
const login = searchParams.get('login') || '';
|
|
|
|
const [password, setPassword] = useState('');
|
|
const [confirmPassword, setConfirmPassword] = useState('');
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [isValidating, setIsValidating] = useState(true);
|
|
const [isValid, setIsValid] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [success, setSuccess] = useState(false);
|
|
|
|
// Validate the reset key on mount
|
|
useEffect(() => {
|
|
const validateKey = async () => {
|
|
if (!key || !login) {
|
|
setError(__('Invalid password reset link. Please request a new one.'));
|
|
setIsValidating(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${window.WNW_CONFIG?.restUrl || '/wp-json/'}woonoow/v1/auth/validate-reset-key`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ key, login }),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.valid) {
|
|
setIsValid(true);
|
|
} else {
|
|
setError(data.message || __('This password reset link has expired or is invalid. Please request a new one.'));
|
|
}
|
|
} catch (err) {
|
|
setError(__('Unable to validate reset link. Please try again later.'));
|
|
} finally {
|
|
setIsValidating(false);
|
|
}
|
|
};
|
|
|
|
validateKey();
|
|
}, [key, login]);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
|
|
// Validate passwords match
|
|
if (password !== confirmPassword) {
|
|
setError(__('Passwords do not match'));
|
|
return;
|
|
}
|
|
|
|
// Validate password strength
|
|
if (password.length < 8) {
|
|
setError(__('Password must be at least 8 characters long'));
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const response = await fetch(`${window.WNW_CONFIG?.restUrl || '/wp-json/'}woonoow/v1/auth/reset-password`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ key, login, password }),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
setSuccess(true);
|
|
} else {
|
|
setError(data.message || __('Failed to reset password. Please try again.'));
|
|
}
|
|
} catch (err) {
|
|
setError(__('An error occurred. Please try again later.'));
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
// Password strength indicator
|
|
const getPasswordStrength = (pwd: string) => {
|
|
if (pwd.length === 0) return { label: '', color: '' };
|
|
if (pwd.length < 8) return { label: __('Too short'), color: 'text-red-500' };
|
|
|
|
let strength = 0;
|
|
if (pwd.length >= 8) strength++;
|
|
if (pwd.length >= 12) strength++;
|
|
if (/[a-z]/.test(pwd) && /[A-Z]/.test(pwd)) strength++;
|
|
if (/\d/.test(pwd)) strength++;
|
|
if (/[!@#$%^&*(),.?":{}|<>]/.test(pwd)) strength++;
|
|
|
|
if (strength <= 2) return { label: __('Weak'), color: 'text-orange-500' };
|
|
if (strength <= 3) return { label: __('Medium'), color: 'text-yellow-500' };
|
|
return { label: __('Strong'), color: 'text-green-500' };
|
|
};
|
|
|
|
const passwordStrength = getPasswordStrength(password);
|
|
|
|
// Loading state
|
|
if (isValidating) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 p-4">
|
|
<Card className="w-full max-w-md">
|
|
<CardContent className="flex flex-col items-center py-8">
|
|
<Loader2 className="h-8 w-8 animate-spin text-primary mb-4" />
|
|
<p className="text-muted-foreground">{__('Validating reset link...')}</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Success state
|
|
if (success) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 p-4">
|
|
<Card className="w-full max-w-md">
|
|
<CardContent className="flex flex-col items-center py-8">
|
|
<CheckCircle className="h-12 w-12 text-green-500 mb-4" />
|
|
<h2 className="text-xl font-semibold mb-2">{__('Password Reset Successful')}</h2>
|
|
<p className="text-muted-foreground text-center mb-6">
|
|
{__('Your password has been updated. You can now log in with your new password.')}
|
|
</p>
|
|
<Button onClick={() => navigate('/login')}>
|
|
{__('Go to Login')}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Error state (invalid key)
|
|
if (!isValid && error) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 p-4">
|
|
<Card className="w-full max-w-md">
|
|
<CardContent className="flex flex-col items-center py-8">
|
|
<AlertCircle className="h-12 w-12 text-red-500 mb-4" />
|
|
<h2 className="text-xl font-semibold mb-2">{__('Invalid Reset Link')}</h2>
|
|
<p className="text-muted-foreground text-center mb-6">{error}</p>
|
|
<Button variant="outline" onClick={() => window.location.href = window.WNW_CONFIG?.siteUrl + '/my-account/lost-password/'}>
|
|
{__('Request New Reset Link')}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Reset form
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 p-4">
|
|
<Card className="w-full max-w-md">
|
|
<CardHeader className="space-y-1">
|
|
<div className="flex justify-center mb-4">
|
|
<div className="rounded-full bg-primary/10 p-3">
|
|
<Lock className="h-6 w-6 text-primary" />
|
|
</div>
|
|
</div>
|
|
<CardTitle className="text-2xl text-center">{__('Reset Your Password')}</CardTitle>
|
|
<CardDescription className="text-center">
|
|
{__('Enter your new password below')}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
{error && (
|
|
<Alert variant="destructive">
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertDescription>{error}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="password">{__('New Password')}</Label>
|
|
<div className="relative">
|
|
<Input
|
|
id="password"
|
|
type={showPassword ? 'text' : 'password'}
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder={__('Enter new password')}
|
|
required
|
|
className="pr-10"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
|
>
|
|
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
</button>
|
|
</div>
|
|
{password && (
|
|
<p className={`text-sm ${passwordStrength.color}`}>
|
|
{__('Strength')}: {passwordStrength.label}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="confirmPassword">{__('Confirm Password')}</Label>
|
|
<Input
|
|
id="confirmPassword"
|
|
type={showPassword ? 'text' : 'password'}
|
|
value={confirmPassword}
|
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
placeholder={__('Confirm new password')}
|
|
required
|
|
/>
|
|
{confirmPassword && password !== confirmPassword && (
|
|
<p className="text-sm text-red-500">{__('Passwords do not match')}</p>
|
|
)}
|
|
</div>
|
|
|
|
<Button type="submit" className="w-full" disabled={isLoading}>
|
|
{isLoading ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
{__('Resetting...')}
|
|
</>
|
|
) : (
|
|
__('Reset Password')
|
|
)}
|
|
</Button>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|