feat: Waves 3+4 — frontend wiring, NATS integration, stores (19 files)
All checks were successful
Test Asgard Runner / test (push) Successful in 2s

Frontend:
- Wire Dashboard quick actions (start/stop/trigger wipe) + next wipe schedule
- Wire Console WebSocket streaming for real-time output
- Implement TOTP 2FA challenge flow in LoginView
- Wire Plugin load/unload toggle + uninstall buttons with confirmations
- Wire WipesView profile selector, disable trigger when no profiles
- Build full WipeProfiles create/edit modal with all config fields
- Wire MapsView file upload with multipart FormData
- Fix SettingsView empty catch blocks → toast error messages
- Fix stale localStorage token reads in CSV exports → auth store
- Fix auth store hardcoded permissions → JWT-decoded role permissions
- Fix wipe store onMounted lifecycle bug → explicit subscribe action
- Update EarlyAccessView from countdown to "Now Live" state

Backend:
- Wire wipe trigger to publish NATS cmd (corrosion.{id}.cmd.wipe)
- Wire plugin reload/uninstall to publish NATS cmd
- Expand NatsBridgeService: add files, wipe status, server status subs
- Add PATCH schedules/:id/toggle endpoint for task toggling

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell
2026-02-21 13:34:09 -05:00
parent a181ed7ded
commit 8bb6cc0890
19 changed files with 776 additions and 139 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ref, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { useApi } from '@/composables/useApi'
import { useAuthStore } from '@/stores/auth'
@@ -14,6 +14,11 @@ const password = ref('')
const loading = ref(false)
const error = ref('')
// TOTP state
const showTotpInput = ref(false)
const totpCode = ref('')
const totpInputEl = ref<HTMLInputElement | null>(null)
async function handleLogin() {
error.value = ''
loading.value = true
@@ -24,6 +29,15 @@ async function handleLogin() {
password: password.value,
})
if (response.requires_totp) {
// Credentials verified — server is waiting for the TOTP code.
// Show the TOTP input and focus it.
showTotpInput.value = true
await nextTick()
totpInputEl.value?.focus()
return
}
authStore.setAuth(response)
router.push('/')
} catch (err: unknown) {
@@ -36,6 +50,43 @@ async function handleLogin() {
loading.value = false
}
}
async function handleTotpVerify() {
if (totpCode.value.length !== 6 || loading.value) return
error.value = ''
loading.value = true
try {
// Re-POST to /auth/login with the same credentials plus the TOTP code.
// The backend LoginDto accepts an optional totp_code field and returns full
// tokens when the code is valid.
const response = await api.post<AuthResponse>('/auth/login', {
email: email.value,
password: password.value,
totp_code: totpCode.value,
})
authStore.setAuth(response)
router.push('/')
} catch (err: unknown) {
totpCode.value = ''
if (err instanceof Error) {
error.value = err.message
} else {
error.value = 'Invalid authentication code. Please try again.'
}
// Keep the TOTP screen visible so the user can retry
} finally {
loading.value = false
}
}
function handleBackToLogin() {
showTotpInput.value = false
totpCode.value = ''
error.value = ''
}
</script>
<template>
@@ -56,7 +107,7 @@ async function handleLogin() {
</div>
<!-- Login form -->
<form @submit.prevent="handleLogin" class="space-y-5">
<form v-if="!showTotpInput" @submit.prevent="handleLogin" class="space-y-5">
<div>
<label for="email" class="block text-sm font-medium text-neutral-400 mb-1.5">
Email
@@ -117,8 +168,74 @@ async function handleLogin() {
</button>
</form>
<!-- TOTP verification form -->
<div v-else class="space-y-5">
<div class="text-center">
<p class="text-sm text-neutral-400">
Enter the 6-digit code from your authenticator app.
</p>
</div>
<div>
<label for="totp" class="block text-sm font-medium text-neutral-400 mb-1.5">
Authentication Code
</label>
<input
id="totp"
ref="totpInputEl"
v-model="totpCode"
type="text"
inputmode="numeric"
autocomplete="one-time-code"
placeholder="000000"
maxlength="6"
@keydown.enter="handleTotpVerify"
class="w-full px-3 py-2.5 bg-neutral-800 border border-neutral-700 rounded-lg text-neutral-100 placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-oxide-500/50 focus:border-oxide-500 transition-colors text-center tracking-widest text-lg font-mono"
/>
</div>
<button
type="button"
:disabled="totpCode.length !== 6 || loading"
@click="handleTotpVerify"
class="w-full py-2.5 bg-oxide-600 hover:bg-oxide-700 disabled:opacity-50 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors flex items-center justify-center gap-2"
>
<svg
v-if="loading"
class="animate-spin h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
/>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
{{ loading ? 'Verifying...' : 'Verify Code' }}
</button>
<button
type="button"
:disabled="loading"
@click="handleBackToLogin"
class="w-full py-2 text-sm text-neutral-500 hover:text-neutral-300 transition-colors"
>
Back to sign in
</button>
</div>
<!-- Register link -->
<p class="mt-6 text-center text-sm text-neutral-500">
<p v-if="!showTotpInput" class="mt-6 text-center text-sm text-neutral-500">
Don't have an account?
<router-link to="/register" class="text-oxide-400 hover:text-oxide-300 transition-colors">
Create one