From 11e46f7352bea9e76c6d6f50a8ee6037ecbb318f Mon Sep 17 00:00:00 2001 From: Untone Date: Fri, 16 May 2025 10:30:02 +0300 Subject: [PATCH] adminpanel login fix --- auth/__init__.py | 2 +- main.py | 10 +-- package-lock.json | 149 ++--------------------------------- package.json | 20 +---- panel/App.tsx | 145 +++++++++++----------------------- panel/admin.tsx | 31 +++++--- panel/auth.ts | 53 ++++++------- panel/graphql.ts | 57 +++++--------- panel/login.tsx | 29 +++---- panel/styles.css | 15 +++- resolvers/pyrightconfig.json | 42 +++++----- tsconfig.json | 3 +- vite.config.ts | 24 +----- 13 files changed, 174 insertions(+), 406 deletions(-) diff --git a/auth/__init__.py b/auth/__init__.py index 3e4a05aa..dc64ef9e 100644 --- a/auth/__init__.py +++ b/auth/__init__.py @@ -43,7 +43,7 @@ async def logout(request: Request): logger.error(f"[auth] logout: Ошибка при отзыве токена: {e}") # Создаем ответ с редиректом на страницу входа - response = RedirectResponse(url="/login") + response = RedirectResponse(url="/") # Удаляем cookie с токеном response.delete_cookie(SESSION_COOKIE_NAME) diff --git a/main.py b/main.py index f2ee9679..bdf67f00 100644 --- a/main.py +++ b/main.py @@ -105,8 +105,8 @@ async def admin_handler(request: Request): """ # Проверяем авторизован ли пользователь if not request.user.is_authenticated: - # Если пользователь не авторизован, перенаправляем на страницу входа - return RedirectResponse(url="/login", status_code=303) + # Если пользователь не авторизован, перенаправляем на главную страницу + return RedirectResponse(url="/", status_code=303) # Проверяем является ли пользователь администратором auth = getattr(request, "auth", None) @@ -199,10 +199,8 @@ async def shutdown(): # Добавляем маршруты статических файлов, если директория существует routes = [] if exists(DIST_DIR): - # Добавляем маршруты для статических ресурсов, если директория dist существует - routes.append(Mount("/assets", app=StaticFiles(directory=join(DIST_DIR, "assets")))) - routes.append(Mount("/chunks", app=StaticFiles(directory=join(DIST_DIR, "chunks")))) - + routes.append(Mount("/", app=StaticFiles(directory=DIST_DIR, html=True))) + # Маршруты для API и веб-приложения routes.extend( [ diff --git a/package-lock.json b/package-lock.json index 2e2cc39e..8fc16004 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,17 +7,11 @@ "": { "name": "publy-admin", "version": "0.4.20", - "dependencies": { - "@solid-primitives/storage": "^4.3.0", - "@solidjs/router": "^0.15.0", - "graphql": "^16.8.0", - "graphql-request": "^6.1.0", - "solid-js": "^1.9.6", - "solid-styled-components": "^0.28.0" - }, "devDependencies": { "@biomejs/biome": "^1.9.4", "@types/node": "^22.15.0", + "graphql": "^16.8.0", + "solid-js": "^1.9.6", "terser": "^5.39.0", "typescript": "^5.8.0", "vite": "^6.3.0", @@ -883,15 +877,6 @@ "node": ">=18" } }, - "node_modules/@graphql-typed-document-node/core": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", - "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", - "license": "MIT", - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", @@ -1236,45 +1221,6 @@ "win32" ] }, - "node_modules/@solid-primitives/storage": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@solid-primitives/storage/-/storage-4.3.2.tgz", - "integrity": "sha512-Vkuk/AqgUOjz6k7Mo5yDFQBQg8KMQcZfaac/bxLApaza3e5c/iNllNvxZWPM9Vf+Gf4m5SgRbvgsm6dSLJ27Jw==", - "license": "MIT", - "dependencies": { - "@solid-primitives/utils": "^6.3.1" - }, - "peerDependencies": { - "@tauri-apps/plugin-store": "*", - "solid-js": "^1.6.12" - }, - "peerDependenciesMeta": { - "@tauri-apps/plugin-store": { - "optional": true - }, - "solid-start": { - "optional": true - } - } - }, - "node_modules/@solid-primitives/utils": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@solid-primitives/utils/-/utils-6.3.1.tgz", - "integrity": "sha512-4/Z59nnwu4MPR//zWZmZm2yftx24jMqQ8CSd/JobL26TPfbn4Ph8GKNVJfGJWShg1QB98qObJSskqizbTvcLLA==", - "license": "MIT", - "peerDependencies": { - "solid-js": "^1.6.12" - } - }, - "node_modules/@solidjs/router": { - "version": "0.15.3", - "resolved": "https://registry.npmjs.org/@solidjs/router/-/router-0.15.3.tgz", - "integrity": "sha512-iEbW8UKok2Oio7o6Y4VTzLj+KFCmQPGEpm1fS3xixwFBdclFVBvaQVeibl1jys4cujfAK5Kn6+uG2uBm3lxOMw==", - "license": "MIT", - "peerDependencies": { - "solid-js": "^1.8.6" - } - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1469,19 +1415,11 @@ "dev": true, "license": "MIT" }, - "node_modules/cross-fetch": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", - "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.7.0" - } - }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -1623,37 +1561,16 @@ "node": ">=4" } }, - "node_modules/goober": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", - "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", - "license": "MIT", - "peerDependencies": { - "csstype": "^3.0.10" - } - }, "node_modules/graphql": { "version": "16.11.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", + "dev": true, "license": "MIT", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, - "node_modules/graphql-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-6.1.0.tgz", - "integrity": "sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==", - "license": "MIT", - "dependencies": { - "@graphql-typed-document-node/core": "^3.2.0", - "cross-fetch": "^3.1.5" - }, - "peerDependencies": { - "graphql": "14 - 16" - } - }, "node_modules/html-entities": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", @@ -1759,26 +1676,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -1902,6 +1799,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.0.tgz", "integrity": "sha512-4tYQDy3HVM0JjJ1CfDK3K8FhBKIDDri27oc2AyabuuHfQw6/yTDPp2Abt1h2cNtf1R0T+7AQYAzPhUgqXztaXw==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -1911,6 +1809,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.0.tgz", "integrity": "sha512-FFu/UE3uA8L1vj0CXXZo2Nlh10MtYoOs0G//ptwlQMjfPFSeIVYUNy0zewfV8iM0CrOebAfHEG6J3xA9c+lsaQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -1923,6 +1822,7 @@ "version": "1.9.6", "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.6.tgz", "integrity": "sha512-PoasAJvLk60hRtOTe9ulvALOdLjjqxuxcGZRolBQqxOnXrBXHGzqMT4ijNhGsDAYdOgEa8ZYaAE94PSldrFSkA==", + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.1.0", @@ -1945,19 +1845,6 @@ "solid-js": "^1.3" } }, - "node_modules/solid-styled-components": { - "version": "0.28.5", - "resolved": "https://registry.npmjs.org/solid-styled-components/-/solid-styled-components-0.28.5.tgz", - "integrity": "sha512-vwTcdp76wZNnESIzB6rRZ3U55NgcSAQXCiiRIiEFhxTFqT0bEh/warNT1qaRZu4OkAzrBkViOngF35ktI8sc4A==", - "license": "MIT", - "dependencies": { - "csstype": "^3.1.0", - "goober": "^2.1.10" - }, - "peerDependencies": { - "solid-js": "^1.4.4" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -2025,12 +1912,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -2209,22 +2090,6 @@ } } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index e27628dd..d2012d86 100644 --- a/package.json +++ b/package.json @@ -7,33 +7,21 @@ "dev": "vite", "build": "vite build", "serve": "vite preview", - "lint": "biome check .", + "lint": "biome check . --fix", "format": "biome format . --write", "type-check": "tsc --noEmit", "test": "vitest", "build:auth": "vite build -c client/auth/vite.config.ts", "watch:auth": "vite build -c client/auth/vite.config.ts --watch" }, - "dependencies": { - "@solidjs/router": "^0.15.0", - "@solid-primitives/storage": "^4.3.0", - "graphql": "^16.8.0", - "graphql-request": "^6.1.0", - "solid-js": "^1.9.6", - "solid-styled-components": "^0.28.0" - }, "devDependencies": { - "@types/node": "^22.15.0", "@biomejs/biome": "^1.9.4", + "@types/node": "^22.15.0", + "graphql": "^16.8.0", + "solid-js": "^1.9.6", "typescript": "^5.8.0", "vite": "^6.3.0", "vite-plugin-solid": "^2.11.0", "terser": "^5.39.0" - }, - "exports": { - ".": { - "import": "./dist/auth.es.js", - "require": "./dist/auth.umd.js" - } } } diff --git a/panel/App.tsx b/panel/App.tsx index 8199930a..3c88eb94 100644 --- a/panel/App.tsx +++ b/panel/App.tsx @@ -1,110 +1,61 @@ -import { Route, Router, RouteSectionProps } from '@solidjs/router' -import { Component, Suspense, lazy } from 'solid-js' +import { Component, Show, Suspense, createSignal, lazy, onMount } from 'solid-js' import { isAuthenticated } from './auth' // Ленивая загрузка компонентов -const LoginPage = lazy(() => import('./login')) const AdminPage = lazy(() => import('./admin')) +const LoginPage = lazy(() => import('./login')) /** - * Компонент корневого шаблона приложения - * @param props - Свойства маршрута, включающие дочерние элементы - */ -const RootLayout: Component = (props) => { - return ( -
- {/* Здесь может быть общий хедер, футер или другие элементы */} - {props.children} -
- ) -} - -/** - * Компонент защиты маршрутов - * Проверяет авторизацию и либо показывает дочерние элементы, - * либо перенаправляет на страницу входа - */ -const RequireAuth: Component = (props) => { - const authed = isAuthenticated() - - if (!authed) { - // Если не авторизован, перенаправляем на /login - window.location.href = '/login' - return ( -
-
-

Перенаправление на страницу входа...

-
- ) - } - - return <>{props.children} -} - -/** - * Компонент для публичных маршрутов с редиректом, - * если пользователь уже авторизован - */ -const PublicOnlyRoute: Component = (props) => { - // Если пользователь авторизован, перенаправляем на админ-панель - if (isAuthenticated()) { - window.location.href = '/admin' - return ( -
-
-

Перенаправление в админ-панель...

-
- ) - } - - return <>{props.children} -} - -/** - * Компонент перенаправления с корневого маршрута - */ -const RootRedirect: Component = () => { - const authenticated = isAuthenticated() - - // Выполняем перенаправление сразу после рендеринга - setTimeout(() => { - window.location.href = authenticated ? '/admin' : '/login' - }, 100) - - return ( -
-
-

Перенаправление...

-
- ) -} - -/** - * Корневой компонент приложения с настроенными маршрутами + * Корневой компонент приложения с простой логикой отображения */ const App: Component = () => { + const [authenticated, setAuthenticated] = createSignal(null) + const [loading, setLoading] = createSignal(true) + + // Проверяем авторизацию при монтировании + onMount(() => { + const authed = isAuthenticated() + setAuthenticated(authed) + setLoading(false) + }) + + // Обработчик успешной авторизации + const handleLoginSuccess = () => { + setAuthenticated(true) + } + + // Обработчик выхода из системы + const handleLogout = () => { + setAuthenticated(false) + } + return ( - - -
-

Загрузка...

- - }> - {/* Корневой маршрут с перенаправлением */} - - - {/* Маршрут логина (только для неавторизованных) */} - - - - - {/* Защищенные маршруты (только для авторизованных) */} - - - +
+ +
+

