This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { login as apiLogin } from '@/services/authService.js'
|
import { login as apiLogin, checkAuth } from '@/services/authService.js'
|
||||||
|
import { isAuthenticated, getAuthToken, logout } from '@/utils/auth.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
currentPage: String,
|
currentPage: String,
|
||||||
@@ -12,6 +13,20 @@ const login = ref('')
|
|||||||
const password = ref('')
|
const password = ref('')
|
||||||
const authError = ref(false)
|
const authError = ref(false)
|
||||||
const isLoading = 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() {
|
async function handleLogin() {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
@@ -19,6 +34,7 @@ async function handleLogin() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await apiLogin(login.value, password.value)
|
const data = await apiLogin(login.value, password.value)
|
||||||
|
|
||||||
if (data.message === 'Login successful') {
|
if (data.message === 'Login successful') {
|
||||||
emit('update', 'rezylt')
|
emit('update', 'rezylt')
|
||||||
} else {
|
} else {
|
||||||
@@ -44,28 +60,42 @@ function showStartPage() {
|
|||||||
password.value = ''
|
password.value = ''
|
||||||
authError.value = false
|
authError.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
logout()
|
||||||
|
emit('update', 'admin-panel')
|
||||||
|
login.value = ''
|
||||||
|
password.value = ''
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- Показываем форму входа только если страница admin-panel и проверка авторизации завершена -->
|
||||||
<div
|
<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"
|
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>
|
<h2 class="text-xl font-semibold mb-4">Вход для администратора</h2>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="block mb-1 text-sm">Логин</label>
|
<label for="login-input" class="block mb-1 text-sm">Логин</label>
|
||||||
<input
|
<input
|
||||||
|
id="login-input"
|
||||||
|
name="login"
|
||||||
v-model="login"
|
v-model="login"
|
||||||
type="text"
|
type="text"
|
||||||
|
autocomplete="username"
|
||||||
class="border rounded-lg p-2 w-full dark:bg-gray-900 dark:border-gray-600"
|
class="border rounded-lg p-2 w-full dark:bg-gray-900 dark:border-gray-600"
|
||||||
@keydown="handleKeyDown"
|
@keydown="handleKeyDown"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="block mb-1 text-sm">Пароль</label>
|
<label for="password-input" class="block mb-1 text-sm">Пароль</label>
|
||||||
<input
|
<input
|
||||||
|
id="password-input"
|
||||||
|
name="password"
|
||||||
v-model="password"
|
v-model="password"
|
||||||
type="password"
|
type="password"
|
||||||
|
autocomplete="current-password"
|
||||||
class="border rounded-lg p-2 w-full dark:bg-gray-900 dark:border-gray-600"
|
class="border rounded-lg p-2 w-full dark:bg-gray-900 dark:border-gray-600"
|
||||||
@keydown="handleKeyDown"
|
@keydown="handleKeyDown"
|
||||||
/>
|
/>
|
||||||
@@ -89,5 +119,10 @@ function showStartPage() {
|
|||||||
Неверный логин или пароль
|
Неверный логин или пароль
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -21,12 +21,16 @@
|
|||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Поиск..."
|
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"
|
class="w-full h-12 dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 pl-11"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<select
|
<select
|
||||||
v-model="activeCategory"
|
v-model="activeCategory"
|
||||||
@change="onFilterChange"
|
@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"
|
class="w-1/3 h-12 dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 ml-2"
|
||||||
>
|
>
|
||||||
<option value="all">Все</option>
|
<option value="all">Все</option>
|
||||||
@@ -47,11 +51,15 @@
|
|||||||
v-model="newSourceUrl"
|
v-model="newSourceUrl"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="https://example.com"
|
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"
|
class="flex-1 h-12 dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 min-w-0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<select
|
<select
|
||||||
v-model="newSourceCategory"
|
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"
|
class="w-28 h-12 dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 min-w-30"
|
||||||
>
|
>
|
||||||
<option value="" disabled>Категория</option>
|
<option value="" disabled>Категория</option>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
:value="props.source.url"
|
:value="props.source.url"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="URL источника"
|
placeholder="URL источника"
|
||||||
|
:id="'source-url-' + props.source.url"
|
||||||
|
:name="'source_url_' + props.source.url"
|
||||||
readonly
|
readonly
|
||||||
class="flex-1 sm:mr-1 h-12 dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 min-w-0"
|
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"
|
:value="props.source.promt"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Промт"
|
placeholder="Промт"
|
||||||
|
:id="'source-promt-' + props.source.url"
|
||||||
|
:name="'source_promt_' + props.source.url"
|
||||||
readonly
|
readonly
|
||||||
class="h-12 dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 w-1/3 sm:max-w-20"
|
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"
|
v-model="searchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Поиск..."
|
placeholder="Поиск..."
|
||||||
|
id="news-search"
|
||||||
|
name="news_search"
|
||||||
class="dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 pl-11"
|
class="dark:bg-gray-900 border-slate-100 shadow rounded-xl p-3 pl-11"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<select
|
<select
|
||||||
v-model="activeFilter"
|
v-model="activeFilter"
|
||||||
@change="onFilterChange"
|
@change="onFilterChange"
|
||||||
|
id="news-filter"
|
||||||
|
name="news_filter"
|
||||||
class="dark:bg-gray-900 border-slate-100 shadow rounded-xl h-12 p-3"
|
class="dark:bg-gray-900 border-slate-100 shadow rounded-xl h-12 p-3"
|
||||||
>
|
>
|
||||||
<option value="all">Все</option>
|
<option value="all">Все</option>
|
||||||
|
|||||||
@@ -33,6 +33,14 @@ defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
inputId: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<!-- Ввод времени -->
|
<!-- Ввод времени -->
|
||||||
<div class="bg-white p-4 mb-4 flex-colum sm:flex dark:bg-gray-800">
|
<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">
|
<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
|
<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"
|
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"
|
@click="startParser1"
|
||||||
@@ -54,6 +54,8 @@
|
|||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
v-model="prompt"
|
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"
|
class="w-full min-h-96 rounded-xl p-2 border-2 border-neutral-300 dark:bg-gray-900 dark:border-neutral-600"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
<div class="flex flex-col lg:flex-row items-center gap-2 flex-grow">
|
<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">
|
<div class="flex items-center gap-2 w-full lg:w-auto">
|
||||||
<span class="dark:text-neutral-300 whitespace-nowrap">с</span>
|
<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>
|
<span class="dark:text-neutral-300 whitespace-nowrap">по</span>
|
||||||
<DatePicker v-model="dateFinish" />
|
<DatePicker v-model="dateFinish" inputId="date-finish" name="date_finish" />
|
||||||
</div>
|
</div>
|
||||||
<select
|
<select
|
||||||
v-model="selectedFilter"
|
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"
|
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>
|
<option value="status">Избранные</option>
|
||||||
|
|||||||
@@ -1,20 +1,50 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { getAuthToken, logout } from '@/utils/auth.js'
|
||||||
|
|
||||||
const API_BASE_8001 = 'https://allowlgroup.ru/api/8001'
|
const API_BASE_8001 = 'https://allowlgroup.ru/api/8001'
|
||||||
const API_BASE_8002 = 'https://allowlgroup.ru/api/8002'
|
const API_BASE_8002 = 'https://allowlgroup.ru/api/8002'
|
||||||
const API_BASE_8004 = 'https://allowlgroup.ru/api/8004'
|
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({
|
export const api8001 = axios.create({
|
||||||
baseURL: API_BASE_8001,
|
baseURL: API_BASE_8001,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
...commonConfig,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const api8002 = axios.create({
|
export const api8002 = axios.create({
|
||||||
baseURL: API_BASE_8002,
|
baseURL: API_BASE_8002,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
...commonConfig,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const api8004 = axios.create({
|
export const api8004 = axios.create({
|
||||||
baseURL: API_BASE_8004,
|
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,27 @@
|
|||||||
import { api8004 } from './api.js'
|
import { api8004 } from './api.js'
|
||||||
|
import { setAuthToken, setUsername, getAuthToken, getUsername } from '@/utils/auth.js'
|
||||||
|
|
||||||
export async function login(username, password) {
|
export async function login(username, password) {
|
||||||
const { data } = await api8004.post('/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
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверяем авторизацию по наличию cookie (без лишнего запроса)
|
||||||
|
export async function checkAuth() {
|
||||||
|
const token = getAuthToken()
|
||||||
|
if (!token) {
|
||||||
|
return { authenticated: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Просто проверяем наличие токена и возвращаем username
|
||||||
|
return { authenticated: true, user: { username: getUsername() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
101
src/utils/auth.js
Normal file
101
src/utils/auth.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* Утилиты для работы с cookie
|
||||||
|
*/
|
||||||
|
|
||||||
|
const TOKEN_COOKIE_NAME = 'auth_token'
|
||||||
|
const USERNAME_COOKIE_NAME = 'username'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Устанавливает cookie
|
||||||
|
* @param {string} name - имя cookie
|
||||||
|
* @param {string} value - значение cookie
|
||||||
|
* @param {number} days - срок хранения в днях
|
||||||
|
*/
|
||||||
|
function setCookie(name, value, days = 30) {
|
||||||
|
const date = new Date()
|
||||||
|
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000)
|
||||||
|
const expires = `expires=${date.toUTCString()}`
|
||||||
|
document.cookie = `${name}=${encodeURIComponent(value)};${expires};path=/;SameSite=Lax`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает cookie по имени
|
||||||
|
* @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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удаляет cookie
|
||||||
|
* @param {string} name - имя cookie
|
||||||
|
*/
|
||||||
|
function removeCookie(name) {
|
||||||
|
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохраняет токен авторизации (для совместимости)
|
||||||
|
* @param {string} token - токен
|
||||||
|
*/
|
||||||
|
export function setAuthToken(token) {
|
||||||
|
setCookie(TOKEN_COOKIE_NAME, token, 30)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохраняет имя пользователя
|
||||||
|
* @param {string} username - имя пользователя
|
||||||
|
*/
|
||||||
|
export function setUsername(username) {
|
||||||
|
setCookie(USERNAME_COOKIE_NAME, username, 30)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает токен авторизации
|
||||||
|
* @returns {string|null}
|
||||||
|
*/
|
||||||
|
export function getAuthToken() {
|
||||||
|
return getCookie(TOKEN_COOKIE_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает имя пользователя
|
||||||
|
* @returns {string|null}
|
||||||
|
*/
|
||||||
|
export function getUsername() {
|
||||||
|
return getCookie(USERNAME_COOKIE_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, авторизован ли пользователь
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isAuthenticated() {
|
||||||
|
return getAuthToken() !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выполняет выход из системы
|
||||||
|
*/
|
||||||
|
export function logout() {
|
||||||
|
removeCookie(TOKEN_COOKIE_NAME)
|
||||||
|
removeCookie(USERNAME_COOKIE_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Очищает все данные авторизации
|
||||||
|
*/
|
||||||
|
export function clearAuthData() {
|
||||||
|
logout()
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user