wraith/src/layouts/UnlockLayout.vue
Vantz Stockwell 2848d79915 feat: Phase 1 complete — Tauri v2 foundation
Rust backend: SQLite (WAL mode, 8 tables), vault encryption
(Argon2id + AES-256-GCM), settings/connections/credentials
services, 19 Tauri command wrappers. 46/46 tests passing.

Vue 3 frontend: unlock/create vault flow, Pinia app store,
Tailwind CSS v4 dark theme with Wraith branding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:09:41 -04:00

253 lines
7.2 KiB
Vue

<script setup lang="ts">
import { ref, computed } from "vue";
import { useAppStore } from "@/stores/app.store";
const app = useAppStore();
const password = ref("");
const confirmPassword = ref("");
const localError = ref<string | null>(null);
const loading = ref(false);
const isFirstRun = computed(() => app.isFirstRun);
async function handleSubmit() {
localError.value = null;
if (!password.value.trim()) {
localError.value = "Password is required.";
return;
}
if (isFirstRun.value) {
if (password.value.length < 12) {
localError.value = "Master password must be at least 12 characters.";
return;
}
if (password.value !== confirmPassword.value) {
localError.value = "Passwords do not match.";
return;
}
}
loading.value = true;
try {
if (isFirstRun.value) {
await app.createVault(password.value);
} else {
await app.unlock(password.value);
}
} catch {
// app.error is already set by the store; surface it locally so the template
// only needs to check one place.
localError.value = app.error ?? "An unexpected error occurred.";
} finally {
loading.value = false;
}
}
const displayError = computed(() => localError.value ?? app.error);
</script>
<template>
<div
class="unlock-root"
style="
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--wraith-bg-primary);
"
>
<div
class="unlock-card"
style="
width: 100%;
max-width: 400px;
padding: 2.5rem;
background-color: var(--wraith-bg-secondary);
border: 1px solid var(--wraith-border);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
"
>
<!-- Logo -->
<div style="text-align: center; margin-bottom: 2rem">
<span
style="
font-size: 2rem;
font-weight: 800;
letter-spacing: 0.3em;
color: var(--wraith-accent-blue);
text-transform: uppercase;
font-family: 'Inter', monospace;
"
>
WRAITH
</span>
<p
style="
margin: 0.5rem 0 0;
font-size: 0.8rem;
color: var(--wraith-text-muted);
letter-spacing: 0.15em;
text-transform: uppercase;
"
>
{{ isFirstRun ? "Initialize Secure Vault" : "Secure Desktop" }}
</p>
</div>
<!-- Form -->
<form @submit.prevent="handleSubmit" style="display: flex; flex-direction: column; gap: 1rem">
<!-- Master password -->
<div>
<label
for="master-password"
style="
display: block;
margin-bottom: 0.4rem;
font-size: 0.8rem;
color: var(--wraith-text-secondary);
letter-spacing: 0.05em;
"
>
MASTER PASSWORD
</label>
<input
id="master-password"
v-model="password"
type="password"
autocomplete="current-password"
placeholder="Enter master password"
:disabled="loading"
style="
width: 100%;
padding: 0.65rem 0.9rem;
background-color: var(--wraith-bg-tertiary);
border: 1px solid var(--wraith-border);
border-radius: 6px;
color: var(--wraith-text-primary);
font-size: 0.95rem;
outline: none;
transition: border-color 0.15s ease;
box-sizing: border-box;
"
@focus="($event.target as HTMLInputElement).style.borderColor = 'var(--wraith-accent-blue)'"
@blur="($event.target as HTMLInputElement).style.borderColor = 'var(--wraith-border)'"
/>
</div>
<!-- Confirm password — only shown on first run -->
<div v-if="isFirstRun">
<label
for="confirm-password"
style="
display: block;
margin-bottom: 0.4rem;
font-size: 0.8rem;
color: var(--wraith-text-secondary);
letter-spacing: 0.05em;
"
>
CONFIRM PASSWORD
</label>
<input
id="confirm-password"
v-model="confirmPassword"
type="password"
autocomplete="new-password"
placeholder="Confirm master password"
:disabled="loading"
style="
width: 100%;
padding: 0.65rem 0.9rem;
background-color: var(--wraith-bg-tertiary);
border: 1px solid var(--wraith-border);
border-radius: 6px;
color: var(--wraith-text-primary);
font-size: 0.95rem;
outline: none;
transition: border-color 0.15s ease;
box-sizing: border-box;
"
@focus="($event.target as HTMLInputElement).style.borderColor = 'var(--wraith-accent-blue)'"
@blur="($event.target as HTMLInputElement).style.borderColor = 'var(--wraith-border)'"
/>
<p
style="
margin: 0.4rem 0 0;
font-size: 0.75rem;
color: var(--wraith-text-muted);
"
>
Minimum 12 characters. This password cannot be recovered.
</p>
</div>
<!-- Error message -->
<div
v-if="displayError"
style="
padding: 0.6rem 0.9rem;
background-color: rgba(248, 81, 73, 0.1);
border: 1px solid rgba(248, 81, 73, 0.3);
border-radius: 6px;
color: var(--wraith-accent-red);
font-size: 0.85rem;
"
>
{{ displayError }}
</div>
<!-- Submit button -->
<button
type="submit"
:disabled="loading"
style="
width: 100%;
padding: 0.7rem;
margin-top: 0.5rem;
background-color: var(--wraith-accent-blue);
color: #0d1117;
font-weight: 700;
font-size: 0.9rem;
letter-spacing: 0.08em;
text-transform: uppercase;
border: none;
border-radius: 6px;
cursor: pointer;
transition: opacity 0.15s ease, background-color 0.15s ease;
"
:style="{ opacity: loading ? '0.6' : '1', cursor: loading ? 'not-allowed' : 'pointer' }"
>
<span v-if="loading">
{{ isFirstRun ? "Creating vault..." : "Unlocking..." }}
</span>
<span v-else>
{{ isFirstRun ? "Create Vault" : "Unlock" }}
</span>
</button>
</form>
<!-- Footer hint -->
<p
style="
margin: 1.5rem 0 0;
text-align: center;
font-size: 0.75rem;
color: var(--wraith-text-muted);
"
>
<template v-if="isFirstRun">
Your vault will be encrypted with AES-256-GCM.
</template>
<template v-else>
All data is encrypted at rest.
</template>
</p>
</div>
</div>
</template>