diff --git a/package-lock.json b/package-lock.json index 8f55da3..3231a52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,11 +11,13 @@ "@tanstack/react-query": "^5.90.11", "@tanstack/react-table": "^8.21.3", "clsx": "^2.1.1", + "jose": "^6.1.2", "next": "16.1.2", "next-intl": "^4.7.0", "react": "19.2.3", "react-dom": "19.2.3", - "sonner": "^2.0.7" + "sonner": "^2.0.7", + "zod": "^4.1.13" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.18", @@ -1803,6 +1805,15 @@ "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": { "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", @@ -2550,6 +2561,15 @@ "peerDependencies": { "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" + } } } } diff --git a/package.json b/package.json index 56ed15a..25991da 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,13 @@ "@tanstack/react-query": "^5.90.11", "@tanstack/react-table": "^8.21.3", "clsx": "^2.1.1", + "jose": "^6.1.2", "next": "16.1.2", "next-intl": "^4.7.0", "react": "19.2.3", "react-dom": "19.2.3", - "sonner": "^2.0.7" + "sonner": "^2.0.7", + "zod": "^4.1.13" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.18", diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 2ef3196..731123d 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -6,10 +6,11 @@ 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 AdminNavLinks from '@/app/ui/navigation/admin-nav-links' -import AdminHeaderPanel from '@/app/ui/admin-header-panel'; +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 = { @@ -17,7 +18,7 @@ export const metadata: Metadata = { description: "Admin panel no copy", }; -export function generateStaticParams() { +export async function generateStaticParams() { return routing.locales.map((locale: any) => ({ locale })); } @@ -29,6 +30,7 @@ export default async function RootLayout({ params: Promise<{ locale: string }>; }>) { const { locale } = await params; + if (!hasLocale(routing.locales, locale)) { notFound(); } @@ -38,21 +40,13 @@ export default async function RootLayout({ return ( - - - - - -
- -
- {children} -
-
- -
-
-
- + + + + {children} + + + + ); -} +} \ No newline at end of file diff --git a/src/app/[locale]/login/layout.tsx b/src/app/[locale]/login/layout.tsx new file mode 100644 index 0000000..3ef8b22 --- /dev/null +++ b/src/app/[locale]/login/layout.tsx @@ -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 ( +
+
+ {children} +
+
+ ); +} diff --git a/src/app/[locale]/login/page.tsx b/src/app/[locale]/login/page.tsx new file mode 100644 index 0000000..f0ee5df --- /dev/null +++ b/src/app/[locale]/login/page.tsx @@ -0,0 +1,18 @@ +import LoginForm from '@/app/ui/forms/login-form'; +import Link from 'next/link'; + +export default function Page() { + return ( +
+ + +
+ + + link + +
+ ) +} \ No newline at end of file diff --git a/src/app/[locale]/analitic/page.tsx b/src/app/[locale]/pages/analitic/page.tsx similarity index 100% rename from src/app/[locale]/analitic/page.tsx rename to src/app/[locale]/pages/analitic/page.tsx diff --git a/src/app/[locale]/api-control/page.tsx b/src/app/[locale]/pages/api-control/page.tsx similarity index 100% rename from src/app/[locale]/api-control/page.tsx rename to src/app/[locale]/pages/api-control/page.tsx diff --git a/src/app/[locale]/content/page.tsx b/src/app/[locale]/pages/content/page.tsx similarity index 100% rename from src/app/[locale]/content/page.tsx rename to src/app/[locale]/pages/content/page.tsx diff --git a/src/app/[locale]/dmc/page.tsx b/src/app/[locale]/pages/dmc/page.tsx similarity index 100% rename from src/app/[locale]/dmc/page.tsx rename to src/app/[locale]/pages/dmc/page.tsx diff --git a/src/app/[locale]/email-notification/page.tsx b/src/app/[locale]/pages/email-notification/page.tsx similarity index 100% rename from src/app/[locale]/email-notification/page.tsx rename to src/app/[locale]/pages/email-notification/page.tsx diff --git a/src/app/[locale]/pages/layout.tsx b/src/app/[locale]/pages/layout.tsx new file mode 100644 index 0000000..d102dc0 --- /dev/null +++ b/src/app/[locale]/pages/layout.tsx @@ -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 ( + +
+ + {children} + +
+
+ ); +} \ No newline at end of file diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/pages/page.tsx similarity index 100% rename from src/app/[locale]/page.tsx rename to src/app/[locale]/pages/page.tsx diff --git a/src/app/[locale]/subscriptions/page.tsx b/src/app/[locale]/pages/subscriptions/page.tsx similarity index 100% rename from src/app/[locale]/subscriptions/page.tsx rename to src/app/[locale]/pages/subscriptions/page.tsx diff --git a/src/app/[locale]/system-logs/page.tsx b/src/app/[locale]/pages/system-logs/page.tsx similarity index 100% rename from src/app/[locale]/system-logs/page.tsx rename to src/app/[locale]/pages/system-logs/page.tsx diff --git a/src/app/[locale]/system/page.tsx b/src/app/[locale]/pages/system/page.tsx similarity index 100% rename from src/app/[locale]/system/page.tsx rename to src/app/[locale]/pages/system/page.tsx diff --git a/src/app/[locale]/tariff-management/page.tsx b/src/app/[locale]/pages/tariff-management/page.tsx similarity index 100% rename from src/app/[locale]/tariff-management/page.tsx rename to src/app/[locale]/pages/tariff-management/page.tsx diff --git a/src/app/[locale]/users/page.tsx b/src/app/[locale]/pages/users/page.tsx similarity index 100% rename from src/app/[locale]/users/page.tsx rename to src/app/[locale]/pages/users/page.tsx diff --git a/src/app/[locale]/violations/page.tsx b/src/app/[locale]/pages/violations/page.tsx similarity index 100% rename from src/app/[locale]/violations/page.tsx rename to src/app/[locale]/pages/violations/page.tsx diff --git a/src/app/actions/auth.ts b/src/app/actions/auth.ts new file mode 100644 index 0000000..c27270f --- /dev/null +++ b/src/app/actions/auth.ts @@ -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 + } | 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 = {} + 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 = {} + + 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(); + } +} \ No newline at end of file diff --git a/src/app/actions/definitions.ts b/src/app/actions/definitions.ts new file mode 100644 index 0000000..2d75c55 --- /dev/null +++ b/src/app/actions/definitions.ts @@ -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'; \ No newline at end of file diff --git a/src/app/actions/session.ts b/src/app/actions/session.ts new file mode 100644 index 0000000..dbd8b07 --- /dev/null +++ b/src/app/actions/session.ts @@ -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 { + 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'); + } +} \ No newline at end of file diff --git a/src/app/components/admin-layout-wrapper.tsx b/src/app/components/admin-layout-wrapper.tsx new file mode 100644 index 0000000..7c06aeb --- /dev/null +++ b/src/app/components/admin-layout-wrapper.tsx @@ -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 ( + <> + +
+ +
+ {children} +
+
+ + ); + } + + return
{children}
; +} \ No newline at end of file diff --git a/src/app/styles/module/login.module.scss b/src/app/styles/module/login.module.scss new file mode 100644 index 0000000..cfd57fc --- /dev/null +++ b/src/app/styles/module/login.module.scss @@ -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; +} \ No newline at end of file diff --git a/src/app/ui/forms/login-form.tsx b/src/app/ui/forms/login-form.tsx new file mode 100644 index 0000000..a93d601 --- /dev/null +++ b/src/app/ui/forms/login-form.tsx @@ -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(null); + const queryClient = useQueryClient(); + + useEffect(() => { + queryClient.clear(); + }, []) + + return ( +
+
+ + { + e.target.value = e.target.value.toLocaleLowerCase(); + }} + /> + {state?.error?.email && ( +

+ {t(state?.error?.email)} +

+ )} +
+
+ +
+ + +
+ {state?.error?.password && ( +

+ {t(state?.error?.password)} +

+ )} +
+ {state?.error.server && ( +

+ {t(state?.error.server)} +

+ )} +
+
+ + +
+ + {t('forgot-password')}? + +
+ +
+ ) +} \ No newline at end of file diff --git a/src/app/ui/navigation/admin-nav-links.tsx b/src/app/ui/navigation/admin-nav-links.tsx index 159f257..921a1d3 100644 --- a/src/app/ui/navigation/admin-nav-links.tsx +++ b/src/app/ui/navigation/admin-nav-links.tsx @@ -4,70 +4,73 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; import clsx from 'clsx'; import { useTranslations } from 'next-intl'; +import { logout } from '@/app/actions/auth'; +import { useQueryClient } from '@tanstack/react-query'; export default function AdminNavLinks() { const pathname = usePathname().replace('/en/', '/').replace('/ru/', '/'); const t = useTranslations('Global'); + const queryClient = useQueryClient(); const links = [ { 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' }, { 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' }, { 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' }, { 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' }, { 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' }, { 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' }, { 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' }, { name: 'email notification', - href: '/email-notification', + href: '/pages/email-notification', img: 'M2 20V4h20v16zm10-7L4 8v10h16V8zm0-2l8-5H4zM4 8V6v12z' }, { 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' }, { 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' }, { 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' }, { 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' } ]; @@ -104,8 +107,10 @@ export default function AdminNavLinks() { key={t('exit')} href='/login' className="admin-nav-item" - onClick={(e) => { + onClick={async (e) => { e.preventDefault(); + queryClient.clear(); + await logout(); }} > diff --git a/src/proxy.ts b/src/proxy.ts index 309b97c..1ef2950 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -2,12 +2,17 @@ import { NextResponse, NextRequest } from 'next/server'; import { cookies } from 'next/headers' import createIntlMiddleware from 'next-intl/middleware' import { routing } from '@/i18n/routing' +import { decrypt } from '@/app/actions/session'; const intlMiddleware = createIntlMiddleware(routing); export default async function proxy(request: NextRequest) { const path = request.nextUrl.pathname; + const cookie = (await cookies()).get('session')?.value; + const session = await decrypt(cookie); + console.log(session); + if ( path.startsWith('/_next') || path.includes('.') @@ -15,13 +20,29 @@ export default async function proxy(request: NextRequest) { return NextResponse.next() } + const requestHeaders = new Headers(request.headers); + requestHeaders.set('x-pathname', path); + const intlResponse = intlMiddleware(request); 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 = {