1
0

Доработка сессий

This commit is contained in:
2026-01-18 21:29:28 +07:00
parent 6928687982
commit 7d7b817d7e
7 changed files with 126 additions and 67 deletions

View File

@@ -24,7 +24,7 @@ if ($method === 'POST') {
RestApi::response($result); RestApi::response($result);
} }
// Выход (удаление всех сессий) // Выход (только текущая сессия)
if ($action === 'logout') { if ($action === 'logout') {
$account = new Account(); $account = new Account();
$keycookies = $data['keycookies'] ?? $_COOKIE['session'] ?? null; $keycookies = $data['keycookies'] ?? $_COOKIE['session'] ?? null;
@@ -32,6 +32,14 @@ if ($method === 'POST') {
RestApi::response($result); RestApi::response($result);
} }
// Выход со всех устройств
if ($action === 'logout_all') {
$account = new Account();
$keycookies = $data['keycookies'] ?? $_COOKIE['session'] ?? null;
$result = $account->logout_all($keycookies);
RestApi::response($result);
}
// Создание пользователя // Создание пользователя
if ($action === 'create_user') { if ($action === 'create_user') {
$account = new Account(); $account = new Account();

View File

@@ -196,7 +196,7 @@ class Account extends BaseEntity {
]; ];
} }
// Удаление всех сессий пользователя (logout) // Удаление только текущей сессии (logout)
public function logout($keycookies) { public function logout($keycookies) {
// Проверяем, что сессия не пустая // Проверяем, что сессия не пустая
if (!$keycookies) { if (!$keycookies) {
@@ -204,6 +204,28 @@ class Account extends BaseEntity {
return $this->getErrors(); return $this->getErrors();
} }
// Удаляем только текущую сессию
Database::delete($this->db_name_session, [
'keycookies' => $keycookies
]);
// Удаляем cookie
setcookie('session', '', [
'expires' => time() - 3600,
'path' => '/'
]);
return ['success' => true];
}
// Удаление всех сессий пользователя (logout со всех устройств)
public function logout_all($keycookies) {
// Проверяем, что сессия не пустая
if (!$keycookies) {
$this->addError('session', 'Сессия не указана');
return $this->getErrors();
}
// Получаем сессию чтобы узнать id пользователя // Получаем сессию чтобы узнать id пользователя
$session = Database::get($this->db_name_session, ['id_accounts'], [ $session = Database::get($this->db_name_session, ['id_accounts'], [
'keycookies' => $keycookies 'keycookies' => $keycookies

View File

@@ -54,7 +54,13 @@ export const authApi = {
credentials: 'include', credentials: 'include',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'logout' }) body: JSON.stringify({ action: 'logout' })
}, true) // skipSessionCheck — это выход }, true), // skipSessionCheck — это выход
logoutAll: () => request('/api/user', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'logout_all' })
}, true) // skipSessionCheck — выход со всех устройств
} }
// ==================== PROJECTS ==================== // ==================== PROJECTS ====================

View File

