feat: master password unlock UI with first-run vault creation
Add the unlock screen that gates entry to the main app. Includes app store (unlocked state, firstRun flag), a centered dark-themed unlock card with WRAITH branding, password validation for first-run vault creation, and conditional rendering in App.vue. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8b891dca00
commit
d67e183d72
@ -1,8 +1,30 @@
|
||||
<template>
|
||||
<div class="h-screen flex items-center justify-center">
|
||||
<div class="h-screen w-screen bg-[var(--wraith-bg-primary)]">
|
||||
<!-- Loading state -->
|
||||
<div v-if="appStore.isLoading" class="h-full flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<h1 class="text-3xl font-bold text-[#58a6ff]">WRAITH</h1>
|
||||
<p class="text-[#8b949e] mt-2">Desktop shell loading...</p>
|
||||
<h1 class="text-3xl font-bold text-[var(--wraith-accent-blue)]">WRAITH</h1>
|
||||
<p class="text-[var(--wraith-text-secondary)] mt-2">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Unlock screen -->
|
||||
<UnlockLayout v-else-if="!appStore.isUnlocked" />
|
||||
|
||||
<!-- Main application -->
|
||||
<MainLayout v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from "vue";
|
||||
import { useAppStore } from "@/stores/app.store";
|
||||
import UnlockLayout from "@/layouts/UnlockLayout.vue";
|
||||
import MainLayout from "@/layouts/MainLayout.vue";
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
onMounted(() => {
|
||||
appStore.checkFirstRun();
|
||||
});
|
||||
</script>
|
||||
|
||||
7
frontend/src/env.d.ts
vendored
Normal file
7
frontend/src/env.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "*.vue" {
|
||||
import type { DefineComponent } from "vue";
|
||||
const component: DefineComponent<object, object, unknown>;
|
||||
export default component;
|
||||
}
|
||||
122
frontend/src/layouts/UnlockLayout.vue
Normal file
122
frontend/src/layouts/UnlockLayout.vue
Normal file
@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="h-screen w-screen flex items-center justify-center bg-[var(--wraith-bg-primary)]">
|
||||
<div class="w-full max-w-sm px-6">
|
||||
<!-- Branding -->
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-4xl font-bold tracking-widest text-[var(--wraith-accent-blue)]">
|
||||
WRAITH
|
||||
</h1>
|
||||
<p class="text-[var(--wraith-text-secondary)] mt-2 text-sm">
|
||||
{{ appStore.isFirstRun ? "Create a master password" : "Enter your master password" }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Card -->
|
||||
<form
|
||||
class="bg-[var(--wraith-bg-secondary)] border border-[var(--wraith-border)] rounded-lg p-6 space-y-4"
|
||||
@submit.prevent="handleSubmit"
|
||||
>
|
||||
<!-- Error -->
|
||||
<div
|
||||
v-if="appStore.error"
|
||||
class="text-sm text-[var(--wraith-accent-red)] bg-[var(--wraith-accent-red)]/10 border border-[var(--wraith-accent-red)]/20 rounded px-3 py-2"
|
||||
>
|
||||
{{ appStore.error }}
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div>
|
||||
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1.5">
|
||||
Master Password
|
||||
</label>
|
||||
<input
|
||||
ref="passwordInput"
|
||||
v-model="password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
placeholder="Enter password..."
|
||||
class="w-full px-3 py-2 text-sm rounded bg-[var(--wraith-bg-tertiary)] border border-[var(--wraith-border)] text-[var(--wraith-text-primary)] placeholder-[var(--wraith-text-muted)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors"
|
||||
@input="appStore.clearError()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Confirm password (first run only) -->
|
||||
<div v-if="appStore.isFirstRun">
|
||||
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1.5">
|
||||
Confirm Password
|
||||
</label>
|
||||
<input
|
||||
v-model="confirmPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
placeholder="Confirm password..."
|
||||
class="w-full px-3 py-2 text-sm rounded bg-[var(--wraith-bg-tertiary)] border border-[var(--wraith-border)] text-[var(--wraith-text-primary)] placeholder-[var(--wraith-text-muted)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors"
|
||||
@input="appStore.clearError()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Submit -->
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="submitting"
|
||||
class="w-full py-2 text-sm font-medium rounded bg-[var(--wraith-accent-blue)] text-white hover:opacity-90 disabled:opacity-50 transition-opacity cursor-pointer disabled:cursor-not-allowed"
|
||||
>
|
||||
<span v-if="submitting">{{ appStore.isFirstRun ? "Creating..." : "Unlocking..." }}</span>
|
||||
<span v-else>{{ appStore.isFirstRun ? "Create Vault" : "Unlock" }}</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Version -->
|
||||
<p class="text-center text-xs text-[var(--wraith-text-muted)] mt-4">
|
||||
v1.0.0-alpha
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useAppStore } from "@/stores/app.store";
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const password = ref("");
|
||||
const confirmPassword = ref("");
|
||||
const submitting = ref(false);
|
||||
const passwordInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
passwordInput.value?.focus();
|
||||
});
|
||||
|
||||
async function handleSubmit(): Promise<void> {
|
||||
if (!password.value) {
|
||||
appStore.error = "Password is required";
|
||||
return;
|
||||
}
|
||||
|
||||
if (appStore.isFirstRun) {
|
||||
if (password.value.length < 8) {
|
||||
appStore.error = "Password must be at least 8 characters";
|
||||
return;
|
||||
}
|
||||
if (password.value !== confirmPassword.value) {
|
||||
appStore.error = "Passwords do not match";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
if (appStore.isFirstRun) {
|
||||
await appStore.createVault(password.value);
|
||||
} else {
|
||||
await appStore.unlock(password.value);
|
||||
}
|
||||
} catch {
|
||||
// Error is set in the store
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
77
frontend/src/stores/app.store.ts
Normal file
77
frontend/src/stores/app.store.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
|
||||
/**
|
||||
* Wraith application store.
|
||||
* Manages unlock state, first-run detection, and vault operations.
|
||||
*
|
||||
* Once Wails v3 bindings are generated, the mock calls below will be
|
||||
* replaced with actual WraithApp.IsFirstRun(), CreateVault(), Unlock(), etc.
|
||||
*/
|
||||
export const useAppStore = defineStore("app", () => {
|
||||
const isUnlocked = ref(false);
|
||||
const isFirstRun = ref(true);
|
||||
const isLoading = ref(true);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
/** Check whether the vault has been created before. */
|
||||
async function checkFirstRun(): Promise<void> {
|
||||
try {
|
||||
// TODO: replace with Wails binding — WraithApp.IsFirstRun()
|
||||
isFirstRun.value = true;
|
||||
} catch {
|
||||
isFirstRun.value = true;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Create a new vault with the given master password. */
|
||||
async function createVault(password: string): Promise<void> {
|
||||
error.value = null;
|
||||
try {
|
||||
// TODO: replace with Wails binding — WraithApp.CreateVault(password)
|
||||
void password;
|
||||
isFirstRun.value = false;
|
||||
isUnlocked.value = true;
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : "Failed to create vault";
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/** Unlock an existing vault with the master password. */
|
||||
async function unlock(password: string): Promise<void> {
|
||||
error.value = null;
|
||||
try {
|
||||
// TODO: replace with Wails binding — WraithApp.Unlock(password)
|
||||
void password;
|
||||
isUnlocked.value = true;
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : "Invalid master password";
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/** Lock the vault (return to unlock screen). */
|
||||
function lock(): void {
|
||||
isUnlocked.value = false;
|
||||
}
|
||||
|
||||
/** Clear the current error message. */
|
||||
function clearError(): void {
|
||||
error.value = null;
|
||||
}
|
||||
|
||||
return {
|
||||
isUnlocked,
|
||||
isFirstRun,
|
||||
isLoading,
|
||||
error,
|
||||
checkFirstRun,
|
||||
createVault,
|
||||
unlock,
|
||||
lock,
|
||||
clearError,
|
||||
};
|
||||
});
|
||||
@ -1,7 +1,13 @@
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { resolve } from "path";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user