Добавлен middleware для обработки авторизации, страница входа с формой и валидацией, а также стили для страницы авторизации. Обновлены зависимости js-cookie и @types/js-cookie.
This commit is contained in:
parent
9b1cbc0300
commit
6ab1a42be7
16
middleware.ts
Normal file
16
middleware.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl;
|
||||
// Получаем access_token из куков (SSR)
|
||||
const token = request.cookies.get('access_token');
|
||||
|
||||
if (pathname === '/auth' && token) {
|
||||
return NextResponse.redirect(new URL('/', request.url));
|
||||
}
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/auth'],
|
||||
};
|
||||
18
package-lock.json
generated
18
package-lock.json
generated
@ -12,6 +12,7 @@
|
||||
"@mui/material": "^7.1.0",
|
||||
"@mui/x-data-grid": "^8.5.0",
|
||||
"@types/react-datepicker": "^6.2.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"material-react-table": "^3.2.1",
|
||||
"next": "15.3.3",
|
||||
"react": "^19.0.0",
|
||||
@ -20,6 +21,7 @@
|
||||
"recharts": "^2.15.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
@ -1497,6 +1499,13 @@
|
||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/js-cookie": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz",
|
||||
"integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.17.57",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz",
|
||||
@ -2067,6 +2076,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/js-cookie": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
"@mui/material": "^7.1.0",
|
||||
"@mui/x-data-grid": "^8.5.0",
|
||||
"@types/react-datepicker": "^6.2.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"material-react-table": "^3.2.1",
|
||||
"next": "15.3.3",
|
||||
"react": "^19.0.0",
|
||||
@ -21,6 +22,7 @@
|
||||
"recharts": "^2.15.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
|
||||
88
src/app/auth/page.tsx
Normal file
88
src/app/auth/page.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
"use client";
|
||||
import { useState, useEffect } from "react";
|
||||
import Cookies from "js-cookie";
|
||||
import styles from "../../styles/auth.module.css";
|
||||
|
||||
function hasToken() {
|
||||
if (typeof document === "undefined") return false;
|
||||
return Cookies.get('access_token') !== undefined;
|
||||
}
|
||||
|
||||
export default function AuthPage() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasToken()) {
|
||||
window.location.href = "/";
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setLoading(true);
|
||||
if (!email || !password) {
|
||||
setError("Пожалуйста, заполните все поля");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch("/api/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
login: email,
|
||||
password: password,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
setError(data.detail || "Ошибка авторизации");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
// Сохраняем токен в куки на 60 минут через js-cookie
|
||||
Cookies.set('access_token', data.access_token, { expires: 1/24, path: '/', sameSite: 'strict'});
|
||||
setError("");
|
||||
window.location.href = "/";
|
||||
} catch (err) {
|
||||
setError("Ошибка сети или сервера");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.authContainer}>
|
||||
<h1 className={styles.authTitle}>Вход в систему</h1>
|
||||
<form className={styles.authForm} onSubmit={handleSubmit}>
|
||||
<input
|
||||
className={styles.authInput}
|
||||
type="text"
|
||||
placeholder="Логин"
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
autoComplete="username"
|
||||
/>
|
||||
<input
|
||||
className={styles.authInput}
|
||||
type="password"
|
||||
placeholder="Пароль"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
{error && <div className={styles.authError}>{error}</div>}
|
||||
<button className={styles.authButton} type="submit" disabled={loading}>
|
||||
{loading ? "Вход..." : "Войти"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -17,6 +17,7 @@ const navItems: NavItem[] = [
|
||||
|
||||
const Navigation: React.FC = () => {
|
||||
const pathname = usePathname();
|
||||
if (pathname === "/auth") return null;
|
||||
return (
|
||||
<nav className={styles.nav}>
|
||||
<div className={styles.logo}>RE:Premium Partner</div>
|
||||
|
||||
55
src/styles/auth.module.css
Normal file
55
src/styles/auth.module.css
Normal file
@ -0,0 +1,55 @@
|
||||
.authContainer {
|
||||
max-width: 400px;
|
||||
margin: 60px auto;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 16px rgba(0,0,0,0.07);
|
||||
padding: 32px 32px 24px 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.authTitle {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 24px;
|
||||
color: #222;
|
||||
}
|
||||
.authForm {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
.authInput {
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
transition: border 0.2s;
|
||||
}
|
||||
.authInput:focus {
|
||||
border: 1.5px solid #0070f3;
|
||||
}
|
||||
.authButton {
|
||||
background: #0070f3;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 12px 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.authButton:hover {
|
||||
background: #005bb5;
|
||||
}
|
||||
.authError {
|
||||
color: #e53935;
|
||||
font-size: 0.95rem;
|
||||
margin-top: -10px;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user