Compare commits
4 Commits
rollback-t
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c27dce95b5 | |||
| 27cc9cc972 | |||
| 28f443a393 | |||
| eea92e19b9 |
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { login as apiLogin } from '@/services/authService.js'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { login as apiLogin, checkAuth } from '@/services/authService.js'
|
||||
import { isAuthenticated, getAuthToken, logout } from '@/utils/auth.js'
|
||||
|
||||
const props = defineProps({
|
||||
currentPage: String,
|
||||
@@ -12,6 +13,20 @@ const login = ref('')
|
||||
const password = ref('')
|
||||
const authError = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const isCheckingAuth = ref(true)
|
||||
|
||||
// Проверка авторизации при загрузке
|
||||
onMounted(async () => {
|
||||
if (isAuthenticated()) {
|
||||
const result = await checkAuth()
|
||||
if (result.authenticated) {
|
||||
emit('update', 'rezylt')
|
||||
} else {
|
||||
logout()
|
||||
}
|
||||
}
|
||||
isCheckingAuth.value = false
|
||||
})
|
||||
|
||||
async function handleLogin() {
|
||||
isLoading.value = true
|
||||
@@ -19,6 +34,7 @@ async function handleLogin() {
|
||||
|
||||
try {
|
||||
const data = await apiLogin(login.value, password.value)
|
||||
|
||||
if (data.message === 'Login successful') {
|
||||
emit('update', 'rezylt')
|
||||
} else {
|
||||
@@ -44,28 +60,42 @@ function showStartPage() {
|
||||
password.value = ''
|
||||
authError.value = false
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
logout()
|
||||
emit('update', 'admin-panel')
|
||||
login.value = ''
|
||||
password.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Показываем форму входа только если страница admin-panel и проверка авторизации завершена -->
|
||||
<div
|
||||
v-if="currentPage === 'admin-panel'"
|
||||
v-if="currentPage === 'admin-panel' && !isCheckingAuth"
|
||||
class="p-6 bg-white rounded-xl shadow max-w-md mx-auto mt-10 dark:bg-gray-800 dark:text-neutral-300"
|
||||
>
|
||||
<h2 class="text-xl font-semibold mb-4">Вход для администратора</h2>
|
||||
<div class="mb-3">
|
||||
<label class="block mb-1 text-sm">Логин</label>
|
||||
<label for="login-input" class="block mb-1 text-sm">Логин</label>
|
||||
<input
|
||||
id="login-input"
|
||||
name="login"
|
||||
v-model="login"
|
||||
type="text"
|
||||
autocomplete="username"
|
||||
class="border rounded-lg p-2 w-full dark:bg-gray-900 dark:border-gray-600"
|
||||
@keydown="handleKeyDown"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block mb-1 text-sm">Пароль</label>
|
||||
<label for="password-input" class="block mb-1 text-sm">Пароль</label>
|
||||
<input
|
||||
id="password-input"
|
||||
name="password"
|
||||
v-model="password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
class="border rounded-lg p-2 w-full dark:bg-gray-900 dark:border-gray-600"
|
||||
@keydown="handleKeyDown"
|
||||
/>
|
||||
@@ -89,5 +119,10 @@ function showStartPage() {
|
||||
Неверный логин или пароль
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Загрузка при проверке авторизации -->
|
||||
<div v-if="isCheckingAuth" class="flex justify-center items-center min-h-screen">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-teal-600"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -21,12 +21,16 @@
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Поиск..."
|
||||
id="source-search"
|
||||
name="source_search"
|
||||
class="w-full h-12 dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 pl-11"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
v-model="activeCategory"
|
||||
@change="onFilterChange"
|
||||
id="source-category"
|
||||
name="source_category"
|
||||
class="w-1/3 h-12 dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 ml-2"
|
||||
>
|
||||
<option value="all">Все</option>
|
||||
@@ -47,11 +51,15 @@
|
||||
v-model="newSourceUrl"
|
||||
type="text"
|
||||
placeholder="https://example.com"
|
||||
id="new-source-url"
|
||||
name="new_source_url"
|
||||
class="flex-1 h-12 dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 min-w-0"
|
||||
/>
|
||||
|
||||
<select
|
||||
v-model="newSourceCategory"
|
||||
id="new-source-category"
|
||||
name="new_source_category"
|
||||
class="w-28 h-12 dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 min-w-30"
|
||||
>
|
||||
<option value="" disabled>Категория</option>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
:value="props.source.url"
|
||||
type="text"
|
||||
placeholder="URL источника"
|
||||
:id="'source-url-' + props.source.url"
|
||||
:name="'source_url_' + props.source.url"
|
||||
readonly
|
||||
class="flex-1 sm:mr-1 h-12 dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 min-w-0"
|
||||
/>
|
||||
@@ -15,6 +17,8 @@
|
||||
:value="props.source.promt"
|
||||
type="text"
|
||||
placeholder="Промт"
|
||||
:id="'source-promt-' + props.source.url"
|
||||
:name="'source_promt_' + props.source.url"
|
||||
readonly
|
||||
class="h-12 dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 w-1/3 sm:max-w-20"
|
||||
/>
|
||||
|
||||
@@ -20,12 +20,16 @@
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Поиск..."
|
||||
id="news-search"
|
||||
name="news_search"
|
||||
class="dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 pl-11"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
v-model="activeFilter"
|
||||
@change="onFilterChange"
|
||||
id="news-filter"
|
||||
name="news_filter"
|
||||
class="dark:bg-gray-900 border-slate-100 shadow rounded-xl h-12 p-3"
|
||||
>
|
||||
<option value="all">Все</option>
|
||||
|
||||
@@ -33,6 +33,14 @@ defineProps({
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
inputId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- Ввод времени -->
|
||||
<div class="bg-white p-4 mb-4 flex-colum sm:flex dark:bg-gray-800">
|
||||
<div class="w-full sm:max-w-100 flex">
|
||||
<DatePicker v-model="time" placeholder="01.01.2026" />
|
||||
<DatePicker v-model="time" placeholder="01.01.2026" inputId="parser-date" name="parser_date" />
|
||||
<button
|
||||
class="dark:bg-orange-500 hover:dark:bg-orange-600 ml-4 shadow text-white bg-sky-700 w-1/2 hover:bg-sky-900 rounded-xl px-2 min-h-11 cursor-pointer"
|
||||
@click="startParser1"
|
||||
@@ -54,6 +54,8 @@
|
||||
|
||||
<textarea
|
||||
v-model="prompt"
|
||||
id="prompt-textarea"
|
||||
name="prompt"
|
||||
class="w-full min-h-96 rounded-xl p-2 border-2 border-neutral-300 dark:bg-gray-900 dark:border-neutral-600"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
<div class="flex flex-col lg:flex-row items-center gap-2 flex-grow">
|
||||
<div class="flex items-center gap-2 w-full lg:w-auto">
|
||||
<span class="dark:text-neutral-300 whitespace-nowrap">с</span>
|
||||
<DatePicker v-model="dateStart" />
|
||||
<DatePicker v-model="dateStart" inputId="date-start" name="date_start" />
|
||||
<span class="dark:text-neutral-300 whitespace-nowrap">по</span>
|
||||
<DatePicker v-model="dateFinish" />
|
||||
<DatePicker v-model="dateFinish" inputId="date-finish" name="date_finish" />
|
||||
</div>
|
||||
<select
|
||||
v-model="selectedFilter"
|
||||
id="filter-select"
|
||||
name="filter"
|
||||
class="dark:bg-gray-900 border-slate-100 shadow rounded-xl h-12 p-3 mt-3 lg:mt-0 lg:ml-4 w-full lg:w-auto flex-shrink-0 cursor-pointer"
|
||||
>
|
||||
<option value="status">Избранные</option>
|
||||
|
||||
@@ -1,20 +1,50 @@
|
||||
import axios from 'axios'
|
||||
import { getAuthToken, logout } from '@/utils/auth.js'
|
||||
|
||||
const API_BASE_8001 = 'https://allowlgroup.ru/api/8001'
|
||||
const API_BASE_8002 = 'https://allowlgroup.ru/api/8002'
|
||||
const API_BASE_8004 = 'https://allowlgroup.ru/api/8004'
|
||||
|
||||
// Настройка с withCredentials для работы с cookie
|
||||
// Настройка без withCredentials для теста
|
||||
const commonConfig = {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
withCredentials: true, // Важно для отправки cookie
|
||||
// withCredentials: true, # Временно отключено для теста
|
||||
}
|
||||
|
||||
export const api8001 = axios.create({
|
||||
baseURL: API_BASE_8001,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
...commonConfig,
|
||||
})
|
||||
|
||||
export const api8002 = axios.create({
|
||||
baseURL: API_BASE_8002,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
...commonConfig,
|
||||
})
|
||||
|
||||
export const api8004 = axios.create({
|
||||
baseURL: API_BASE_8004,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
...commonConfig,
|
||||
})
|
||||
|
||||
// Добавляем токен из cookie в заголовки для защищённых запросов
|
||||
api8004.interceptors.request.use((config) => {
|
||||
const token = getAuthToken()
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
// Обработка ошибок 401 - автоматический выход
|
||||
api8004.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
logout()
|
||||
window.location.reload()
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,6 +1,42 @@
|
||||
import { api8004 } from './api.js'
|
||||
import { setAuthToken, setUsername, getAuthToken, getUsername } from '@/utils/auth.js'
|
||||
|
||||
export async function login(username, password) {
|
||||
const { data } = await api8004.post('/login', { username, password })
|
||||
|
||||
// Бэкенд сам устанавливает cookie, нам нужно только убедиться что токен сохранён
|
||||
if (data.message === 'Login successful' && data.token) {
|
||||
setAuthToken(data.token)
|
||||
setUsername(username)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// Проверяем авторизацию через запрос к бэкенду (куки HttpOnly не читаются JS)
|
||||
export async function checkAuth() {
|
||||
try {
|
||||
// Проверяем токен в памяти
|
||||
const token = getAuthToken()
|
||||
if (!token) {
|
||||
return { authenticated: false }
|
||||
}
|
||||
|
||||
// Делаем запрос к /verify для валидации токена/куки
|
||||
const response = await api8004.get('/verify', {
|
||||
validateStatus: () => true
|
||||
})
|
||||
|
||||
if (response.status === 200 && response.data?.user?.username) {
|
||||
// Сохраняем username из ответа
|
||||
setUsername(response.data.user.username)
|
||||
return { authenticated: true, user: { username: response.data.user.username } }
|
||||
}
|
||||
|
||||
return { authenticated: false }
|
||||
} catch (error) {
|
||||
return { authenticated: false }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
92
src/utils/auth.js
Normal file
92
src/utils/auth.js
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Утилиты для работы с cookie
|
||||
* Примечание: auth_token и username устанавливаются бэкендом через Set-Cookie
|
||||
* Фронтенд только читает их через API ответы
|
||||
*/
|
||||
|
||||
const TOKEN_COOKIE_NAME = 'auth_token'
|
||||
const USERNAME_COOKIE_NAME = 'username'
|
||||
|
||||
// Временное хранилище для токена (до тех пор пока бэкенд не вернёт куки)
|
||||
let tempToken = null
|
||||
let tempUsername = null
|
||||
|
||||
/**
|
||||
* Получает cookie по имени (только для не-Httponly куки)
|
||||
* @param {string} name - имя cookie
|
||||
* @returns {string|null} значение cookie или null
|
||||
*/
|
||||
function getCookie(name) {
|
||||
const nameEQ = `${name}=`
|
||||
const ca = document.cookie.split(';')
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i]
|
||||
while (c.charAt(0) === ' ') c = c.substring(1)
|
||||
if (c.indexOf(nameEQ) === 0) {
|
||||
return decodeURIComponent(c.substring(nameEQ.length, c.length))
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохраняет токен авторизации (для совместимости)
|
||||
* @param {string} token - токен
|
||||
*/
|
||||
export function setAuthToken(token) {
|
||||
tempToken = token
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохраняет имя пользователя
|
||||
* @param {string} username - имя пользователя
|
||||
*/
|
||||
export function setUsername(username) {
|
||||
tempUsername = username
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает токен авторизации
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function getAuthToken() {
|
||||
return tempToken || getCookie(TOKEN_COOKIE_NAME)
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает имя пользователя
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function getUsername() {
|
||||
return tempUsername || getCookie(USERNAME_COOKIE_NAME)
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, авторизован ли пользователь
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isAuthenticated() {
|
||||
return getAuthToken() !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполняет выход из системы
|
||||
*/
|
||||
export function logout() {
|
||||
tempToken = null
|
||||
tempUsername = null
|
||||
// Очистка через API (бэкенд удалит куки)
|
||||
return fetch('/api/8004/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Очищает все данные авторизации
|
||||
*/
|
||||
export function clearAuthData() {
|
||||
tempToken = null
|
||||
tempUsername = null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user