@@ -11,10 +11,12 @@
<button <button
v-if="dialogShowDiscard" v-if="dialogShowDiscard"
class="btn-discard" class="btn-discard"
:class="dialogDiscardVariant"
@click="handleDiscard" @click="handleDiscard"
:disabled="loading" :disabled="loading"
> >
{{ dialogDiscardText }} <span v-if="loading === 'discard'" class="btn-loader"></span>
<span v-else>{{ dialogDiscardText }}</span>
</button> </button>
<button <button
class="btn-confirm" class="btn-confirm"
@@ -22,7 +24,7 @@
@click="handleConfirm" @click="handleConfirm"
:disabled="loading" :disabled="loading"
> >
<span v-if="loading" class="btn-loader"></span> <span v-if="loading === 'confirm'" class="btn-loader"></span>
<span v-else>{{ dialogConfirmText }}</span> <span v-else>{{ dialogConfirmText }}</span>
</button> </button>
</div> </div>
@@ -56,10 +58,16 @@ const props = defineProps({
default: undefined default: undefined
}, },
variant: String, variant: String,
discardVariant: String,
// Async callback для подтверждения — сам управляет loading // Async callback для подтверждения — сам управляет loading
action: { action: {
type: Function, type: Function,
default: null default: null
},
// Async callback для discard (опционально)
discardAction: {
type: Function,
default: null
} }
}) })
@@ -71,19 +79,20 @@ const dialogTitle = computed(() => props.title ?? config.value.title ?? 'Под
const dialogMessage = computed(() => props.message ?? config.value.message ?? 'Вы уверены?') const dialogMessage = computed(() => props.message ?? config.value.message ?? 'Вы уверены?')
const dialogConfirmText = computed(() => props.confirmText ?? config.value.confirmText ?? 'Подтвердить') const dialogConfirmText = computed(() => props.confirmText ?? config.value.confirmText ?? 'Подтвердить')
const dialogCancelText = computed(() => props.cancelText ?? 'Отмена') const dialogCancelText = computed(() => props.cancelText ?? 'Отмена')
const dialogDiscardText = computed(() => props.discardText ?? 'Не сохранять') const dialogDiscardText = computed(() => props.discardText ?? config.value.discardText ?? 'Не сохранять')
const dialogShowDiscard = computed(() => props.showDiscard ?? config.value.showDiscard ?? false) const dialogShowDiscard = computed(() => props.showDiscard ?? config.value.showDiscard ?? false)
const dialogVariant = computed(() => props.variant ?? config.value.variant ?? 'default') const dialogVariant = computed(() => props.variant ?? config.value.variant ?? 'default')
const dialogDiscardVariant = computed(() => props.discardVariant ?? config.value.discardVariant ?? 'default')
const emit = defineEmits(['confirm', 'cancel', 'discard']) const emit = defineEmits(['confirm', 'cancel', 'discard'])
// Внутреннее состояние загрузки // Внутреннее состояние загрузки: null | 'confirm' | 'discard'
const loading = ref(false) const loading = ref(null)
// Сброс состояния при закрытии диалога // Сброс состояния при закрытии диалога
watch(() => props.show, (newVal) => { watch(() => props.show, (newVal) => {
if (!newVal) { if (!newVal) {
loading.value = false loading.value = null
} }
}) })
@@ -92,7 +101,7 @@ const handleConfirm = async () => {
// Если есть async action — вызываем его и управляем loading // Если есть async action — вызываем его и управляем loading
if (props.action) { if (props.action) {
loading.value = true loading.value = 'confirm'
try { try {
await props.action() await props.action()
// Успех — эмитим confirm для закрытия // Успех — эмитим confirm для закрытия
@@ -101,7 +110,7 @@ const handleConfirm = async () => {
console.error('ConfirmDialog action failed:', e) console.error('ConfirmDialog action failed:', e)
// При ошибке — не закрываем диалог // При ошибке — не закрываем диалог
} finally { } finally {
loading.value = false loading.value = null
} }
} else { } else {
// Простой режим — просто эмитим // Простой режим — просто эмитим
@@ -114,9 +123,26 @@ const handleCancel = () => {
emit('cancel') emit('cancel')
} }
const handleDiscard = () => { const handleDiscard = async () => {
if (loading.value) return if (loading.value) return
// Если есть async discardAction — вызываем его и управляем loading
if (props.discardAction) {
loading.value = 'discard'
try {
await props.discardAction()
// Успех — эмитим discard для закрытия
emit('discard') emit('discard')
} catch (e) {
console.error('ConfirmDialog discardAction failed:', e)
// При ошибке — не закрываем диалог
} finally {
loading.value = null
}
} else {
// Простой режим — просто эмитим
emit('discard')
}
} }
</script> </script>
@@ -196,6 +222,30 @@ const handleDiscard = () => {
background: rgba(239, 68, 68, 0.25); background: rgba(239, 68, 68, 0.25);
} }
.btn-discard.warning {
background: rgba(245, 158, 11, 0.15);
color: #fbbf24;
}
.btn-discard.warning:hover {
background: rgba(245, 158, 11, 0.25);
color: #fcd34d;
}
.btn-discard.danger {
background: #ef4444;
color: #fff;
}
.btn-discard.danger:hover {
background: #dc2626;
}
.btn-discard:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.btn-confirm { .btn-confirm {
background: var(--accent); background: var(--accent);
color: #000; color: #000;
@@ -206,21 +256,23 @@ const handleDiscard = () => {
} }
.btn-confirm.danger { .btn-confirm.danger {
background: #ef4444; background: rgba(239, 68, 68, 0.15);
color: #fff; color: #f87171;
} }
.btn-confirm.danger:hover { .btn-confirm.danger:hover {
background: #dc2626; background: rgba(239, 68, 68, 0.25);
color: #fca5a5;
} }
.btn-confirm.warning { .btn-confirm.warning {
background: #f59e0b; background: rgba(245, 158, 11, 0.15);
color: #000; color: #fbbf24;
} }
.btn-confirm.warning:hover { .btn-confirm.warning:hover {
background: #d97706; background: rgba(245, 158, 11, 0.25);
color: #fcd34d;
} }
.btn-confirm:disabled { .btn-confirm:disabled {

View File

@@ -11,7 +11,8 @@
<ConfirmDialog <ConfirmDialog
v-model:show="showDialog" v-model:show="showDialog"
type="logout" type="logout"
:action="logout" :action="logoutAll"
:discard-action="logoutCurrent"
/> />
</template> </template>
@@ -38,13 +39,22 @@ const router = useRouter()
const store = useProjectsStore() const store = useProjectsStore()
const showDialog = ref(false) const showDialog = ref(false)
const logout = async () => { // Выход с текущей сессии (discard action)
const logoutCurrent = async () => {
clearAuthCache() clearAuthCache()
await authApi.logout() await authApi.logout()
store.reset() store.reset()
router.push('/login') router.push('/login')
} }
// Выход со всех сессий (confirm action)
const logoutAll = async () => {
clearAuthCache()
await authApi.logoutAll()
store.reset()
router.push('/login')
}
const refreshIcons = () => { const refreshIcons = () => {
if (window.lucide) window.lucide.createIcons() if (window.lucide) window.lucide.createIcons()
} }

View File

@@ -92,12 +92,15 @@ export const DIALOGS = {
variant: 'danger' variant: 'danger'
}, },
// Выход из системы // Выход из системы (3 кнопки)
logout: { logout: {
title: 'Выйти из аккаунта?', title: 'Выйти из аккаунта?',
message: 'Вы будете перенаправлены<br>на страницу входа.', message: 'Выберите, откуда хотите выйти',
confirmText: 'Выйти', confirmText: 'Все сессии',
variant: 'warning' discardText: 'Текущая сессия',
showDiscard: true,
variant: 'danger',
discardVariant: 'warning'
}, },
// Удаление отдела // Удаление отдела

View File

@@ -16,10 +16,6 @@
<div class="float-icon icon-12"><i data-lucide="message-circle"></i></div> <div class="float-icon icon-12"><i data-lucide="message-circle"></i></div>
</div> </div>
<!-- Градиентные сферы на фоне -->
<div class="bg-glow glow-2"></div>
<div class="bg-glow glow-3"></div>
<!-- Контент авторизации --> <!-- Контент авторизации -->
<div class="login-content" :class="{ 'is-loading': isProcessing, 'is-success': showSuccess }"> <div class="login-content" :class="{ 'is-loading': isProcessing, 'is-success': showSuccess }">
@@ -489,44 +485,6 @@ watch([showSuccess, isRegisterMode], () => {
padding: 20px; padding: 20px;
} }
/* Градиентные сферы на фоне */
.bg-glow {
position: absolute;
border-radius: 50%;
filter: blur(120px);
opacity: 0.4;
pointer-events: none;
}
.glow-2 {
width: 500px;
height: 500px;
background: radial-gradient(circle, rgba(96, 165, 250, 0.25) 0%, transparent 70%);
bottom: -150px;
left: -100px;
animation: glowFloat2 10s ease-in-out infinite;
}
.glow-3 {
width: 400px;
height: 400px;
background: radial-gradient(circle, rgba(244, 114, 182, 0.2) 0%, transparent 70%);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
animation: glowFloat3 12s ease-in-out infinite;
}
@keyframes glowFloat2 {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(40px, -20px) scale(1.05); }
}
@keyframes glowFloat3 {
0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 0.3; }
50% { transform: translate(-50%, -50%) scale(1.2); opacity: 0.5; }
}
/* Левитирующие иконки */ /* Левитирующие иконки */
.floating-icons { .floating-icons {
position: absolute; position: absolute;