181 lines
9.4 KiB
Vue
181 lines
9.4 KiB
Vue
<script setup lang="ts">
|
|
import { Head } from '@inertiajs/vue3';
|
|
import { ref, onMounted, nextTick } from 'vue';
|
|
import UserLayout from '../../layouts/user/userlayout.vue';
|
|
|
|
type Sess = { id: string; ip: string|null; user_agent: string|null; last_activity: number; current: boolean };
|
|
|
|
const sessions = ref<Sess[]>([]);
|
|
const loading = ref(true);
|
|
const error = ref<string|null>(null);
|
|
|
|
async function loadSessions() {
|
|
loading.value = true;
|
|
error.value = null;
|
|
try {
|
|
const res = await fetch('/settings/security/sessions', { headers: { 'X-Requested-With':'XMLHttpRequest' } });
|
|
if (!res.ok) throw new Error('Failed to load sessions');
|
|
const j = await res.json();
|
|
sessions.value = j.data || [];
|
|
} catch (e: any) {
|
|
error.value = e?.message || 'Failed to load sessions';
|
|
} finally {
|
|
loading.value = false;
|
|
nextTick(() => { if (window.lucide) window.lucide.createIcons(); });
|
|
}
|
|
}
|
|
|
|
async function revoke(id: string) {
|
|
if (!confirm('Revoke this session?')) return;
|
|
const res = await fetch(`/settings/security/sessions/${id}`, {
|
|
method: 'DELETE',
|
|
headers: { 'X-Requested-With':'XMLHttpRequest', 'X-CSRF-TOKEN': (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement)?.content || '' },
|
|
});
|
|
if (res.ok) {
|
|
await loadSessions();
|
|
}
|
|
}
|
|
|
|
function fmtTime(ts: number) {
|
|
try { return new Date(ts * 1000).toLocaleString(); } catch { return String(ts); }
|
|
}
|
|
|
|
onMounted(loadSessions);
|
|
</script>
|
|
|
|
<template>
|
|
<UserLayout>
|
|
<Head title="Security" />
|
|
<section class="content">
|
|
<div class="wrap">
|
|
<div class="main-panel">
|
|
<header class="page-head">
|
|
<div class="head-flex">
|
|
<div class="title-group">
|
|
<div class="title">Security Center</div>
|
|
<p class="subtitle">Manage sessions, change your password, and configure two-factor authentication.</p>
|
|
</div>
|
|
<div class="security-badge">
|
|
<i data-lucide="shield"></i>
|
|
<span>Protection Active</span>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="settings-nav">
|
|
<a href="/settings/profile" class="nav-item"><i data-lucide="user"></i> Profile</a>
|
|
<a href="/settings/kyc" class="nav-item"><i data-lucide="file-check"></i> KYC</a>
|
|
<a href="/settings/security" class="nav-item active"><i data-lucide="lock"></i> Security</a>
|
|
<a href="/settings/two-factor" class="nav-item"><i data-lucide="shield"></i> 2FA</a>
|
|
</div>
|
|
|
|
<div class="settings-body">
|
|
<div class="left-col">
|
|
<div class="card">
|
|
<div class="card-head"><i data-lucide="monitor-smartphone"></i> Active sessions</div>
|
|
<div v-if="loading" class="empty">
|
|
<div class="spinner"></div>
|
|
<span>Loading sessions...</span>
|
|
</div>
|
|
<div v-else-if="error" class="err">{{ error }}</div>
|
|
<div v-else>
|
|
<div v-if="!sessions.length" class="empty">No active sessions found.</div>
|
|
<div v-for="s in sessions" :key="s.id" class="sess">
|
|
<div class="meta">
|
|
<div class="ua">{{ s.user_agent || 'Unknown device' }}</div>
|
|
<div class="ip">IP: {{ s.ip || '—' }}</div>
|
|
</div>
|
|
<div class="meta2">
|
|
<span class="muted">Last activity: {{ fmtTime(s.last_activity) }}</span>
|
|
<span v-if="s.current" class="badge cur">Current session</span>
|
|
</div>
|
|
<div class="actions">
|
|
<button class="btn danger" :disabled="s.current" @click="revoke(s.id)">Revoke</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row2">
|
|
<div class="card hover-effect">
|
|
<div class="card-head"><i data-lucide="key-round"></i> Change password</div>
|
|
<p class="muted">Use a strong and unique password. You will be asked for your current password.</p>
|
|
<a class="btn" href="/settings/password">Change password</a>
|
|
</div>
|
|
<div class="card hover-effect">
|
|
<div class="card-head"><i data-lucide="shield"></i> Two-Factor Authentication (2FA)</div>
|
|
<p class="muted">Protect your account with a second step, like an authenticator app.</p>
|
|
<a class="btn" href="/settings/two-factor">Manage 2FA</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<aside class="right-col">
|
|
<div class="side-card">
|
|
<div class="card-head">Tips</div>
|
|
<ul>
|
|
<li>Enable 2FA for better security.</li>
|
|
<li>Review sessions regularly and revoke unknown devices.</li>
|
|
<li>Never share your password or 2FA codes.</li>
|
|
</ul>
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</UserLayout>
|
|
</template>
|
|
|
|
<style scoped>
|
|
:global(:root) { --bg-card:#0a0a0a; --border:#151515; --cyan:#00f2ff; --magenta:#ff007a; --green:#00ff9d; }
|
|
.content { padding: 30px; animation: fade-in 0.8s cubic-bezier(0.2, 0, 0, 1); }
|
|
.wrap { max-width: 1100px; margin: 0 auto; }
|
|
.main-panel { background: var(--bg-card); border: 1px solid var(--border); border-radius: 20px; overflow: hidden; box-shadow: 0 20px 50px rgba(0,0,0,0.6); }
|
|
.page-head { padding: 25px 30px; border-bottom: 1px solid var(--border); background: linear-gradient(to right, rgba(0,242,255,0.03), transparent); }
|
|
.head-flex { display: flex; justify-content: space-between; align-items: center; }
|
|
.title { font-size: 14px; font-weight: 900; color: #fff; letter-spacing: 3px; text-transform: uppercase; }
|
|
.subtitle { color: #555; font-size: 12px; margin-top: 4px; font-weight: 600; }
|
|
.security-badge { display:flex; align-items:center; gap:8px; color: var(--green); background: rgba(0,255,157,0.05); padding:6px 14px; border-radius:50px; border:1px solid rgba(0,255,157,0.1); font-size:10px; font-weight:900; text-transform:uppercase; letter-spacing:1px; }
|
|
|
|
/* Navigation */
|
|
.settings-nav { display: flex; gap: 5px; padding: 15px 30px; border-bottom: 1px solid var(--border); background: rgba(0,0,0,0.2); overflow-x: auto; }
|
|
.nav-item { display: flex; align-items: center; gap: 8px; padding: 10px 16px; border-radius: 8px; font-size: 11px; font-weight: 800; color: #666; text-transform: uppercase; letter-spacing: 1px; transition: 0.2s; text-decoration: none; }
|
|
.nav-item:hover { color: #fff; background: rgba(255,255,255,0.05); }
|
|
.nav-item.active { color: var(--cyan); background: rgba(0,242,255,0.1); }
|
|
.nav-item i { width: 14px; }
|
|
|
|
.settings-body { padding: 24px 28px; display:grid; grid-template-columns: 1.6fr .8fr; gap: 24px; }
|
|
.left-col { display: flex; flex-direction: column; gap: 18px; animation: slide-up 0.6s cubic-bezier(0.2, 0, 0, 1) backwards; animation-delay: 0.1s; }
|
|
.right-col { animation: slide-up 0.6s cubic-bezier(0.2, 0, 0, 1) backwards; animation-delay: 0.2s; }
|
|
|
|
.card { border: 1px solid var(--border); background: #0a0a0a; border-radius: 14px; padding: 14px; display: grid; gap: 12px; transition: 0.3s; }
|
|
.card.hover-effect:hover { border-color: #333; transform: translateY(-2px); box-shadow: 0 10px 30px rgba(0,0,0,0.3); }
|
|
.card-head { color: #fff; font-weight: 900; display:flex; align-items:center; gap:8px; }
|
|
.row2 { display:grid; grid-template-columns: 1fr 1fr; gap: 18px; }
|
|
|
|
.empty { color: #666; display: flex; align-items: center; gap: 10px; padding: 20px; justify-content: center; }
|
|
.spinner { width: 16px; height: 16px; border: 2px solid rgba(255,255,255,0.1); border-top-color: #fff; border-radius: 50%; animation: spin 0.8s linear infinite; }
|
|
.err { color: #ff5b5b; }
|
|
|
|
.sess { border: 1px solid #151515; background: #050505; border-radius: 12px; padding: 12px; display: grid; gap: 8px; transition: 0.3s; animation: fade-in 0.5s backwards; }
|
|
.sess:hover { border-color: #333; }
|
|
.meta { display:flex; justify-content:space-between; gap:10px; color:#ddd; }
|
|
.meta2 { display:flex; gap:14px; color:#666; font-size:12px; align-items:center; }
|
|
.badge.cur { font-size:10px; font-weight:900; padding:3px 8px; border-radius:999px; border:1px solid var(--border); color:#bbb; background:#0b0b0b; }
|
|
.actions { display:flex; gap:8px; }
|
|
.btn { background: var(--cyan); color: #000; border: none; border-radius: 10px; padding: 10px 14px; font-weight: 900; cursor: pointer; text-decoration: none; text-align: center; display: inline-block; transition: 0.3s; }
|
|
.btn:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0,242,255,0.2); }
|
|
.btn.danger { background: #ff5b5b; color: #000; }
|
|
.btn.danger:hover { box-shadow: 0 5px 15px rgba(255,91,91,0.2); }
|
|
|
|
.side-card { border: 1px solid var(--border); background: #0a0a0a; border-radius: 14px; padding: 16px; position: sticky; top: 20px; }
|
|
.side-card ul { padding-left: 16px; color:#9aa0a6; }
|
|
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
@keyframes fade-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
|
@keyframes slide-up { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
|
|
|
|
@media (max-width: 900px) { .settings-body { grid-template-columns: 1fr; padding: 16px; } .row2 { grid-template-columns: 1fr; } .side-card { position: static; } .settings-nav { padding: 10px 15px; } }
|
|
</style>
|