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>
|
<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">
|
<div class="text-center">
|
||||||
<h1 class="text-3xl font-bold text-[#58a6ff]">WRAITH</h1>
|
<h1 class="text-3xl font-bold text-[var(--wraith-accent-blue)]">WRAITH</h1>
|
||||||
<p class="text-[#8b949e] mt-2">Desktop shell loading...</p>
|
<p class="text-[var(--wraith-text-secondary)] mt-2">Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Unlock screen -->
|
||||||
|
<UnlockLayout v-else-if="!appStore.isUnlocked" />
|
||||||
|
|
||||||
|
<!-- Main application -->
|
||||||
|
<MainLayout v-else />
|
||||||
|
</div>
|
||||||
</template>
|
</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 { defineConfig } from "vite";
|
||||||
import vue from "@vitejs/plugin-vue";
|
import vue from "@vitejs/plugin-vue";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import { resolve } from "path";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue(), tailwindcss()],
|
plugins: [vue(), tailwindcss()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": resolve(__dirname, "src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user