feat: Waves 3+4 — frontend wiring, NATS integration, stores (19 files)
All checks were successful
Test Asgard Runner / test (push) Successful in 2s
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user