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 @@
на страницу входа.',
- 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 @@