feat: SPA-based password reset page
- 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.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { HashRouter, Routes, Route, NavLink, useLocation, useParams, Navigate, Link } from 'react-router-dom';
|
||||
import { Login } from './routes/Login';
|
||||
import ResetPassword from './routes/ResetPassword';
|
||||
import Dashboard from '@/routes/Dashboard';
|
||||
import DashboardRevenue from '@/routes/Dashboard/Revenue';
|
||||
import DashboardOrders from '@/routes/Dashboard/Orders';
|
||||
@@ -501,6 +502,7 @@ function AppRoutes() {
|
||||
<Routes>
|
||||
{/* Dashboard */}
|
||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
<Route path="/reset-password" element={<ResetPassword />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/dashboard/revenue" element={<DashboardRevenue />} />
|
||||
<Route path="/dashboard/orders" element={<DashboardOrders />} />
|
||||
|
||||
255
admin-spa/src/routes/ResetPassword.tsx
Normal file
255
admin-spa/src/routes/ResetPassword.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user