Files
nexus/frontend/src/views/SecurityView.vue
T
devops 86ceb2bcce
CI - Build & Test / Backend (.NET) (push) Successful in 31s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 16s
CI - Build & Test / Security Check (push) Successful in 3s
CI - Build & Test / Deploy Nexus (push) Has been skipped
style: unify all views on Mission Control V2 design tokens
- Map legacy v1 CSS variables (--nx-*, --panel, --text-*, --surface*) to V2 tokens in nexus-tokens.css
- Restyle global shell (main.css), AppSidebar, AppHeader to match V2 Sidebar/Topbar (glass, gradients, Space Grotesk)
- Add GalaxyBackground to the v1 shell in App.vue
- Replace hardcoded v1 hex colors with V2 tokens in all deviating views (Agents, Calendar, Docs, Incidents, Memory, Notifications, ProjectDetail, Security, Team, TaskDetail, Settings)
- Keep JS status colors as hex where alpha suffixes are concatenated (AgentsIndex, Team, Notifications)
- Add Settings nav item (System group) + gear icon to V2 sidebar so it shows on the dashboard
- No component or layout structure changes; LoginView and TaskBoardView untouched

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 08:05:52 +02:00

333 lines
9.2 KiB
Vue

<script setup lang="ts">
import { onMounted, ref } from 'vue'
import {
Shield, Loader2, KeyRound, Timer, Gauge, Lock,
Cookie, Fingerprint, ShieldCheck, CheckCircle2, XCircle,
} from '@lucide/vue'
import { apiFetch } from '../services/api'
import type { SecurityStatus } from '../types'
const status = ref<SecurityStatus | null>(null)
const loading = ref(false)
const error = ref('')
async function loadStatus() {
loading.value = true
error.value = ''
try {
const response = await apiFetch('/api/v1/security/status')
if (!response.ok) throw new Error('Failed to load security status')
status.value = await response.json()
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to load security status'
} finally {
loading.value = false
}
}
onMounted(loadStatus)
</script>
<template>
<div class="page-heading">
<div>
<span class="eyebrow">SECURITY</span>
<h1>Security Center</h1>
<p>Authentication configuration, token policy, and access controls.</p>
</div>
</div>
<div v-if="loading" class="memory-status">
<Loader2 :size="20" class="spin" />
Loading security status...
</div>
<div v-else-if="error" class="memory-status error">{{ error }}</div>
<template v-else-if="status">
<div class="security-grid">
<!-- Auth Method -->
<article class="security-card">
<div class="security-card-icon">
<KeyRound :size="20" />
</div>
<h3>Authentication</h3>
<div class="security-value">{{ status.authMethod }}</div>
<p class="security-desc">
JWT-based authentication with PBKDF2 password hashing and refresh token rotation.
</p>
</article>
<!-- Token Configuration -->
<article class="security-card">
<div class="security-card-icon">
<Timer :size="20" />
</div>
<h3>Token Configuration</h3>
<div class="security-detail-list">
<div class="security-detail-row">
<span class="security-detail-label">Issuer</span>
<code class="security-detail-value">{{ status.tokenConfig.issuer }}</code>
</div>
<div class="security-detail-row">
<span class="security-detail-label">Audience</span>
<code class="security-detail-value">{{ status.tokenConfig.audience }}</code>
</div>
<div class="security-detail-row">
<span class="security-detail-label">Access Token</span>
<span class="security-detail-value">{{ status.tokenConfig.accessTokenMinutes }} min</span>
</div>
<div class="security-detail-row">
<span class="security-detail-label">Refresh Token</span>
<span class="security-detail-value">{{ status.tokenConfig.refreshTokenDays }} days</span>
</div>
</div>
</article>
<!-- Rate Limiting -->
<article class="security-card">
<div class="security-card-icon">
<Gauge :size="20" />
</div>
<h3>Rate Limiting</h3>
<div class="security-value">{{ status.rateLimit }}</div>
<p class="security-desc">
Requests are throttled per IP and endpoint to prevent abuse and brute force attacks.
</p>
</article>
<!-- Password Policy -->
<article class="security-card">
<div class="security-card-icon">
<Lock :size="20" />
</div>
<h3>Password Policy</h3>
<div class="security-value policy-text">{{ status.passwordPolicy }}</div>
<p class="security-desc">
Enforced at registration and password change. Minimum length and complexity requirements.
</p>
</article>
<!-- Cookie Configuration -->
<article class="security-card">
<div class="security-card-icon">
<Cookie :size="20" />
</div>
<h3>Cookie Configuration</h3>
<div class="security-detail-list">
<div class="security-detail-row">
<span class="security-detail-label">HttpOnly</span>
<span :class="['status-bool', status.cookieConfig.httpOnly ? 'enabled' : 'disabled']">
{{ status.cookieConfig.httpOnly ? 'Yes' : 'No' }}
</span>
</div>
<div class="security-detail-row">
<span class="security-detail-label">Secure</span>
<span :class="['status-bool', status.cookieConfig.secure ? 'enabled' : 'disabled']">
{{ status.cookieConfig.secure ? 'Yes' : 'No' }}
</span>
</div>
<div class="security-detail-row">
<span class="security-detail-label">SameSite</span>
<span class="security-detail-value">{{ status.cookieConfig.sameSite }}</span>
</div>
</div>
<p class="security-desc">
Refresh tokens stored in secure HTTP-only cookies, accessible only server-side.
</p>
</article>
<!-- 2FA Status -->
<article class="security-card">
<div class="security-card-icon twofa-icon">
<Fingerprint :size="20" />
</div>
<h3>Two-Factor Authentication</h3>
<div class="security-status-row">
<template v-if="status.twoFactorEnabled">
<CheckCircle2 :size="16" class="check-icon" />
<span class="security-value enabled-text">Enabled</span>
</template>
<template v-else>
<XCircle :size="16" class="x-icon" />
<span class="security-value disabled-text">Disabled</span>
</template>
</div>
<p class="security-desc">
{{ status.twoFactorEnabled
? '2FA adds an extra layer of security to owner accounts.'
: 'Two-factor authentication is not currently configured.' }}
</p>
</article>
<!-- Passkey Status -->
<article class="security-card">
<div class="security-card-icon passkey-icon">
<ShieldCheck :size="20" />
</div>
<h3>Passkey Authentication</h3>
<div class="security-status-row">
<template v-if="status.passkeyEnabled">
<CheckCircle2 :size="16" class="check-icon" />
<span class="security-value enabled-text">Enabled</span>
</template>
<template v-else>
<XCircle :size="16" class="x-icon" />
<span class="security-value disabled-text">Disabled</span>
</template>
</div>
<p class="security-desc">
{{ status.passkeyEnabled
? 'WebAuthn-based passkey authentication is available.'
: 'Passkey authentication is not currently configured.' }}
</p>
</article>
</div>
</template>
</template>
<style scoped>
.security-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
}
.security-card {
border: 1px solid var(--line);
border-radius: var(--r);
background: var(--panel);
padding: 20px;
display: flex;
flex-direction: column;
gap: 10px;
}
.security-card-icon {
width: 40px;
height: 40px;
display: grid;
place-items: center;
border-radius: var(--r);
color: var(--a-mid);
background: var(--accent-soft);
}
.security-card-icon.twofa-icon {
color: var(--st-queue);
background: rgba(229,176,94,.1);
}
.security-card-icon.passkey-icon {
color: var(--a-blue);
background: rgba(109,159,230,.1);
}
.security-card h3 {
margin: 0;
font-size: 13px;
color: var(--tx);
}
.security-value {
font-size: 13px;
font-weight: 600;
color: var(--tx);
}
.policy-text {
font-size: 11px;
font-family: monospace;
word-break: break-word;
}
.security-desc {
margin: 0;
font-size: 10px;
color: var(--tx-3);
line-height: 1.5;
}
.security-detail-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.security-detail-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.security-detail-label {
font-size: 10px;
color: var(--tx-2);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.security-detail-value {
font-size: 10px;
color: var(--tx);
font-weight: 500;
text-align: right;
word-break: break-word;
}
code.security-detail-value {
font-family: monospace;
background: rgba(139,124,246,.06);
padding: 1px 5px;
border-radius: 4px;
font-size: 9px;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-bool {
font-size: 10px;
font-weight: 600;
padding: 2px 8px;
border-radius: 6px;
}
.status-bool.enabled {
background: rgba(81,212,154,.1);
color: var(--st-work);
}
.status-bool.disabled {
background: rgba(225,110,117,.08);
color: var(--st-block);
}
.security-status-row {
display: flex;
align-items: center;
gap: 6px;
}
.check-icon {
color: var(--st-work);
}
.x-icon {
color: var(--st-block);
}
.enabled-text {
color: var(--st-work);
}
.disabled-text {
color: var(--st-block);
}
.memory-status {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 48px;
color: var(--tx-3);
font-size: 12px;
}
.memory-status.error {
color: var(--st-block);
}
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@media (max-width: 700px) {
.security-grid {
grid-template-columns: 1fr;
}
}
</style>