wraith/frontend/pages/login.vue
Vantz Stockwell 13111ae007 feat: nav bar (Home link), profile management, TOTP 2FA
- Add Home/Profile links to nav bar alongside Vault/Settings/Logout
- Profile page: change email, display name, password
- TOTP 2FA: setup with QR code, verify, disable with password
- Login flow: two-step TOTP challenge when 2FA is enabled
- Backend: new endpoints PUT /profile, POST /totp/setup|verify|disable
- Migration: add totp_secret and totp_enabled columns to users

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 08:36:03 -04:00

77 lines
2.9 KiB
Vue

<script setup lang="ts">
definePageMeta({ layout: 'auth' })
const auth = useAuthStore()
const email = ref('admin@wraith.local')
const password = ref('')
const totpCode = ref('')
const error = ref('')
const loading = ref(false)
const requiresTotp = ref(false)
async function handleLogin() {
error.value = ''
loading.value = true
try {
const res = await auth.login(
email.value,
password.value,
requiresTotp.value ? totpCode.value : undefined,
)
if (res.requires_totp) {
requiresTotp.value = true
loading.value = false
return
}
navigateTo('/')
} catch (e: any) {
error.value = e.data?.message || 'Login failed'
} finally {
loading.value = false
}
}
</script>
<template>
<div class="w-full max-w-sm">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-white tracking-wide">WRAITH</h1>
<p class="text-gray-500 mt-1 text-sm">Remote Access Terminal</p>
</div>
<form @submit.prevent="handleLogin" class="space-y-4 bg-gray-900 p-6 rounded-lg border border-gray-800">
<template v-if="!requiresTotp">
<div>
<label class="block text-sm text-gray-400 mb-1">Email</label>
<input v-model="email" type="email" required autofocus
class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-white focus:border-wraith-500 focus:outline-none" />
</div>
<div>
<label class="block text-sm text-gray-400 mb-1">Password</label>
<input v-model="password" type="password" required
class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-white focus:border-wraith-500 focus:outline-none" />
</div>
</template>
<template v-else>
<div class="text-center mb-2">
<p class="text-sm text-gray-400">Enter your authenticator code</p>
</div>
<div>
<label class="block text-sm text-gray-400 mb-1">TOTP Code</label>
<input v-model="totpCode" type="text" inputmode="numeric" pattern="[0-9]*" maxlength="6"
required autofocus autocomplete="one-time-code" placeholder="000000"
class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-white text-center text-lg tracking-widest focus:border-wraith-500 focus:outline-none" />
</div>
</template>
<p v-if="error" class="text-red-400 text-sm">{{ error }}</p>
<button type="submit" :disabled="loading"
class="w-full py-2 bg-wraith-600 hover:bg-wraith-700 text-white rounded font-medium disabled:opacity-50">
{{ loading ? 'Verifying...' : requiresTotp ? 'Verify' : 'Sign In' }}
</button>
<button v-if="requiresTotp" type="button" @click="requiresTotp = false; totpCode = ''; error = ''"
class="w-full py-2 text-sm text-gray-500 hover:text-gray-300">
Back to login
</button>
</form>
</div>
</template>