Добавлен middleware для обработки авторизации, страница входа с формой и валидацией, а также стили для страницы авторизации. Обновлены зависимости js-cookie и @types/js-cookie.

This commit is contained in:
Redsandyg 2025-06-03 12:07:03 +03:00
parent 9b1cbc0300
commit 6ab1a42be7
6 changed files with 180 additions and 0 deletions

16
middleware.ts Normal file
View 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
View File

@ -12,6 +12,7 @@
"@mui/material": "^7.1.0", "@mui/material": "^7.1.0",
"@mui/x-data-grid": "^8.5.0", "@mui/x-data-grid": "^8.5.0",
"@types/react-datepicker": "^6.2.0", "@types/react-datepicker": "^6.2.0",
"js-cookie": "^3.0.5",
"material-react-table": "^3.2.1", "material-react-table": "^3.2.1",
"next": "15.3.3", "next": "15.3.3",
"react": "^19.0.0", "react": "^19.0.0",
@ -20,6 +21,7 @@
"recharts": "^2.15.3" "recharts": "^2.15.3"
}, },
"devDependencies": { "devDependencies": {
"@types/js-cookie": "^3.0.6",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
@ -1497,6 +1499,13 @@
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT" "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": { "node_modules/@types/node": {
"version": "20.17.57", "version": "20.17.57",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz",
@ -2067,6 +2076,15 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",

View File

@ -13,6 +13,7 @@
"@mui/material": "^7.1.0", "@mui/material": "^7.1.0",
"@mui/x-data-grid": "^8.5.0", "@mui/x-data-grid": "^8.5.0",
"@types/react-datepicker": "^6.2.0", "@types/react-datepicker": "^6.2.0",
"js-cookie": "^3.0.5",
"material-react-table": "^3.2.1", "material-react-table": "^3.2.1",
"next": "15.3.3", "next": "15.3.3",
"react": "^19.0.0", "react": "^19.0.0",
@ -21,6 +22,7 @@
"recharts": "^2.15.3" "recharts": "^2.15.3"
}, },
"devDependencies": { "devDependencies": {
"@types/js-cookie": "^3.0.6",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",

88
src/app/auth/page.tsx Normal file
View 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>
);
}

View File

@ -17,6 +17,7 @@ const navItems: NavItem[] = [
const Navigation: React.FC = () => { const Navigation: React.FC = () => {
const pathname = usePathname(); const pathname = usePathname();
if (pathname === "/auth") return null;
return ( return (
<nav className={styles.nav}> <nav className={styles.nav}>
<div className={styles.logo}>RE:Premium Partner</div> <div className={styles.logo}>RE:Premium Partner</div>

View 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;
}