feat: login with username or email + change password
Backend: - Login handler accepts username OR email via the same field - New PUT /auth/me/password endpoint (verifies current password, requires min 8 chars for new password) Frontend: - Login form shows EMAIL OR USERNAME field (single field for both) - Auth store updated with changePassword function - UserSettings has new CHANGE PASSWORD section
This commit is contained in:
@@ -4,7 +4,7 @@ import { useAuthStore } from '../stores/auth.ts';
|
||||
import { usePushStore } from '../stores/push.ts';
|
||||
|
||||
export function UserSettings() {
|
||||
const { user, updateProfile, isLoading, error, clearError, fetchMe } = useAuthStore();
|
||||
const { user, updateProfile, changePassword, isLoading, error, clearError, fetchMe } = useAuthStore();
|
||||
const { isSupported, isSubscribed, subscribe, unsubscribe, checkSubscription } = usePushStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -17,6 +17,14 @@ export function UserSettings() {
|
||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Password change state
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmNewPassword, setConfirmNewPassword] = useState('');
|
||||
const [passwordLoading, setPasswordLoading] = useState(false);
|
||||
const [passwordError, setPasswordError] = useState<string | null>(null);
|
||||
const [passwordSuccess, setPasswordSuccess] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setDisplayName(user.display_name || '');
|
||||
@@ -86,6 +94,32 @@ export function UserSettings() {
|
||||
|
||||
const hexValid = /^#[0-9a-fA-F]{6}$/.test(accentColor);
|
||||
|
||||
const handleChangePassword = async () => {
|
||||
setPasswordError(null);
|
||||
setPasswordSuccess(false);
|
||||
if (newPassword !== confirmNewPassword) {
|
||||
setPasswordError("new passwords do not match");
|
||||
return;
|
||||
}
|
||||
if (newPassword.length < 8) {
|
||||
setPasswordError("password must be at least 8 characters");
|
||||
return;
|
||||
}
|
||||
setPasswordLoading(true);
|
||||
try {
|
||||
await changePassword(currentPassword, newPassword);
|
||||
setPasswordSuccess(true);
|
||||
setCurrentPassword('');
|
||||
setNewPassword('');
|
||||
setConfirmNewPassword('');
|
||||
setTimeout(() => setPasswordSuccess(false), 3000);
|
||||
} catch {
|
||||
// error is set in store
|
||||
} finally {
|
||||
setPasswordLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-full bg-gb-bg p-4 md:p-8 overflow-y-auto">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
@@ -234,6 +268,53 @@ export function UserSettings() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Change Password */}
|
||||
<div className="border border-gb-bg-t p-4">
|
||||
<label className="block text-gb-fg-f mb-2 font-mono text-sm">
|
||||
CHANGE PASSWORD:
|
||||
</label>
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
className="terminal-input w-full"
|
||||
placeholder="current password"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
className="terminal-input w-full"
|
||||
placeholder="new password (min 8 chars)"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmNewPassword}
|
||||
onChange={(e) => setConfirmNewPassword(e.target.value)}
|
||||
className="terminal-input w-full"
|
||||
placeholder="confirm new password"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
{passwordError && (
|
||||
<p className="text-gb-red text-xs font-mono">ERR: {passwordError}</p>
|
||||
)}
|
||||
{passwordSuccess && (
|
||||
<p className="text-gb-green text-xs font-mono">[password changed!]</p>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleChangePassword}
|
||||
disabled={passwordLoading || !currentPassword || newPassword.length < 8 || newPassword !== confirmNewPassword}
|
||||
className="terminal-button text-xs"
|
||||
>
|
||||
{passwordLoading ? '[CHANGING...]' : '[CHANGE PASSWORD]'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Push Notifications */}
|
||||
{isSupported && (
|
||||
<div className="border border-gb-bg-t p-4">
|
||||
|
||||
Reference in New Issue
Block a user