adminpanel login fix
This commit is contained in:
145
panel/App.tsx
145
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<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>
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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('Вход не выполнен')
|
||||
}
|
||||
|
@@ -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); }
|
||||
}
|
||||
|
Reference in New Issue
Block a user