внедрена защищенная смена пароля (бэк + фронт)
This commit is contained in:
@@ -62,6 +62,13 @@ export class AuthApi {
|
||||
return httpClient.request<any>('/config');
|
||||
}
|
||||
|
||||
static async changePassword(data: { oldPassword: string; newPassword: string; confirmPassword: string }) {
|
||||
return httpClient.request<void>('/auth/change-password', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
static setToken(token: string | null) {
|
||||
httpClient.setToken(token);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { Loader2, X } from 'lucide-react';
|
||||
import { AuthApi } from '../../../auth/infrastructure/authApi';
|
||||
import { useLang } from '../../../../core/infrastructure/i18n';
|
||||
|
||||
interface ChangePasswordModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
export default function ChangePasswordModal({ open, onClose, onSuccess }: ChangePasswordModalProps) {
|
||||
const { lang } = useLang();
|
||||
const [oldPassword, setOldPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const minLengthOk = newPassword.length >= 8;
|
||||
const matchOk = newPassword.length > 0 && confirmPassword.length > 0 && newPassword === confirmPassword;
|
||||
const canSubmit = useMemo(
|
||||
() => oldPassword.length > 0 && minLengthOk && matchOk && !loading,
|
||||
[oldPassword.length, minLengthOk, matchOk, loading]
|
||||
);
|
||||
|
||||
const resetState = () => {
|
||||
setOldPassword('');
|
||||
setNewPassword('');
|
||||
setConfirmPassword('');
|
||||
setError(null);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
resetState();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
if (!canSubmit) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await AuthApi.changePassword({ oldPassword, newPassword, confirmPassword });
|
||||
resetState();
|
||||
onSuccess();
|
||||
} catch (e: any) {
|
||||
const message = e?.message || (lang === 'ru' ? 'Не удалось сменить пароль' : 'Failed to change password');
|
||||
setError(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-[12000] flex items-center justify-center p-4"
|
||||
>
|
||||
<div className="absolute inset-0 bg-black/70 backdrop-blur-sm" onClick={close} />
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 12, scale: 0.98 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="relative w-full max-w-md rounded-[2rem] border border-white/10 bg-[#111111] p-6 shadow-2xl"
|
||||
>
|
||||
<button
|
||||
onClick={close}
|
||||
className="absolute right-4 top-4 p-2 rounded-xl text-white/50 hover:text-white hover:bg-white/10"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
|
||||
<h3 className="text-lg font-black text-white mb-5">
|
||||
{lang === 'ru' ? 'Смена пароля' : 'Change password'}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
type="password"
|
||||
value={oldPassword}
|
||||
onChange={(e) => setOldPassword(e.target.value)}
|
||||
placeholder={lang === 'ru' ? 'Текущий пароль' : 'Current password'}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-2xl px-4 py-3 text-sm text-white outline-none focus:border-primary/60"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
placeholder={lang === 'ru' ? 'Новый пароль' : 'New password'}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-2xl px-4 py-3 text-sm text-white outline-none focus:border-primary/60"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
placeholder={lang === 'ru' ? 'Подтвердите пароль' : 'Confirm password'}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-2xl px-4 py-3 text-sm text-white outline-none focus:border-primary/60"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 space-y-1">
|
||||
<p className={`text-xs ${minLengthOk ? 'text-emerald-400' : 'text-white/45'}`}>
|
||||
{lang === 'ru' ? 'Минимум 8 символов' : 'At least 8 characters'}
|
||||
</p>
|
||||
<p className={`text-xs ${matchOk ? 'text-emerald-400' : 'text-white/45'}`}>
|
||||
{lang === 'ru' ? 'Пароли совпадают' : 'Passwords match'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && <p className="mt-3 text-sm text-red-400">{error}</p>}
|
||||
|
||||
<div className="mt-6 flex items-center justify-end gap-2">
|
||||
<button
|
||||
onClick={close}
|
||||
className="px-4 py-2 rounded-xl text-xs font-black uppercase tracking-wider text-white/60 hover:text-white"
|
||||
>
|
||||
{lang === 'ru' ? 'Отмена' : 'Cancel'}
|
||||
</button>
|
||||
<button
|
||||
onClick={submit}
|
||||
disabled={!canSubmit}
|
||||
className="px-5 py-2 rounded-xl bg-primary text-on-primary text-xs font-black uppercase tracking-wider disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? <Loader2 size={14} className="animate-spin" /> : (lang === 'ru' ? 'Сменить' : 'Change')}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import AvatarCropModal from './AvatarCropModal';
|
||||
import { Area } from 'react-easy-crop';
|
||||
import { getCroppedImg } from '../../../../core/utils/cropImage';
|
||||
import EmojiOnlyPicker from '../../../stories/presentation/components/EmojiOnlyPicker';
|
||||
import ChangePasswordModal from './ChangePasswordModal';
|
||||
|
||||
const BIO_MAX_LENGTH = 200;
|
||||
const STATUS_MAX_LENGTH = 50;
|
||||
@@ -40,6 +41,7 @@ export default function SettingsPage() {
|
||||
});
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [showStatusPicker, setShowStatusPicker] = useState(false);
|
||||
const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
|
||||
|
||||
// Avatar cropping state
|
||||
const [cropModal, setCropModal] = useState<{
|
||||
@@ -410,6 +412,20 @@ export default function SettingsPage() {
|
||||
|
||||
{/* Account Section */}
|
||||
<div className="p-6 rounded-3xl bg-white/[0.02] border border-white/5 flex flex-col justify-between">
|
||||
<button
|
||||
onClick={() => setShowChangePasswordModal(true)}
|
||||
className="w-full mb-3 flex items-center justify-between p-4 rounded-2xl bg-white/5 hover:bg-white/10 border border-white/10 transition-all group"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-white/10 flex items-center justify-center text-primary group-hover:scale-105 transition-transform">
|
||||
<Shield size={16} />
|
||||
</div>
|
||||
<span className="text-[11px] font-black uppercase tracking-widest text-white/70 group-hover:text-white transition-colors">
|
||||
{lang === 'ru' ? 'Сменить пароль' : 'Change password'}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronRight size={16} className="text-white/30" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { logout(); }}
|
||||
className="w-full flex items-center justify-between p-4 rounded-2xl bg-red-500/5 hover:bg-red-500/15 border border-red-500/10 transition-all group"
|
||||
@@ -447,6 +463,15 @@ export default function SettingsPage() {
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<ChangePasswordModal
|
||||
open={showChangePasswordModal}
|
||||
onClose={() => setShowChangePasswordModal(false)}
|
||||
onSuccess={() => {
|
||||
setShowChangePasswordModal(false);
|
||||
// Access token remains valid; ask user to re-login to ensure security on all devices.
|
||||
logout();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user