adminpanel login fix

This commit is contained in:
2025-05-16 10:30:02 +03:00
parent 2d382be794
commit 11e46f7352
13 changed files with 174 additions and 406 deletions

View File

@@ -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<RouteSectionProps> = (props) => {
return (
<div class="app-container">
{/* Здесь может быть общий хедер, футер или другие элементы */}
{props.children}
</div>
)
}
/**
* Компонент защиты маршрутов
* Проверяет авторизацию и либо показывает дочерние элементы,
* либо перенаправляет на страницу входа
*/
const RequireAuth: Component<RouteSectionProps> = (props) => {
const authed = isAuthenticated()
if (!authed) {
// Если не авторизован, перенаправляем на /login
window.location.href = '/login'
return (
<div class="loading-screen">
<div class="loading-spinner"></div>
<h2>Перенаправление на страницу входа...</h2>
</div>
)
}
return <>{props.children}</>
}
/**
* Компонент для публичных маршрутов с редиректом,
* если пользователь уже авторизован
*/
const PublicOnlyRoute: Component<RouteSectionProps> = (props) => {
// Если пользователь авторизован, перенаправляем на админ-панель
if (isAuthenticated()) {
window.location.href = '/admin'
return (
<div class="loading-screen">
<div class="loading-spinner"></div>
<h2>Перенаправление в админ-панель...</h2>
</div>
)
}
return <>{props.children}</>
}
/**
* Компонент перенаправления с корневого маршрута
*/
const RootRedirect: Component = () => {
const authenticated = isAuthenticated()
// Выполняем перенаправление сразу после рендеринга
setTimeout(() => {
window.location.href = authenticated ? '/admin' : '/login'
}, 100)
return (
<div class="loading-screen">
<div class="loading-spinner"></div>
<h2>Перенаправление...</h2>
</div>
)
}
/**
* Корневой компонент приложения с настроенными маршрутами
* Корневой компонент приложения с простой логикой отображения
*/
const App: Component = () => {
const [authenticated, setAuthenticated] = createSignal<boolean | null>(null)
const [loading, setLoading] = createSignal(true)
// Проверяем авторизацию при монтировании
onMount(() => {
const authed = isAuthenticated()
setAuthenticated(authed)
setLoading(false)
})
// Обработчик успешной авторизации
const handleLoginSuccess = () => {
setAuthenticated(true)
}
// Обработчик выхода из системы
const handleLogout = () => {
setAuthenticated(false)
}
return (
<Router root={RootLayout}>
<Suspense fallback={
<div class="loading-screen">
<div class="loading-spinner"></div>
<h2>Загрузка...</h2>
</div>
}>
{/* Корневой маршрут с перенаправлением */}
<Route path="/" component={RootRedirect} />
{/* Маршрут логина (только для неавторизованных) */}
<Route path="/login" component={PublicOnlyRoute}>
<Route path="/" component={LoginPage} />
</Route>
{/* Защищенные маршруты (только для авторизованных) */}
<Route path="/admin" component={RequireAuth}>
<Route path="/*" component={AdminPage} />
</Route>
<div class="app-container">
<Suspense
fallback={
<div class="loading-screen">
<div class="loading-spinner" />
<h2>Загрузка...</h2>
</div>
}
>
<Show
when={!loading()}
fallback={
<div class="loading-screen">
<div class="loading-spinner" />
<h2>Загрузка...</h2>
</div>
}
>
{authenticated() ? (
<AdminPage onLogout={handleLogout} />
) : (
<LoginPage onLoginSuccess={handleLoginSuccess} />
)}
</Show>
</Suspense>
</Router>
</div>
)
}

View File

@@ -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<AdminPageProps> = (props) => {
const [activeTab, setActiveTab] = createSignal('users')
const [users, setUsers] = createSignal<User[]>([])
const [roles, setRoles] = createSignal<Role[]>([])
@@ -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<AdminGetUsersResponse>(
`${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<AdminGetRolesResponse>(`
const data = await query<AdminGetRolesResponse>(
`${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()
}
})
}

View File

@@ -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<boolean> {
try {
// Используем query из graphql.ts для выполнения запроса
const data = await query<LoginResponse>(
`${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<boolean> {
throw error
}
}

View File

@@ -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<string, unknown>
/**
* Получает токен авторизации из 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<string> {
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<T = GraphQLData>(
url: string,
query: string,
variables: Record<string, unknown> = {}
): Promise<T> {
@@ -103,13 +80,13 @@ export async function query<T = GraphQLData>(
// Проверяем наличие токена в 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 <token>
@@ -118,7 +95,7 @@ export async function query<T = GraphQLData>(
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<T = GraphQLData>(
// Если получен 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<T = GraphQLData>(
// Проверяем ошибки на признаки проблем с авторизацией
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<T = GraphQLData>(
/**
* Выполняет GraphQL мутацию
* @param url - URL для запроса
* @param mutation - GraphQL мутация
* @param variables - Переменные мутации
* @returns Результат мутации
*/
export function mutate<T = GraphQLData>(
url: string,
mutation: string,
variables: Record<string, unknown> = {}
): Promise<T> {
return query<T>(mutation, variables)
return query<T>(url, mutation, variables)
}

View File

@@ -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<LoginPageProps> = (props) => {
const [email, setEmail] = createSignal('')
const [password, setPassword] = createSignal('')
const [isLoading, setIsLoading] = createSignal(false)
const [error, setError] = createSignal<string | null>(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('Вход не выполнен')
}

View File

@@ -584,4 +584,17 @@ button.unmute {
flex-direction: column;
gap: 10px;
}
}
}
.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); }
}