- 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>
77 lines
2.9 KiB
Vue
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>
|