mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +00:00
Add Audible region config and user password change modal
Implements configurable Audible region selection in setup and admin settings, affecting all Audible API calls and triggering data refresh on change. Adds a user-facing 'Change Password' modal in the header for local users, moving password change from admin-only to all local users via a new /api/auth/change-password endpoint. Updates documentation, API routes, and context to support these features, and removes the old admin-only password change flow.
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* Component: Change Password Modal
|
||||
* Documentation: documentation/frontend/components.md
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Modal } from './Modal';
|
||||
import { Input } from './Input';
|
||||
import { Button } from './Button';
|
||||
|
||||
interface ChangePasswordModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function ChangePasswordModal({ isOpen, onClose }: ChangePasswordModalProps) {
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
// Validation errors for individual fields
|
||||
const [errors, setErrors] = useState({
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors = {
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
};
|
||||
|
||||
let isValid = true;
|
||||
|
||||
if (!currentPassword) {
|
||||
newErrors.currentPassword = 'Current password is required';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!newPassword) {
|
||||
newErrors.newPassword = 'New password is required';
|
||||
isValid = false;
|
||||
} else if (newPassword.length < 8) {
|
||||
newErrors.newPassword = 'Password must be at least 8 characters';
|
||||
isValid = false;
|
||||
} else if (newPassword === currentPassword) {
|
||||
newErrors.newPassword = 'New password must be different from current password';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!confirmPassword) {
|
||||
newErrors.confirmPassword = 'Please confirm your new password';
|
||||
isValid = false;
|
||||
} else if (newPassword !== confirmPassword) {
|
||||
newErrors.confirmPassword = 'Passwords do not match';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Clear previous error and success states
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
// Validate form
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const accessToken = localStorage.getItem('accessToken');
|
||||
if (!accessToken) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const response = await fetch('/api/auth/change-password', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
currentPassword,
|
||||
newPassword,
|
||||
confirmPassword,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
setSuccess(true);
|
||||
// Clear form
|
||||
setCurrentPassword('');
|
||||
setNewPassword('');
|
||||
setConfirmPassword('');
|
||||
setErrors({ currentPassword: '', newPassword: '', confirmPassword: '' });
|
||||
|
||||
// Auto-close after 2 seconds
|
||||
setTimeout(() => {
|
||||
setSuccess(false);
|
||||
onClose();
|
||||
}, 2000);
|
||||
} else {
|
||||
setError(data.error || 'Failed to change password');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to change password');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
// Reset form state when closing
|
||||
setCurrentPassword('');
|
||||
setNewPassword('');
|
||||
setConfirmPassword('');
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
setErrors({ currentPassword: '', newPassword: '', confirmPassword: '' });
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={handleClose} title="Change Password" size="sm">
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Success message */}
|
||||
{success && (
|
||||
<div className="p-4 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-800 rounded-lg">
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 text-green-600 dark:text-green-400 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
<p className="text-sm text-green-800 dark:text-green-200">
|
||||
Password changed successfully!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error message */}
|
||||
{error && (
|
||||
<div className="p-4 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 text-red-600 dark:text-red-400 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<p className="text-sm text-red-800 dark:text-red-200">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Current Password */}
|
||||
<Input
|
||||
type="password"
|
||||
label="Current Password"
|
||||
value={currentPassword}
|
||||
onChange={(e) => {
|
||||
setCurrentPassword(e.target.value);
|
||||
setErrors({ ...errors, currentPassword: '' });
|
||||
}}
|
||||
placeholder="Enter your current password"
|
||||
autoComplete="current-password"
|
||||
error={errors.currentPassword}
|
||||
disabled={loading || success}
|
||||
/>
|
||||
|
||||
{/* New Password */}
|
||||
<Input
|
||||
type="password"
|
||||
label="New Password"
|
||||
value={newPassword}
|
||||
onChange={(e) => {
|
||||
setNewPassword(e.target.value);
|
||||
setErrors({ ...errors, newPassword: '' });
|
||||
}}
|
||||
placeholder="Enter your new password"
|
||||
autoComplete="new-password"
|
||||
helperText="Must be at least 8 characters"
|
||||
error={errors.newPassword}
|
||||
disabled={loading || success}
|
||||
/>
|
||||
|
||||
{/* Confirm New Password */}
|
||||
<Input
|
||||
type="password"
|
||||
label="Confirm New Password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => {
|
||||
setConfirmPassword(e.target.value);
|
||||
setErrors({ ...errors, confirmPassword: '' });
|
||||
}}
|
||||
placeholder="Confirm your new password"
|
||||
autoComplete="new-password"
|
||||
error={errors.confirmPassword}
|
||||
disabled={loading || success}
|
||||
/>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-end gap-3 pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
disabled={loading || success}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
loading={loading}
|
||||
disabled={loading || success}
|
||||
>
|
||||
Change Password
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user