Доработка сессий
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 ====================
|
||||||
|
|||||||
@@ -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
|
||||||
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')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
},
|
},
|
||||||
|
|
||||||
// Удаление отдела
|
// Удаление отдела
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user