diff options
| -rw-r--r-- | src/components/settings/ChangePassword.tsx | 91 | ||||
| -rw-r--r-- | src/components/settings/SecurityPage.tsx | 78 | ||||
| -rw-r--r-- | src/lib/api/ChangeEmail.tsx | 49 | ||||
| -rw-r--r-- | src/lib/api/ChangePassword.tsx | 54 | ||||
| -rw-r--r-- | src/lib/contexts/Auth.context.tsx | 2 |
5 files changed, 237 insertions, 37 deletions
diff --git a/src/components/settings/ChangePassword.tsx b/src/components/settings/ChangePassword.tsx new file mode 100644 index 0000000..d437b5e --- /dev/null +++ b/src/components/settings/ChangePassword.tsx @@ -0,0 +1,91 @@ +'use client'; + +import { useState } from 'react'; +import { InputField } from '@/components/ui/inputfield'; +import { Button } from '@/components/ui/button'; +import { Separator } from '@/components/ui/separator'; +import { changePassword } from '@/lib/api/ChangePassword'; + +export const ChangePasswordField = () => { + 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 handleSubmit = async () => { + setError(null); + + if (newPassword !== confirmPassword) { + setError('Пароли не совпадают'); + return; + } + + if (newPassword.length < 6) { + setError('Пароль должен быть минимум 6 символов'); + return; + } + + setLoading(true); + + const res = await changePassword( + currentPassword, + newPassword, + confirmPassword, + ); + + setLoading(false); + + if (res.error) { + setError(res.error.general); + return; + } + + setCurrentPassword(''); + setNewPassword(''); + setConfirmPassword(''); + }; + + return ( + <div className="flex flex-col gap-[20px] w-[310px]"> + <p className="text-light-violet font-medium">СМЕНА ПАРОЛЯ</p> + + <div className="flex flex-col gap-[10px]"> + <InputField + placeholder="Текущий пароль" + isPassword + type="password" + name="currentPassword" + value={currentPassword} + onChange={(e: any) => setCurrentPassword(e.target.value)} + /> + + <InputField + placeholder="Новый пароль" + isPassword + type="password" + name="newPassword" + value={newPassword} + onChange={(e: any) => setNewPassword(e.target.value)} + /> + + <InputField + placeholder="Повторите пароль" + isPassword + type="password" + name="confirmPassword" + value={confirmPassword} + onChange={(e: any) => setConfirmPassword(e.target.value)} + /> + </div> + + {error && <p className="text-red text-sm">{error}</p>} + + <Button onClick={handleSubmit} disabled={loading}> + {loading ? 'Смена...' : 'Сменить'} + </Button> + + <Separator className="bg-violet/30 h-[1px]" /> + </div> + ); +}; diff --git a/src/components/settings/SecurityPage.tsx b/src/components/settings/SecurityPage.tsx index b5f38a9..dd650c4 100644 --- a/src/components/settings/SecurityPage.tsx +++ b/src/components/settings/SecurityPage.tsx @@ -3,14 +3,41 @@ import { Button } from '@/components/ui'; import { Separator } from '@/components/ui'; import { useAuthContext } from '@/lib/contexts/Auth.context'; import LogoutButton from './LogoutButton'; +import { changeEmail } from '@/lib/api/ChangeEmail'; +import { useState } from 'react'; +import { ChangePasswordField } from './ChangePassword'; export default function SecurityPage() { const { user } = useAuthContext(); if (!user) return; + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(false); + + if (!user) return null; + const hasGoogle = !!user.google_id; - const hasPassword = !!user.password; + const hasPassword = user.has_password; const showSetPassword = hasGoogle && !hasPassword; + + const handleChangeEmail = async () => { + if (!email || !password) return; + + setLoading(true); + + const res = await changeEmail(email, password); + + setLoading(false); + + if (res.error) { + console.error(res.error.general); + return; + } + + setEmail(''); + setPassword(''); + }; return ( <> <div className="flex flex-col flex-start gap-[40px] w-[900px] h-[816px]"> @@ -38,55 +65,34 @@ export default function SecurityPage() { </div> )} - <div className="flex flex-col flex-start gap-[20px] w-[310px] h-[192px]"> + <div className="flex flex-col gap-[20px] w-[310px]"> <p className="text-light-violet font-medium"> СМЕНА ПОЧТЫ </p> - <div className="flex flex-col flex-start gap-[10px] w-[310px] h-[137px]"> + + <div className="flex flex-col gap-[10px]"> <InputField - placeholder="Почта аккаунта" - type="password" - name="password" + placeholder="Новая почта" + type="email" + name="email" + value={email} + onChange={(e) => setEmail(e.target.value)} /> + <InputField isPassword placeholder="Введите пароль" type="password" name="password" + value={password} + onChange={(e) => setPassword(e.target.value)} /> </div> - <Button>Сменить</Button> + + <Button onClick={handleChangeEmail}>Сменить</Button> </div> <Separator className="bg-violet/30 h-[1px]" /> - {hasPassword && ( - <div className="flex flex-col flex-start gap-[20px] w-[310px] "> - <p className="text-light-violet font-medium"> - СМЕНА ПАРОЛЯ - </p> - <div className="flex flex-col flex-start gap-[10px] w-[310px]"> - <InputField - placeholder="Текущий пароль" - isPassword - type="password" - name="password" - /> - <InputField - isPassword - placeholder="Введите пароль" - type="password" - name="password" - /> - <InputField - isPassword - placeholder="Повторите пароль" - type="password" - name="password" - /> - </div> - <Button>Сменить</Button> - <Separator className="bg-violet/30 h-[1px]" /> - </div> - )} + {hasPassword && <ChangePasswordField />} <LogoutButton /> </div> </div> diff --git a/src/lib/api/ChangeEmail.tsx b/src/lib/api/ChangeEmail.tsx new file mode 100644 index 0000000..13d462c --- /dev/null +++ b/src/lib/api/ChangeEmail.tsx @@ -0,0 +1,49 @@ +const API_URL = process.env.NEXT_PUBLIC_API_URL; + +type ChangeEmailResponse = + | { data: { email: string }; error: null } + | { data: null; error: { general: string } }; + +export const changeEmail = async ( + email: string, + password: string, +): Promise<ChangeEmailResponse> => { + try { + const res = await fetch(`${API_URL}/api/users/email`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ email, password }), + }); + + const data = await res.json().catch(() => null); + + if (!res.ok) { + const detail = data?.detail; + + return { + data: null, + error: { + general: + typeof detail === 'string' + ? detail + : detail?.msg || 'Ошибка смены почты', + }, + }; + } + + return { + data, + error: null, + }; + } catch (err: any) { + return { + data: null, + error: { + general: err?.message || 'Network error', + }, + }; + } +}; diff --git a/src/lib/api/ChangePassword.tsx b/src/lib/api/ChangePassword.tsx new file mode 100644 index 0000000..93900b9 --- /dev/null +++ b/src/lib/api/ChangePassword.tsx @@ -0,0 +1,54 @@ +const API_URL = process.env.NEXT_PUBLIC_API_URL; + +export type ChangePasswordResponse = + | { data: { success: true }; error: null } + | { data: null; error: { general: string } }; + +export const changePassword = async ( + currentPassword: string, + newPassword: string, + repeatPassword: string, +): Promise<ChangePasswordResponse> => { + try { + const res = await fetch(`${API_URL}/api/users/password`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ + current_password: currentPassword, + new_password: newPassword, + repeat_password: repeatPassword, + }), + }); + + const data = await res.json().catch(() => null); + + if (!res.ok) { + const detail = data?.detail; + + return { + data: null, + error: { + general: + typeof detail === 'string' + ? detail + : detail?.msg || 'Ошибка смены пароля', + }, + }; + } + + return { + data: { success: true }, + error: null, + }; + } catch (err: any) { + return { + data: null, + error: { + general: err?.message || 'Network error', + }, + }; + } +}; diff --git a/src/lib/contexts/Auth.context.tsx b/src/lib/contexts/Auth.context.tsx index 127c942..e5ed643 100644 --- a/src/lib/contexts/Auth.context.tsx +++ b/src/lib/contexts/Auth.context.tsx @@ -5,7 +5,7 @@ export type User = { id: number; username: string; email: string; - password: boolean; + has_password: boolean; google_id?: string | null; avatar?: string; banner_file?: string; |
