Compare commits

4 Commits

Author SHA1 Message Date
c27dce95b5 res
All checks were successful
continuous-integration/drone/push Build is passing
2026-05-31 18:55:10 +10:00
27cc9cc972 ghfdrf
All checks were successful
continuous-integration/drone/push Build is passing
2026-05-31 18:51:35 +10:00
28f443a393 исправление\
All checks were successful
continuous-integration/drone/push Build is passing
2026-05-31 18:30:52 +10:00
eea92e19b9 добавление cookie
All checks were successful
continuous-integration/drone/push Build is passing
2026-05-31 17:50:04 +10:00
10 changed files with 232 additions and 11 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"
/> />

View File

@@ -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>

View File

@@ -33,6 +33,14 @@ defineProps({
type: String, type: String,
default: '', default: '',
}, },
inputId: {
type: String,
default: '',
},
name: {
type: String,
default: '',
},
}) })
</script> </script>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)
}
)

View File

@@ -1,6 +1,42 @@
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
} }
// Проверяем авторизацию через запрос к бэкенду (куки 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
View 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
}