diff --git a/backend/api/user.php b/backend/api/user.php index 822d5bc..4ebd21e 100644 --- a/backend/api/user.php +++ b/backend/api/user.php @@ -24,7 +24,7 @@ if ($method === 'POST') { RestApi::response($result); } - // Выход (удаление всех сессий) + // Выход (только текущая сессия) if ($action === 'logout') { $account = new Account(); $keycookies = $data['keycookies'] ?? $_COOKIE['session'] ?? null; @@ -32,6 +32,14 @@ if ($method === 'POST') { 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') { $account = new Account(); diff --git a/backend/app/class/enity/class_user.php b/backend/app/class/enity/class_user.php index db0652c..ef97a25 100644 --- a/backend/app/class/enity/class_user.php +++ b/backend/app/class/enity/class_user.php @@ -196,7 +196,7 @@ class Account extends BaseEntity { ]; } - // Удаление всех сессий пользователя (logout) + // Удаление только текущей сессии (logout) public function logout($keycookies) { // Проверяем, что сессия не пустая if (!$keycookies) { @@ -204,6 +204,28 @@ class Account extends BaseEntity { 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 пользователя $session = Database::get($this->db_name_session, ['id_accounts'], [ 'keycookies' => $keycookies diff --git a/front_vue/src/api.js b/front_vue/src/api.js index 4bb8fd6..b18d9d6 100644 --- a/front_vue/src/api.js +++ b/front_vue/src/api.js @@ -54,7 +54,13 @@ export const authApi = { credentials: 'include', headers: { 'Content-Type': 'application/json' }, 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 ==================== diff --git a/front_vue/src/components/ConfirmDialog.vue b/front_vue/src/components/ConfirmDialog.vue index 1fa7de6..9ab4076 100644 --- a/front_vue/src/components/ConfirmDialog.vue +++ b/front_vue/src/components/ConfirmDialog.vue @@ -11,10 +11,12 @@ @@ -56,10 +58,16 @@ const props = defineProps({ default: undefined }, variant: String, + discardVariant: String, // Async callback для подтверждения — сам управляет loading action: { type: Function, 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 dialogConfirmText = computed(() => props.confirmText ?? config.value.confirmText ?? 'Подтвердить') 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 dialogVariant = computed(() => props.variant ?? config.value.variant ?? 'default') +const dialogDiscardVariant = computed(() => props.discardVariant ?? config.value.discardVariant ?? 'default') const emit = defineEmits(['confirm', 'cancel', 'discard']) -// Внутреннее состояние загрузки -const loading = ref(false) +// Внутреннее состояние загрузки: null | 'confirm' | 'discard' +const loading = ref(null) // Сброс состояния при закрытии диалога watch(() => props.show, (newVal) => { if (!newVal) { - loading.value = false + loading.value = null } }) @@ -92,7 +101,7 @@ const handleConfirm = async () => { // Если есть async action — вызываем его и управляем loading if (props.action) { - loading.value = true + loading.value = 'confirm' try { await props.action() // Успех — эмитим confirm для закрытия @@ -101,7 +110,7 @@ const handleConfirm = async () => { console.error('ConfirmDialog action failed:', e) // При ошибке — не закрываем диалог } finally { - loading.value = false + loading.value = null } } else { // Простой режим — просто эмитим @@ -114,9 +123,26 @@ const handleCancel = () => { emit('cancel') } -const handleDiscard = () => { +const handleDiscard = async () => { if (loading.value) return - emit('discard') + + // Если есть async discardAction — вызываем его и управляем loading + if (props.discardAction) { + loading.value = 'discard' + try { + await props.discardAction() + // Успех — эмитим discard для закрытия + emit('discard') + } catch (e) { + console.error('ConfirmDialog discardAction failed:', e) + // При ошибке — не закрываем диалог + } finally { + loading.value = null + } + } else { + // Простой режим — просто эмитим + emit('discard') + } } @@ -196,6 +222,30 @@ const handleDiscard = () => { 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 { background: var(--accent); color: #000; @@ -206,21 +256,23 @@ const handleDiscard = () => { } .btn-confirm.danger { - background: #ef4444; - color: #fff; + background: rgba(239, 68, 68, 0.15); + color: #f87171; } .btn-confirm.danger:hover { - background: #dc2626; + background: rgba(239, 68, 68, 0.25); + color: #fca5a5; } .btn-confirm.warning { - background: #f59e0b; - color: #000; + background: rgba(245, 158, 11, 0.15); + color: #fbbf24; } .btn-confirm.warning:hover { - background: #d97706; + background: rgba(245, 158, 11, 0.25); + color: #fcd34d; } .btn-confirm:disabled { diff --git a/front_vue/src/components/ui/LogoutButton.vue b/front_vue/src/components/ui/LogoutButton.vue index 2cc1441..6f6ccf5 100644 --- a/front_vue/src/components/ui/LogoutButton.vue +++ b/front_vue/src/components/ui/LogoutButton.vue @@ -11,7 +11,8 @@ @@ -38,13 +39,22 @@ const router = useRouter() const store = useProjectsStore() const showDialog = ref(false) -const logout = async () => { +// Выход с текущей сессии (discard action) +const logoutCurrent = async () => { clearAuthCache() await authApi.logout() store.reset() router.push('/login') } +// Выход со всех сессий (confirm action) +const logoutAll = async () => { + clearAuthCache() + await authApi.logoutAll() + store.reset() + router.push('/login') +} + const refreshIcons = () => { if (window.lucide) window.lucide.createIcons() } diff --git a/front_vue/src/stores/dialogs.js b/front_vue/src/stores/dialogs.js index 443d72b..e151260 100644 --- a/front_vue/src/stores/dialogs.js +++ b/front_vue/src/stores/dialogs.js @@ -92,12 +92,15 @@ export const DIALOGS = { variant: 'danger' }, - // Выход из системы + // Выход из системы (3 кнопки) logout: { title: 'Выйти из аккаунта?', - message: 'Вы будете перенаправлены
на страницу входа.', - confirmText: 'Выйти', - variant: 'warning' + message: 'Выберите, откуда хотите выйти', + confirmText: 'Все сессии', + discardText: 'Текущая сессия', + showDiscard: true, + variant: 'danger', + discardVariant: 'warning' }, // Удаление отдела diff --git a/front_vue/src/views/LoginPage.vue b/front_vue/src/views/LoginPage.vue index 33074ab..cf404be 100644 --- a/front_vue/src/views/LoginPage.vue +++ b/front_vue/src/views/LoginPage.vue @@ -16,10 +16,6 @@
- -
-
-
@@ -489,44 +485,6 @@ watch([showSuccess, isRegisterMode], () => { 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 { position: absolute;