Initial commit: Nexus Mission Control Platform
- ASP.NET Core 10 Backend (JWT Auth, Agent config API) - Vue 3 Frontend (Dashboard, Team, Agents, Config Editor) - PostgreSQL Database - Docker Compose setup - Mission Control Dashboard redesign
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
<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: 9px;
|
||||
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: 9px;
|
||||
color: #a99cff;
|
||||
background: var(--accent-soft);
|
||||
}
|
||||
.security-card-icon.twofa-icon {
|
||||
color: #e5b05e;
|
||||
background: rgba(229,176,94,.1);
|
||||
}
|
||||
.security-card-icon.passkey-icon {
|
||||
color: #6d9fe6;
|
||||
background: rgba(109,159,230,.1);
|
||||
}
|
||||
.security-card h3 {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #e8eaf0;
|
||||
}
|
||||
.security-value {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #e8eaf0;
|
||||
}
|
||||
.policy-text {
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
word-break: break-word;
|
||||
}
|
||||
.security-desc {
|
||||
margin: 0;
|
||||
font-size: 10px;
|
||||
color: #7e8799;
|
||||
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: #8991a1;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.security-detail-value {
|
||||
font-size: 10px;
|
||||
color: #e8eaf0;
|
||||
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: #51d49a;
|
||||
}
|
||||
.status-bool.disabled {
|
||||
background: rgba(225,110,117,.08);
|
||||
color: #e16e75;
|
||||
}
|
||||
.security-status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.check-icon {
|
||||
color: #51d49a;
|
||||
}
|
||||
.x-icon {
|
||||
color: #e16e75;
|
||||
}
|
||||
.enabled-text {
|
||||
color: #51d49a;
|
||||
}
|
||||
.disabled-text {
|
||||
color: #e16e75;
|
||||
}
|
||||
|
||||
.memory-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 48px;
|
||||
color: #7e8799;
|
||||
font-size: 12px;
|
||||
}
|
||||
.memory-status.error {
|
||||
color: #e16e75;
|
||||
}
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.security-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user