Загрузка...

+
+ } + > + +
+

Загрузка...

+
+ } + > + {authenticated() ? ( + + ) : ( + + )} +
- +
) } diff --git a/panel/admin.tsx b/panel/admin.tsx index 16d831a1..31c996e2 100644 --- a/panel/admin.tsx +++ b/panel/admin.tsx @@ -3,10 +3,9 @@ * @module AdminPage */ -import { useNavigate } from '@solidjs/router' -import { Component, For, Show, createEffect, createSignal, onCleanup, onMount } from 'solid-js' +import { Component, For, Show, createSignal, onMount } from 'solid-js' +import { logout } from './auth' import { query } from './graphql' -import { isAuthenticated, logout } from './auth' /** * Интерфейс для данных пользователя @@ -52,10 +51,15 @@ interface AdminGetRolesResponse { adminGetRoles: Role[] } +// Интерфейс для пропсов AdminPage +interface AdminPageProps { + onLogout?: () => void +} + /** * Компонент страницы администратора */ -const AdminPage: Component = () => { +const AdminPage: Component = (props) => { const [activeTab, setActiveTab] = createSignal('users') const [users, setUsers] = createSignal([]) const [roles, setRoles] = createSignal([]) @@ -81,8 +85,6 @@ const AdminPage: Component = () => { // Поиск const [searchQuery, setSearchQuery] = createSignal('') - const navigate = useNavigate() - // Периодическая проверка авторизации onMount(() => { // Загружаем данные при монтировании @@ -103,6 +105,7 @@ const AdminPage: Component = () => { const search = searchQuery().trim() const data = await query( + `${location.origin}/graphql`, ` query AdminGetUsers($limit: Int, $offset: Int, $search: String) { adminGetUsers(limit: $limit, offset: $offset, search: $search) { @@ -160,7 +163,9 @@ const AdminPage: Component = () => { */ async function loadRoles() { try { - const data = await query(` + const data = await query( + `${location.origin}/graphql`, + ` query AdminGetRoles { adminGetRoles { id @@ -168,7 +173,8 @@ const AdminPage: Component = () => { description } } - `) + ` + ) if (data?.adminGetRoles) { setRoles(data.adminGetRoles) @@ -249,6 +255,7 @@ const AdminPage: Component = () => { try { await query( + `${location.origin}/graphql`, ` mutation AdminToggleUserBlock($userId: Int!) { adminToggleUserBlock(userId: $userId) { @@ -295,6 +302,7 @@ const AdminPage: Component = () => { try { await query( + `${location.origin}/graphql`, ` mutation AdminToggleUserMute($userId: Int!) { adminToggleUserMute(userId: $userId) { @@ -343,6 +351,7 @@ const AdminPage: Component = () => { async function updateUserRoles(userId: number, newRoles: string[]) { try { await query( + `${location.origin}/graphql`, ` mutation AdminUpdateUser($userId: Int!, $input: AdminUserUpdateInput!) { adminUpdateUser(userId: $userId, input: $input) { @@ -391,8 +400,10 @@ const AdminPage: Component = () => { // Затем выполняем выход logout(() => { - // Для гарантии перенаправления после выхода - window.location.href = '/login' + // Вызываем коллбэк для оповещения родителя о выходе + if (props.onLogout) { + props.onLogout() + } }) } diff --git a/panel/auth.ts b/panel/auth.ts index ae27b2f8..961f4a2b 100644 --- a/panel/auth.ts +++ b/panel/auth.ts @@ -5,6 +5,28 @@ import { query } from './graphql' + +// Константа для имени ключа токена в localStorage +const AUTH_COOKIE_NAME = 'auth_token' + +// Константа для имени ключа токена в cookie +export const AUTH_TOKEN_KEY = 'auth_token' + +/** + * Получает токен авторизации из cookie + * @returns Токен или пустую строку, если токен не найден + */ +export const getAuthTokenFromCookie = (): string => { + const cookieItems = document.cookie.split(';') + for (const item of cookieItems) { + const [name, value] = item.trim().split('=') + if (name === 'auth_token') { + return value + } + } + return '' +} + /** * Интерфейс для учетных данных */ @@ -29,31 +51,6 @@ interface LoginResponse { login: LoginResult } -/** - * Константа для имени ключа токена в localStorage - */ -const AUTH_TOKEN_KEY = 'auth_token' - -/** - * Константа для имени ключа токена в cookie - */ -const AUTH_COOKIE_NAME = 'auth_token' - -/** - * Получает токен авторизации из cookie - * @returns Токен или пустую строку, если токен не найден - */ -function getAuthTokenFromCookie(): string { - const cookieItems = document.cookie.split(';') - for (const item of cookieItems) { - const [name, value] = item.trim().split('=') - if (name === AUTH_COOKIE_NAME) { - return value - } - } - return '' -} - /** * Проверяет, авторизован ли пользователь * @returns Статус авторизации @@ -84,10 +81,10 @@ export function logout(callback?: () => void): void { // Дополнительно пытаемся сделать запрос на сервер для удаления серверных сессий try { - fetch('/logout', { + fetch('/logout', { method: 'GET', credentials: 'include' - }).catch(e => { + }).catch((e) => { console.error('Ошибка при запросе на выход:', e) }) } catch (e) { @@ -107,6 +104,7 @@ export async function login(credentials: Credentials): Promise { try { // Используем query из graphql.ts для выполнения запроса const data = await query( + `${location.origin}/graphql`, ` mutation Login($email: String!, $password: String!) { login(email: $email, password: $password) { @@ -141,3 +139,4 @@ export async function login(credentials: Credentials): Promise { throw error } } + diff --git a/panel/graphql.ts b/panel/graphql.ts index 7368fcbf..5d7738f9 100644 --- a/panel/graphql.ts +++ b/panel/graphql.ts @@ -3,37 +3,13 @@ * @module api */ -/** - * Базовый URL для API - */ -// Всегда используем абсолютный путь к API -const API_URL = window.location.origin + '/graphql' - -/** - * Константа для имени ключа токена в localStorage - */ -const AUTH_TOKEN_KEY = 'auth_token' +import { AUTH_TOKEN_KEY, getAuthTokenFromCookie } from "./auth" /** * Тип для произвольных данных GraphQL */ type GraphQLData = Record -/** - * Получает токен авторизации из cookie - * @returns Токен или пустую строку, если токен не найден - */ -function getAuthTokenFromCookie(): string { - const cookieItems = document.cookie.split(';') - for (const item of cookieItems) { - const [name, value] = item.trim().split('=') - if (name === 'auth_token') { - return value - } - } - return '' -} - /** * Обрабатывает ошибки от API * @param response - Ответ от сервера @@ -74,13 +50,12 @@ async function handleApiError(response: Response): Promise { function hasAuthErrors(errors: Array<{ message?: string; extensions?: { code?: string } }>): boolean { return errors.some( (error) => - (error.message && ( - error.message.toLowerCase().includes('unauthorized') || - error.message.toLowerCase().includes('авторизации') || - error.message.toLowerCase().includes('authentication') || - error.message.toLowerCase().includes('unauthenticated') || - error.message.toLowerCase().includes('token') - )) || + (error.message && + (error.message.toLowerCase().includes('unauthorized') || + error.message.toLowerCase().includes('авторизации') || + error.message.toLowerCase().includes('authentication') || + error.message.toLowerCase().includes('unauthenticated') || + error.message.toLowerCase().includes('token'))) || error.extensions?.code === 'UNAUTHENTICATED' || error.extensions?.code === 'FORBIDDEN' ) @@ -88,11 +63,13 @@ function hasAuthErrors(errors: Array<{ message?: string; extensions?: { code?: s /** * Выполняет GraphQL запрос + * @param url - URL для запроса * @param query - GraphQL запрос * @param variables - Переменные запроса * @returns Результат запроса */ export async function query( + url: string, query: string, variables: Record = {} ): Promise { @@ -103,13 +80,13 @@ export async function query( // Проверяем наличие токена в localStorage const localToken = localStorage.getItem(AUTH_TOKEN_KEY) - + // Проверяем наличие токена в cookie const cookieToken = getAuthTokenFromCookie() - + // Используем токен из localStorage или cookie const token = localToken || cookieToken - + // Если есть токен, добавляем его в заголовок Authorization с префиксом Bearer if (token && token.length > 10) { // В соответствии с логами сервера, формат должен быть: Bearer @@ -118,7 +95,7 @@ export async function query( console.debug('Отправка запроса с токеном авторизации') } - const response = await fetch(API_URL, { + const response = await fetch(url, { method: 'POST', headers, // Важно: credentials: 'include' - для передачи cookies с запросом @@ -141,7 +118,7 @@ export async function query( // Если получен 401 Unauthorized, перенаправляем на страницу входа if (response.status === 401) { localStorage.removeItem(AUTH_TOKEN_KEY) - window.location.href = '/login' + window.location.href = '/' throw new Error('Unauthorized') } @@ -161,7 +138,7 @@ export async function query( // Проверяем ошибки на признаки проблем с авторизацией if (hasAuthErrors(result.errors)) { localStorage.removeItem(AUTH_TOKEN_KEY) - window.location.href = '/login' + window.location.href = '/' throw new Error('Unauthorized') } @@ -177,13 +154,15 @@ export async function query( /** * Выполняет GraphQL мутацию + * @param url - URL для запроса * @param mutation - GraphQL мутация * @param variables - Переменные мутации * @returns Результат мутации */ export function mutate( + url: string, mutation: string, variables: Record = {} ): Promise { - return query(mutation, variables) + return query(url, mutation, variables) } diff --git a/panel/login.tsx b/panel/login.tsx index da94d288..2ba2b8fd 100644 --- a/panel/login.tsx +++ b/panel/login.tsx @@ -3,30 +3,21 @@ * @module LoginPage */ -import { useNavigate } from '@solidjs/router' -import { Component, createSignal, onMount } from 'solid-js' -import { login, isAuthenticated } from './auth' +import { Component, createSignal } from 'solid-js' +import { login } from './auth' + +interface LoginPageProps { + onLoginSuccess?: () => void +} /** * Компонент страницы входа */ -const LoginPage: Component = () => { +const LoginPage: Component = (props) => { const [email, setEmail] = createSignal('') const [password, setPassword] = createSignal('') const [isLoading, setIsLoading] = createSignal(false) const [error, setError] = createSignal(null) - const navigate = useNavigate() - - /** - * Проверка авторизации при загрузке компонента - * и перенаправление если пользователь уже авторизован - */ - onMount(() => { - // Если пользователь уже авторизован, перенаправляем на админ-панель - if (isAuthenticated()) { - window.location.href = '/admin' - } - }) /** * Обработчик отправки формы входа @@ -54,8 +45,10 @@ const LoginPage: Component = () => { }) if (loginSuccessful) { - // Используем прямое перенаправление для надежности - window.location.href = '/admin' + // Вызываем коллбэк для оповещения родителя об успешном входе + if (props.onLoginSuccess) { + props.onLoginSuccess() + } } else { throw new Error('Вход не выполнен') } diff --git a/panel/styles.css b/panel/styles.css index 990465e1..d95f0ef2 100644 --- a/panel/styles.css +++ b/panel/styles.css @@ -584,4 +584,17 @@ button.unmute { flex-direction: column; gap: 10px; } -} \ No newline at end of file +} + +.loading-spinner { + width: 40px; + height: 40px; + border-radius: 50%; + animation: spin 6s linear infinite; + background-color: transparent; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} diff --git a/resolvers/pyrightconfig.json b/resolvers/pyrightconfig.json index c3d0f5cf..42b776ce 100644 --- a/resolvers/pyrightconfig.json +++ b/resolvers/pyrightconfig.json @@ -1,25 +1,19 @@ { - "include": [ - "." - ], - "exclude": [ - "**/node_modules", - "**/__pycache__", - "**/.*" - ], - "defineConstant": { - "DEBUG": true - }, - "venvPath": ".", - "venv": ".venv", - "pythonVersion": "3.11", - "typeCheckingMode": "strict", - "reportMissingImports": true, - "reportMissingTypeStubs": false, - "reportUnknownMemberType": false, - "reportUnknownParameterType": false, - "reportUnknownVariableType": false, - "reportUnknownArgumentType": false, - "reportPrivateUsage": false, - "reportUntypedFunctionDecorator": false -} \ No newline at end of file + "include": ["."], + "exclude": ["**/node_modules", "**/__pycache__", "**/.*", "**/dist"], + "defineConstant": { + "DEBUG": true + }, + "venvPath": ".", + "venv": ".venv", + "pythonVersion": "3.11", + "typeCheckingMode": "strict", + "reportMissingImports": true, + "reportMissingTypeStubs": false, + "reportUnknownMemberType": false, + "reportUnknownParameterType": false, + "reportUnknownVariableType": false, + "reportUnknownArgumentType": false, + "reportPrivateUsage": false, + "reportUntypedFunctionDecorator": false +} diff --git a/tsconfig.json b/tsconfig.json index 46481316..4c016bd4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,8 +16,7 @@ "isolatedModules": true, "lib": ["DOM", "ESNext"], "paths": { - "~/*": ["panel/admin/*"], - "@/*": ["panel/auth/*"] + "~/*": ["./panel/*"] } }, "exclude": [] diff --git a/vite.config.ts b/vite.config.ts index f733b1c5..b4ea22b8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,34 +7,12 @@ const isProd = process.env.NODE_ENV === 'production' export default defineConfig({ plugins: [solidPlugin()], - base: '/', - build: { target: 'esnext', outDir: 'dist', minify: isProd, sourcemap: !isProd, - rollupOptions: { - input: { - main: resolve(__dirname, 'client/index.tsx') - }, - - output: { - // Настройка выходных файлов - entryFileNames: '[name].js', - chunkFileNames: 'chunks/[name].[hash].js', - assetFileNames: 'assets/[name].[hash][extname]', - - // Настройка разделения кода - manualChunks: { - vendor: ['solid-js', '@solidjs/router'], - graphql: ['./client/graphql.ts'], - auth: ['./client/auth.ts'] - } - } - }, - // Оптимизация сборки cssCodeSplit: true, assetsInlineLimit: 4096, @@ -65,7 +43,7 @@ export default defineConfig({ // Настройка алиасов для путей resolve: { alias: { - '@': resolve(__dirname, 'client') + '~': resolve(__dirname, 'panel') } } })