summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorl3wdfut4pwr <l3wdfut4pwr@gmail.com>2026-03-31 13:52:16 +0300
committerl3wdfut4pwr <l3wdfut4pwr@gmail.com>2026-03-31 13:52:16 +0300
commit37f51ee88710868a77b4645294cf32862f55e7c4 (patch)
treeac399bde1d35b6a52dd7421f68dd9567f7a71961
parentcdc736b4823a3dda97f2b49c04b8ef7321e7a254 (diff)
add jwt
-rw-r--r--src/components/header/authdialog/LoginForm.tsx117
-rw-r--r--src/components/header/authdialog/RegisterForm.tsx1
-rw-r--r--src/components/header/authdialog/register.ts76
-rw-r--r--src/lib/contexts/Auth.context.tsx29
-rw-r--r--src/lib/contexts/Global.context.tsx2
5 files changed, 184 insertions, 41 deletions
diff --git a/src/components/header/authdialog/LoginForm.tsx b/src/components/header/authdialog/LoginForm.tsx
index 0a72f67..ceadbcd 100644
--- a/src/components/header/authdialog/LoginForm.tsx
+++ b/src/components/header/authdialog/LoginForm.tsx
@@ -1,23 +1,112 @@
+'use client';
+import { useState } from 'react';
import { Button } from '@/components/ui/button';
import GoogleIcon from '../../../../public/icons/google.svg';
import { InputField } from '@/components/ui/inputfield';
-import { Input } from '@/components/ui/input';
+import { useAuthContext } from '@/lib/contexts/Auth.context';
+
export default function LoginForm() {
+ const [errors, setErrors] = useState<any>({});
+ const [loading, setLoading] = useState(false);
+ const { setUser } = useAuthContext();
+
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
+ e.preventDefault();
+ setLoading(true);
+ setErrors({});
+
+ const formData = new FormData(e.currentTarget);
+ const emailOrUsername =
+ formData.get('emailOrUsername')?.toString() || '';
+ const password = formData.get('password')?.toString() || '';
+
+ const newErrors: any = {};
+ if (!emailOrUsername)
+ newErrors.emailOrUsername = 'Введите никнейм или email';
+ if (!password) newErrors.password = 'Введите пароль';
+
+ if (Object.keys(newErrors).length > 0) {
+ setErrors(newErrors);
+ setLoading(false);
+ return;
+ }
+
+ const body = new URLSearchParams();
+ body.append('username', emailOrUsername);
+ body.append('password', password);
+
+ try {
+ const res = await fetch('http://localhost:8000/api/auth/login', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: body.toString(),
+ });
+
+ const data = await res.json();
+
+ if (!res.ok) {
+ setErrors({ general: 'Неверный логин или пароль' });
+ } else if (data.access_token) {
+ localStorage.setItem('token', data.access_token);
+
+ const meRes = await fetch('http://localhost:8000/api/me', {
+ headers: { Authorization: `Bearer ${data.access_token}` },
+ });
+ const meData = await meRes.json();
+ setUser(meData);
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
return (
- <>
- <div className="gap-5 flex flex-col h-[148px] w-[310px]">
- <Button className="w-full bg-white hover:bg-white hover:text-black">
- <GoogleIcon />
- <span className="text-black text-sm">
- Войти через Google
- </span>
- </Button>
- <div className="gap-[10px] flex-col flex h-[88px]">
- <InputField placeholder="Никнейм или почта" />
- <InputField isPassword placeholder="Пароль" />
+ <form
+ onSubmit={handleSubmit}
+ className="flex flex-col gap-5 min-w-[310px] w-fit"
+ >
+ <Button className="w-full bg-white hover:bg-white hover:text-black">
+ <GoogleIcon />
+ <span className="text-black text-sm">Войти через Google</span>
+ </Button>
+
+ <div className="flex flex-col gap-2.5">
+ <div className="flex flex-col">
+ <InputField
+ placeholder="Никнейм или почта"
+ name="emailOrUsername"
+ />
+ {errors.emailOrUsername && (
+ <p className="text-red pl-5 text-[12px] leading-[16px] mt-[5px]">
+ {errors.emailOrUsername}
+ </p>
+ )}
+ </div>
+
+ <div className="flex flex-col">
+ <InputField
+ isPassword
+ placeholder="Пароль"
+ name="password"
+ />
+ {errors.password && (
+ <p className="text-red pl-5 text-[12px] leading-[16px] mt-[5px]">
+ {errors.password}
+ </p>
+ )}
+ {!errors.password && errors.general && (
+ <p className="text-red pl-5 text-[12px] leading-[16px] mt-[5px]">
+ {errors.general}
+ </p>
+ )}
</div>
</div>
- <Button className="w-full gap-[10px]">Войти</Button>
- </>
+
+ <Button type="submit" className="w-full" disabled={loading}>
+ {loading ? 'Вход...' : 'Войти'}
+ </Button>
+ </form>
);
}
diff --git a/src/components/header/authdialog/RegisterForm.tsx b/src/components/header/authdialog/RegisterForm.tsx
index 3c2c164..c8ddb55 100644
--- a/src/components/header/authdialog/RegisterForm.tsx
+++ b/src/components/header/authdialog/RegisterForm.tsx
@@ -5,6 +5,7 @@ import GoogleIcon from '../../../../public/icons/google.svg';
import { InputField } from '@/components/ui/inputfield';
import { validate, registerUser } from './register';
import { useAuthContext } from '@/lib/contexts/Auth.context';
+import Image from 'next/image';
export default function RegisterForm() {
const [errors, setErrors] = useState<any>({});
diff --git a/src/components/header/authdialog/register.ts b/src/components/header/authdialog/register.ts
index bcbc21b..ba8bceb 100644
--- a/src/components/header/authdialog/register.ts
+++ b/src/components/header/authdialog/register.ts
@@ -1,44 +1,37 @@
+// register.ts
export const validate = (
username: string,
email: string,
password: string,
passwordConfirm: string,
) => {
- const newErrors: any = {};
+ const newErrors: Record<string, string> = {};
- if (!username) {
- newErrors.username = 'Введите никнейм';
- } else if (username.length < 3) {
- newErrors.username = 'Минимум 3 символа.';
- } else if (username.length > 32) {
- newErrors.username = 'Максимум 32 символа';
- }
+ if (!username) newErrors.username = 'Введите никнейм';
+ else if (username.length < 3) newErrors.username = 'Минимум 3 символа.';
+ else if (username.length > 32) newErrors.username = 'Максимум 32 символа';
- if (!email) {
- newErrors.email = 'Введите email.';
- } else {
+ if (!email) newErrors.email = 'Введите email.';
+ else {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) newErrors.email = 'Некорректный email';
if (email.length > 254) newErrors.email = 'Слишком длинный email.';
}
- if (!password) {
- newErrors.password = 'Введите пароль.';
- } else if (password.length < 8) {
+ if (!password) newErrors.password = 'Введите пароль.';
+ else if (password.length < 8)
newErrors.password = 'Необходимо минимум 8 символов.';
- } else {
+ else {
const hasLetter = /[A-Za-z]/.test(password);
const hasNumber = /\d/.test(password);
const hasSymbol = /[^\w\s]/.test(password);
-
if (!(hasLetter && hasNumber && hasSymbol)) {
newErrors.password = 'Попробуйте сочетание букв, цифр и символов.';
}
}
- if (password !== passwordConfirm) {
- newErrors.passwordConfirm = 'пароли не совпадают.';
- }
+ if (password !== passwordConfirm)
+ newErrors.passwordConfirm = 'Пароли не совпадают.';
return newErrors;
};
@@ -54,16 +47,51 @@ export const registerUser = async (
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, email, password }),
});
-
const data = await res.json();
if (!res.ok) {
- const { field, message } = data.detail;
- return { error: { [field]: message } };
+ const { field, message } = data.detail || {};
+ return {
+ data: null,
+ error: field
+ ? { [field]: message }
+ : { general: 'Ошибка регистрации' },
+ };
}
- return { data };
+ const body = new URLSearchParams();
+ body.append('username', username);
+ body.append('password', password);
+
+ const loginRes = await fetch('http://localhost:8000/api/auth/login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ body: body.toString(),
+ });
+
+ const loginData = await loginRes.json();
+
+ if (!loginRes.ok) {
+ return {
+ data: null,
+ error: {
+ general:
+ loginData.detail || 'Ошибка входа после регистрации',
+ },
+ };
+ }
+
+ localStorage.setItem('token', loginData.access_token);
+ localStorage.setItem('refresh_token', loginData.refresh_token);
+
+ const meRes = await fetch('http://localhost:8000/api/me', {
+ headers: { Authorization: `Bearer ${loginData.access_token}` },
+ });
+
+ const meData = await meRes.json();
+
+ return { data: meData, error: null };
} catch (err: any) {
- return { error: { general: err.message } };
+ return { data: null, error: { general: err.message } };
}
};
diff --git a/src/lib/contexts/Auth.context.tsx b/src/lib/contexts/Auth.context.tsx
index d93ca92..29e2005 100644
--- a/src/lib/contexts/Auth.context.tsx
+++ b/src/lib/contexts/Auth.context.tsx
@@ -1,5 +1,6 @@
'use client';
-import React, { createContext, use, useState, useContext } from 'react';
+import React, { createContext, useState, useContext, useEffect } from 'react';
+
type User = {
id: number;
username: string;
@@ -8,15 +9,36 @@ type User = {
interface AuthContextType {
user: User | null;
setUser: (user: User | null) => void;
+ logout: () => void;
}
-const AuthContext = createContext<AuthContext | null>(null);
+const AuthContext = createContext<AuthContextType | null>(null);
export const AuthContextProvider = ({ children }: React.PropsWithChildren) => {
const [user, setUser] = useState<User | null>(null);
+ useEffect(() => {
+ const token = localStorage.getItem('token');
+ if (!token) return;
+
+ fetch('http://localhost:8000/api/me', {
+ headers: { Authorization: `Bearer ${token}` },
+ })
+ .then((res) => {
+ if (!res.ok) throw new Error('Not authenticated');
+ return res.json();
+ })
+ .then((userData) => setUser(userData))
+ .catch(() => setUser(null));
+ }, []);
+
+ const logout = () => {
+ localStorage.removeItem('token');
+ setUser(null);
+ };
+
return (
- <AuthContext.Provider value={{ user, setUser }}>
+ <AuthContext.Provider value={{ user, setUser, logout }}>
{children}
</AuthContext.Provider>
);
@@ -32,3 +54,4 @@ export const useAuthContext = () => {
};
export const useUser = () => useAuthContext().user;
+export const useLogout = () => useAuthContext().logout;
diff --git a/src/lib/contexts/Global.context.tsx b/src/lib/contexts/Global.context.tsx
index 3f8b61b..c903456 100644
--- a/src/lib/contexts/Global.context.tsx
+++ b/src/lib/contexts/Global.context.tsx
@@ -1,3 +1,5 @@
+import react from 'react';
+import { authcontextprovider } from './auth.context';
import React from 'react';
import { AuthContextProvider } from './Auth.context';