rework session work
This commit is contained in:
Generated
+21
-1
@@ -11,11 +11,13 @@
|
|||||||
"@tanstack/react-query": "^5.90.11",
|
"@tanstack/react-query": "^5.90.11",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"jose": "^6.1.2",
|
||||||
"next": "16.1.2",
|
"next": "16.1.2",
|
||||||
"next-intl": "^4.7.0",
|
"next-intl": "^4.7.0",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"sonner": "^2.0.7"
|
"sonner": "^2.0.7",
|
||||||
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
@@ -1803,6 +1805,15 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jose": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.30.2",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
||||||
@@ -2550,6 +2561,15 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0"
|
"react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "4.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||||
|
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -11,11 +11,13 @@
|
|||||||
"@tanstack/react-query": "^5.90.11",
|
"@tanstack/react-query": "^5.90.11",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"jose": "^6.1.2",
|
||||||
"next": "16.1.2",
|
"next": "16.1.2",
|
||||||
"next-intl": "^4.7.0",
|
"next-intl": "^4.7.0",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"sonner": "^2.0.7"
|
"sonner": "^2.0.7",
|
||||||
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
|
|||||||
+13
-19
@@ -6,10 +6,11 @@ import { getQueryClient } from '@/app/providers/getQueryClient';
|
|||||||
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
|
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
|
||||||
import { prefetchLayoutQueries } from '@/app/lib/prefetch-queries';
|
import { prefetchLayoutQueries } from '@/app/lib/prefetch-queries';
|
||||||
import { routing } from '@/i18n/routing';
|
import { routing } from '@/i18n/routing';
|
||||||
import AdminNavLinks from '@/app/ui/navigation/admin-nav-links'
|
import AdminLayoutWrapper from '@/app/components/admin-layout-wrapper';
|
||||||
import AdminHeaderPanel from '@/app/ui/admin-header-panel';
|
|
||||||
|
|
||||||
|
// @ts-expect-error - CSS modules types not defined
|
||||||
import "../styles/globals.css";
|
import "../styles/globals.css";
|
||||||
|
// @ts-expect-error - CSS modules types not defined
|
||||||
import "../styles/global-styles.scss"
|
import "../styles/global-styles.scss"
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -17,7 +18,7 @@ export const metadata: Metadata = {
|
|||||||
description: "Admin panel no copy",
|
description: "Admin panel no copy",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
return routing.locales.map((locale: any) => ({ locale }));
|
return routing.locales.map((locale: any) => ({ locale }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ export default async function RootLayout({
|
|||||||
params: Promise<{ locale: string }>;
|
params: Promise<{ locale: string }>;
|
||||||
}>) {
|
}>) {
|
||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
|
|
||||||
if (!hasLocale(routing.locales, locale)) {
|
if (!hasLocale(routing.locales, locale)) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
@@ -38,21 +40,13 @@ export default async function RootLayout({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<NextIntlClientProvider>
|
<body>
|
||||||
<Providers>
|
<NextIntlClientProvider>
|
||||||
<HydrationBoundary state={dehydrate(queryClient)}>
|
<Providers>
|
||||||
<body className="admin-panel">
|
{children}
|
||||||
<AdminNavLinks />
|
</Providers>
|
||||||
<div className="admin-main">
|
</NextIntlClientProvider>
|
||||||
<AdminHeaderPanel />
|
</body>
|
||||||
<main className="main-containter">
|
</html>
|
||||||
{children}
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</HydrationBoundary>
|
|
||||||
</Providers>
|
|
||||||
</NextIntlClientProvider>
|
|
||||||
</html >
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { hasLocale } from 'next-intl';
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
import { getQueryClient } from '@/app/providers/getQueryClient';
|
||||||
|
import { prefetchLayoutQueries } from '@/app/lib/prefetch-queries';
|
||||||
|
import { routing } from '@/i18n/routing';
|
||||||
|
|
||||||
|
export default async function RootLayout({
|
||||||
|
children,
|
||||||
|
params
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
params: Promise<{ locale: string }>;
|
||||||
|
}>) {
|
||||||
|
const { locale } = await params;
|
||||||
|
if (!hasLocale(routing.locales, locale)) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryClient = getQueryClient();
|
||||||
|
await prefetchLayoutQueries(queryClient);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="">
|
||||||
|
<main className="main-containter">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import LoginForm from '@/app/ui/forms/login-form';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<LoginForm />
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href={'/pages/'}
|
||||||
|
>
|
||||||
|
link
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Providers from '@/app/providers/getQueryServer'
|
||||||
|
import { NextIntlClientProvider, hasLocale } from 'next-intl';
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
import { getQueryClient } from '@/app/providers/getQueryClient';
|
||||||
|
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
|
||||||
|
import { prefetchLayoutQueries } from '@/app/lib/prefetch-queries';
|
||||||
|
import { routing } from '@/i18n/routing';
|
||||||
|
import AdminLayoutWrapper from '@/app/components/admin-layout-wrapper';
|
||||||
|
|
||||||
|
// @ts-expect-error - CSS modules types not defined
|
||||||
|
import "../../styles/globals.css";
|
||||||
|
// @ts-expect-error - CSS modules types not defined
|
||||||
|
import "../../styles/global-styles.scss"
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Admin panel no copy",
|
||||||
|
description: "Admin panel no copy",
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
return routing.locales.map((locale: any) => ({ locale }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function RootLayout({
|
||||||
|
children,
|
||||||
|
params
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
params: Promise<{ locale: string }>;
|
||||||
|
}>) {
|
||||||
|
const { locale } = await params;
|
||||||
|
|
||||||
|
if (!hasLocale(routing.locales, locale)) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryClient = getQueryClient();
|
||||||
|
await prefetchLayoutQueries(queryClient);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||||
|
<div className="admin-panel">
|
||||||
|
<AdminLayoutWrapper>
|
||||||
|
{children}
|
||||||
|
</AdminLayoutWrapper>
|
||||||
|
</div>
|
||||||
|
</HydrationBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
import { createSession, deleteSession, getSessionData, updateSession } from '@/app/actions/session';
|
||||||
|
import { loginFormSchema, API_BASE_URL } from '@/app/actions/definitions';
|
||||||
|
|
||||||
|
export async function authorization(
|
||||||
|
state: {
|
||||||
|
previousState: {
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
error: Record<string, string>
|
||||||
|
} | undefined,
|
||||||
|
formData: FormData,
|
||||||
|
) {
|
||||||
|
const email = formData.get('email') as string || '';
|
||||||
|
const password = formData.get('password');
|
||||||
|
|
||||||
|
/* для теста */
|
||||||
|
if (email === 'test' && password === 'test') {
|
||||||
|
await createSession('1111', 'test');
|
||||||
|
redirect('/pages/');
|
||||||
|
}
|
||||||
|
/* для теста */
|
||||||
|
|
||||||
|
const validatedFields = loginFormSchema.safeParse({
|
||||||
|
email,
|
||||||
|
password
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!validatedFields.success) {
|
||||||
|
const errors: Record<string, string> = {}
|
||||||
|
JSON.parse(validatedFields.error.message).forEach((obj: {
|
||||||
|
message: string,
|
||||||
|
path: string
|
||||||
|
}) => {
|
||||||
|
if (errors[obj.path[0]]) {
|
||||||
|
errors[obj.path[0]] = `${errors[obj.path[0]]} ${obj.message}`;
|
||||||
|
} else {
|
||||||
|
errors[obj.path[0]] = obj.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
previousState: {
|
||||||
|
email
|
||||||
|
},
|
||||||
|
error: errors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { email, password } = validatedFields.data;
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
msg_id: 20001,
|
||||||
|
message_body: {
|
||||||
|
email: email,
|
||||||
|
password: password
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
let parsed = await response.json();
|
||||||
|
if (parsed.message_code === 0) {
|
||||||
|
await createSession(parsed.message_body.token, email);
|
||||||
|
} else {
|
||||||
|
throw (`${parsed.message_code}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw ('request-ended-with-an-error');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const errors: Record<string, string> = {}
|
||||||
|
|
||||||
|
switch (error) {
|
||||||
|
case '2':
|
||||||
|
errors['server'] = 'password-does-not-match'
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '4':
|
||||||
|
errors['server'] = 'email-not-found'
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
errors['server'] = 'request-ended-with-an-error'
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return {
|
||||||
|
previousState: {
|
||||||
|
email,
|
||||||
|
password
|
||||||
|
},
|
||||||
|
error: errors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect('/pages/');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function logout() {
|
||||||
|
const token = await getSessionData('token');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch(`${API_BASE_URL}/v1/api/auth/logout`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await deleteSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import * as z from 'zod'
|
||||||
|
|
||||||
|
export const loginFormSchema = z
|
||||||
|
.object({
|
||||||
|
email: z
|
||||||
|
.string()
|
||||||
|
.min(1, { error: 'login-error-email' })
|
||||||
|
.trim(),
|
||||||
|
password: z
|
||||||
|
.string()
|
||||||
|
.min(1, { error: 'login-error-password' })
|
||||||
|
.trim(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const localDevelopmentUrl = process.env.DEV_URL ? process.env.DEV_URL : 'http://localhost';
|
||||||
|
|
||||||
|
export const API_BASE_URL = process.env.NODE_ENV === 'development'
|
||||||
|
? localDevelopmentUrl
|
||||||
|
: 'http://app:8080';
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
/* import 'server-only'; */
|
||||||
|
import { cookies } from 'next/headers'
|
||||||
|
import { SignJWT, jwtVerify } from 'jose'
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
/* const secretKey = process.env.SESSION_SECRET */
|
||||||
|
const secretKey = 'AAA+sq1yKte/gMgUUu/B/OyXqr45/LMYplPVOlWc+uo='
|
||||||
|
const encodedKey = new TextEncoder().encode(secretKey)
|
||||||
|
|
||||||
|
export async function encrypt(payload: any) {
|
||||||
|
return new SignJWT(payload)
|
||||||
|
.setProtectedHeader({ alg: 'HS256' })
|
||||||
|
.setIssuedAt()
|
||||||
|
.setExpirationTime('7d')
|
||||||
|
.sign(encodedKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function decrypt(session: string | undefined = '') {
|
||||||
|
try {
|
||||||
|
const { payload } = await jwtVerify(session, encodedKey, {
|
||||||
|
algorithms: ['HS256'],
|
||||||
|
})
|
||||||
|
return payload
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Failed to verify session')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSession(token: string, email: string) {
|
||||||
|
const expiresAt = new Date(Date.now() + 50 * 60 * 1000);
|
||||||
|
const session = await encrypt({
|
||||||
|
token,
|
||||||
|
email,
|
||||||
|
expiresAt
|
||||||
|
});
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
|
||||||
|
cookieStore.set('session', session, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: false,
|
||||||
|
expires: expiresAt,
|
||||||
|
sameSite: 'lax',
|
||||||
|
path: '/',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateSession(newToken: string) {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const session = cookieStore.get('session')?.value;
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = await decrypt(session);
|
||||||
|
|
||||||
|
if (!payload) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedPayload = {
|
||||||
|
...payload,
|
||||||
|
token: newToken,
|
||||||
|
expiresAt: new Date(Date.now() + 50 * 60 * 1000)
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedSession = await encrypt(updatedPayload);
|
||||||
|
|
||||||
|
cookieStore.set('session', updatedSession, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: false,
|
||||||
|
expires: updatedPayload.expiresAt,
|
||||||
|
sameSite: 'lax',
|
||||||
|
path: '/',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSessionData(type: 'token' | 'email'): Promise<string | null> {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const sessionCookie = cookieStore.get('session')?.value;
|
||||||
|
|
||||||
|
if (!sessionCookie) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = await decrypt(sessionCookie);
|
||||||
|
|
||||||
|
if (payload && typeof payload === 'object' && type in payload) {
|
||||||
|
return payload[type] as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function deleteSession() {
|
||||||
|
console.log('delete session')
|
||||||
|
try {
|
||||||
|
const cookieStore = await cookies()
|
||||||
|
cookieStore.delete('session');
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
redirect('/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import AdminNavLinks from '@/app/ui/navigation/admin-nav-links';
|
||||||
|
import AdminHeaderPanel from '@/app/ui/admin-header-panel';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export default function AdminLayoutWrapper({
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const [isLoginPage, setIsLoginPage] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoginPage(pathname?.includes('/login') || false);
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
|
if (!isLoginPage) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AdminNavLinks />
|
||||||
|
<div className="admin-main">
|
||||||
|
<AdminHeaderPanel />
|
||||||
|
<main className="main-containter">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>{children}</div>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
.login-container-wrapper {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 40px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
|
||||||
|
&-language {
|
||||||
|
position: fixed;
|
||||||
|
right: 20px;
|
||||||
|
top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switcher {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
&-button {
|
||||||
|
padding: 5px 15px;
|
||||||
|
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label.required::after {
|
||||||
|
content: ' *';
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 2px solid #e5e7eb;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
background: #f9fafb;
|
||||||
|
|
||||||
|
&-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[type="password"]::-ms-reveal {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.password {
|
||||||
|
padding-right: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.valid-card {
|
||||||
|
border: 2px solid #2f9e44;
|
||||||
|
background: #ecf8ee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-options {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-error {
|
||||||
|
color: #fb2c36;
|
||||||
|
font-size: .875rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
margin-right: 10px;
|
||||||
|
accent-color: #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #6366f1;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.forgot-password {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6366f1;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px;
|
||||||
|
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
margin: 20px 0 0 0;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
background: linear-gradient(135deg, #414291, #4b3975);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.refresh {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 3px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group-confrim-window {
|
||||||
|
min-width: 500px;
|
||||||
|
|
||||||
|
&-button-group {
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 50% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 550px) {
|
||||||
|
min-width: auto;
|
||||||
|
|
||||||
|
&-button-group {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-link {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 18px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #6366f1;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.features {
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: 8px;
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info {
|
||||||
|
background: #f0f9ff;
|
||||||
|
border: 1px solid #bae6fd;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
color: #0369a1;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #0c4a6e;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import styles from '@/app/styles/module/login.module.scss'
|
||||||
|
import { useActionState, useState, useRef, useEffect } from 'react';
|
||||||
|
import { authorization } from '@/app/actions/auth';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { IconEye } from '@/app/ui/icons/icons';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export default function LoginForm() {
|
||||||
|
const [state, formAction, isPending] = useActionState(
|
||||||
|
authorization,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
const t = useTranslations('Login-register-form');
|
||||||
|
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
|
function showPassowrd() {
|
||||||
|
setShowPassword(!showPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkBoxRememberMe = useRef<HTMLInputElement>(null);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
queryClient.clear();
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form action={formAction}>
|
||||||
|
<div className={`${styles['form-group']}`}>
|
||||||
|
<label className={`${styles['form-label']}`}>
|
||||||
|
{t('email-adress')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="readOnly"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
className={`${styles['form-input']}`}
|
||||||
|
placeholder={t('enter-email')}
|
||||||
|
defaultValue={state?.previousState?.email ? state?.previousState?.email : ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.target.value = e.target.value.toLocaleLowerCase();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{state?.error?.email && (
|
||||||
|
<p className={`${styles['form-error']}`}>
|
||||||
|
{t(state?.error?.email)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={`${styles['form-group']}`}>
|
||||||
|
<label className={`${styles['form-label']}`}>
|
||||||
|
{t('password')}
|
||||||
|
</label>
|
||||||
|
<div className={`${styles['password-wrapper']}`}>
|
||||||
|
<input
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
className={`${styles['form-input']} ${styles['password']}`}
|
||||||
|
placeholder={t('enter-password')}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
showPassowrd();
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
className={`show-password-button ${showPassword ? 'show' : ''}`}
|
||||||
|
>
|
||||||
|
<IconEye />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{state?.error?.password && (
|
||||||
|
<p className={`${styles['form-error']}`}>
|
||||||
|
{t(state?.error?.password)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{state?.error.server && (
|
||||||
|
<p className={`${styles['form-error']}`}>
|
||||||
|
{t(state?.error.server)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className={`${styles['form-options']}`}>
|
||||||
|
<div className={`${styles['form-checkbox']}`}>
|
||||||
|
<input type="checkbox" id="remember" name="remember" ref={checkBoxRememberMe} />
|
||||||
|
<label
|
||||||
|
onClick={() => {
|
||||||
|
if (checkBoxRememberMe.current) {
|
||||||
|
checkBoxRememberMe.current.click();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="select-none"
|
||||||
|
>
|
||||||
|
{t('remember-me')}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<a href="forgot-password" className={`${styles['forgot-password']}`} >
|
||||||
|
{t('forgot-password')}?
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={`${styles['btn']}`}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
{t("sign-in")}
|
||||||
|
{isPending && (
|
||||||
|
<div className="loading-animation">
|
||||||
|
<div className="global-spinner"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,70 +4,73 @@ import Link from 'next/link';
|
|||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { logout } from '@/app/actions/auth';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
export default function AdminNavLinks() {
|
export default function AdminNavLinks() {
|
||||||
const pathname = usePathname().replace('/en/', '/').replace('/ru/', '/');
|
const pathname = usePathname().replace('/en/', '/').replace('/ru/', '/');
|
||||||
const t = useTranslations('Global');
|
const t = useTranslations('Global');
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
{
|
{
|
||||||
name: 'dashboard',
|
name: 'dashboard',
|
||||||
href: '/',
|
href: '/pages',
|
||||||
img: 'M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z'
|
img: 'M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'users',
|
name: 'users',
|
||||||
href: '/users',
|
href: '/pages/users',
|
||||||
img: 'M16 17v2H2v-2s0-4 7-4s7 4 7 4m-3.5-9.5A3.5 3.5 0 1 0 9 11a3.5 3.5 0 0 0 3.5-3.5m3.44 5.5A5.32 5.32 0 0 1 18 17v2h4v-2s0-3.63-6.06-4M15 4a3.4 3.4 0 0 0-1.93.59a5 5 0 0 1 0 5.82A3.4 3.4 0 0 0 15 11a3.5 3.5 0 0 0 0-7'
|
img: 'M16 17v2H2v-2s0-4 7-4s7 4 7 4m-3.5-9.5A3.5 3.5 0 1 0 9 11a3.5 3.5 0 0 0 3.5-3.5m3.44 5.5A5.32 5.32 0 0 1 18 17v2h4v-2s0-3.63-6.06-4M15 4a3.4 3.4 0 0 0-1.93.59a5 5 0 0 1 0 5.82A3.4 3.4 0 0 0 15 11a3.5 3.5 0 0 0 0-7'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'subscriptions',
|
name: 'subscriptions',
|
||||||
href: '/subscriptions',
|
href: '/pages/subscriptions',
|
||||||
img: 'M22 6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2zm-2 2H4V6h16zM4 11h16v7H4z'
|
img: 'M22 6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2zm-2 2H4V6h16zM4 11h16v7H4z'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tariff management',
|
name: 'tariff management',
|
||||||
href: '/tariff-management',
|
href: '/pages/tariff-management',
|
||||||
img: 'M4 14v-2h7v2zm0-4V8h11v2zm0-4V4h11v2zm9 14v-3.075l5.525-5.5q.225-.225.5-.325t.55-.1q.3 0 .575.113t.5.337l.925.925q.2.225.313.5t.112.55t-.1.563t-.325.512l-5.5 5.5zm6.575-5.6l.925-.975l-.925-.925l-.95.95z'
|
img: 'M4 14v-2h7v2zm0-4V8h11v2zm0-4V4h11v2zm9 14v-3.075l5.525-5.5q.225-.225.5-.325t.55-.1q.3 0 .575.113t.5.337l.925.925q.2.225.313.5t.112.55t-.1.563t-.325.512l-5.5 5.5zm6.575-5.6l.925-.975l-.925-.925l-.95.95z'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'content',
|
name: 'content',
|
||||||
href: '/content',
|
href: '/pages/content',
|
||||||
img: 'M4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h6l2 2h8q.825 0 1.413.588T22 8v10q0 .825-.587 1.413T20 20zm0-2h16V8h-8.825l-2-2H4zm0 0V6z'
|
img: 'M4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h6l2 2h8q.825 0 1.413.588T22 8v10q0 .825-.587 1.413T20 20zm0-2h16V8h-8.825l-2-2H4zm0 0V6z'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'violations',
|
name: 'violations',
|
||||||
href: '/violations',
|
href: '/pages/violations',
|
||||||
img: 'M1 21L12 2l11 19zm3.45-2h15.1L12 6zM12 18q.425 0 .713-.288T13 17t-.288-.712T12 16t-.712.288T11 17t.288.713T12 18m-1-3h2v-5h-2zm1-2.5'
|
img: 'M1 21L12 2l11 19zm3.45-2h15.1L12 6zM12 18q.425 0 .713-.288T13 17t-.288-.712T12 16t-.712.288T11 17t.288.713T12 18m-1-3h2v-5h-2zm1-2.5'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'dmc',
|
name: 'dmc',
|
||||||
href: '/dmc',
|
href: '/pages/dmc',
|
||||||
img: 'M12 20a8 8 0 1 0 0-16a8 8 0 0 0 0 16m0 2C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10m-1-6h2v2h-2zm0-10h2v8h-2z'
|
img: 'M12 20a8 8 0 1 0 0-16a8 8 0 0 0 0 16m0 2C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10m-1-6h2v2h-2zm0-10h2v8h-2z'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'email notification',
|
name: 'email notification',
|
||||||
href: '/email-notification',
|
href: '/pages/email-notification',
|
||||||
img: 'M2 20V4h20v16zm10-7L4 8v10h16V8zm0-2l8-5H4zM4 8V6v12z'
|
img: 'M2 20V4h20v16zm10-7L4 8v10h16V8zm0-2l8-5H4zM4 8V6v12z'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'api control',
|
name: 'api control',
|
||||||
href: '/api-control',
|
href: '/pages/api-control',
|
||||||
img: 'M13.26 10.5h2v1h-2zM8.4 15L8 13.77H6.06L5.62 15H4l2.2-6h1.62L10 15Zm8.36-3.5a1.47 1.47 0 0 1-1.5 1.5h-2v2h-1.5V9h3.5a1.47 1.47 0 0 1 1.5 1.5ZM20 15h-1.5V9H20ZM6.43 12.77h1.16l-.58-1.59zM20 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2'
|
img: 'M13.26 10.5h2v1h-2zM8.4 15L8 13.77H6.06L5.62 15H4l2.2-6h1.62L10 15Zm8.36-3.5a1.47 1.47 0 0 1-1.5 1.5h-2v2h-1.5V9h3.5a1.47 1.47 0 0 1 1.5 1.5ZM20 15h-1.5V9H20ZM6.43 12.77h1.16l-.58-1.59zM20 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'analitic',
|
name: 'analitic',
|
||||||
href: '/analitic',
|
href: '/pages/analitic',
|
||||||
img: 'm16 11.78l4.24-7.33l1.73 1l-5.23 9.05l-6.51-3.75L5.46 19H22v2H2V3h2v14.54L9.5 8z'
|
img: 'm16 11.78l4.24-7.33l1.73 1l-5.23 9.05l-6.51-3.75L5.46 19H22v2H2V3h2v14.54L9.5 8z'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'system logs',
|
name: 'system logs',
|
||||||
href: '/system-logs',
|
href: '/pages/system-logs',
|
||||||
img: 'M4.5 2.25v19.5h15V7.195l-.21-.235l-4.5-4.5l-.236-.21H4.5zm1.5 1.5h7.5v4.5h4.5v12H6zm9 1.078L16.922 6.75H15zM8.25 9.75v1.5h7.5v-1.5zm0 3v1.5h7.5v-1.5zm0 3v1.5h7.5v-1.5z'
|
img: 'M4.5 2.25v19.5h15V7.195l-.21-.235l-4.5-4.5l-.236-.21H4.5zm1.5 1.5h7.5v4.5h4.5v12H6zm9 1.078L16.922 6.75H15zM8.25 9.75v1.5h7.5v-1.5zm0 3v1.5h7.5v-1.5zm0 3v1.5h7.5v-1.5z'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'system',
|
name: 'system',
|
||||||
href: '/system',
|
href: '/pages/system',
|
||||||
img: 'M12 8a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 2a2 2 0 0 0-2 2a2 2 0 0 0 2 2a2 2 0 0 0 2-2a2 2 0 0 0-2-2m-2 12c-.25 0-.46-.18-.5-.42l-.37-2.65c-.63-.25-1.17-.59-1.69-.99l-2.49 1.01c-.22.08-.49 0-.61-.22l-2-3.46a.493.493 0 0 1 .12-.64l2.11-1.66L4.5 12l.07-1l-2.11-1.63a.493.493 0 0 1-.12-.64l2-3.46c.12-.22.39-.31.61-.22l2.49 1c.52-.39 1.06-.73 1.69-.98l.37-2.65c.04-.24.25-.42.5-.42h4c.25 0 .46.18.5.42l.37 2.65c.63.25 1.17.59 1.69.98l2.49-1c.22-.09.49 0 .61.22l2 3.46c.13.22.07.49-.12.64L19.43 11l.07 1l-.07 1l2.11 1.63c.19.15.25.42.12.64l-2 3.46c-.12.22-.39.31-.61.22l-2.49-1c-.52.39-1.06.73-1.69.98l-.37 2.65c-.04.24-.25.42-.5.42zm1.25-18l-.37 2.61c-1.2.25-2.26.89-3.03 1.78L5.44 7.35l-.75 1.3L6.8 10.2a5.55 5.55 0 0 0 0 3.6l-2.12 1.56l.75 1.3l2.43-1.04c.77.88 1.82 1.52 3.01 1.76l.37 2.62h1.52l.37-2.61c1.19-.25 2.24-.89 3.01-1.77l2.43 1.04l.75-1.3l-2.12-1.55c.4-1.17.4-2.44 0-3.61l2.11-1.55l-.75-1.3l-2.41 1.04a5.42 5.42 0 0 0-3.03-1.77L12.75 4z'
|
img: 'M12 8a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 2a2 2 0 0 0-2 2a2 2 0 0 0 2 2a2 2 0 0 0 2-2a2 2 0 0 0-2-2m-2 12c-.25 0-.46-.18-.5-.42l-.37-2.65c-.63-.25-1.17-.59-1.69-.99l-2.49 1.01c-.22.08-.49 0-.61-.22l-2-3.46a.493.493 0 0 1 .12-.64l2.11-1.66L4.5 12l.07-1l-2.11-1.63a.493.493 0 0 1-.12-.64l2-3.46c.12-.22.39-.31.61-.22l2.49 1c.52-.39 1.06-.73 1.69-.98l.37-2.65c.04-.24.25-.42.5-.42h4c.25 0 .46.18.5.42l.37 2.65c.63.25 1.17.59 1.69.98l2.49-1c.22-.09.49 0 .61.22l2 3.46c.13.22.07.49-.12.64L19.43 11l.07 1l-.07 1l2.11 1.63c.19.15.25.42.12.64l-2 3.46c-.12.22-.39.31-.61.22l-2.49-1c-.52.39-1.06.73-1.69.98l-.37 2.65c-.04.24-.25.42-.5.42zm1.25-18l-.37 2.61c-1.2.25-2.26.89-3.03 1.78L5.44 7.35l-.75 1.3L6.8 10.2a5.55 5.55 0 0 0 0 3.6l-2.12 1.56l.75 1.3l2.43-1.04c.77.88 1.82 1.52 3.01 1.76l.37 2.62h1.52l.37-2.61c1.19-.25 2.24-.89 3.01-1.77l2.43 1.04l.75-1.3l-2.12-1.55c.4-1.17.4-2.44 0-3.61l2.11-1.55l-.75-1.3l-2.41 1.04a5.42 5.42 0 0 0-3.03-1.77L12.75 4z'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -104,8 +107,10 @@ export default function AdminNavLinks() {
|
|||||||
key={t('exit')}
|
key={t('exit')}
|
||||||
href='/login'
|
href='/login'
|
||||||
className="admin-nav-item"
|
className="admin-nav-item"
|
||||||
onClick={(e) => {
|
onClick={async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
queryClient.clear();
|
||||||
|
await logout();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
|||||||
+23
-2
@@ -2,12 +2,17 @@ import { NextResponse, NextRequest } from 'next/server';
|
|||||||
import { cookies } from 'next/headers'
|
import { cookies } from 'next/headers'
|
||||||
import createIntlMiddleware from 'next-intl/middleware'
|
import createIntlMiddleware from 'next-intl/middleware'
|
||||||
import { routing } from '@/i18n/routing'
|
import { routing } from '@/i18n/routing'
|
||||||
|
import { decrypt } from '@/app/actions/session';
|
||||||
|
|
||||||
const intlMiddleware = createIntlMiddleware(routing);
|
const intlMiddleware = createIntlMiddleware(routing);
|
||||||
|
|
||||||
export default async function proxy(request: NextRequest) {
|
export default async function proxy(request: NextRequest) {
|
||||||
const path = request.nextUrl.pathname;
|
const path = request.nextUrl.pathname;
|
||||||
|
|
||||||
|
const cookie = (await cookies()).get('session')?.value;
|
||||||
|
const session = await decrypt(cookie);
|
||||||
|
console.log(session);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
path.startsWith('/_next') ||
|
path.startsWith('/_next') ||
|
||||||
path.includes('.')
|
path.includes('.')
|
||||||
@@ -15,13 +20,29 @@ export default async function proxy(request: NextRequest) {
|
|||||||
return NextResponse.next()
|
return NextResponse.next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const requestHeaders = new Headers(request.headers);
|
||||||
|
requestHeaders.set('x-pathname', path);
|
||||||
|
|
||||||
const intlResponse = intlMiddleware(request);
|
const intlResponse = intlMiddleware(request);
|
||||||
|
|
||||||
if (intlResponse) {
|
if (intlResponse) {
|
||||||
return intlResponse
|
const response = new NextResponse(intlResponse.body, intlResponse);
|
||||||
|
response.headers.set('x-pathname', path);
|
||||||
|
|
||||||
|
/* if (!session?.token) {
|
||||||
|
const loginUrl = new URL('/login', request.nextUrl)
|
||||||
|
loginUrl.searchParams.set('locale', request.nextUrl.locale || 'ru')
|
||||||
|
return NextResponse.redirect(loginUrl)
|
||||||
|
} */
|
||||||
|
|
||||||
|
return intlResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.next();
|
return NextResponse.next({
|
||||||
|
request: {
|
||||||
|
headers: requestHeaders,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
|
|||||||
Reference in New Issue
Block a user