Добавлен 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/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",
|
||||||
|
|||||||
@ -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
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 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>
|
||||||
|
|||||||
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