Files
inkwell/frontend/src/views/LoginView.vue
T
bvandeusenandClaude Opus 5.5 c5f93cf9f1
Android / Build, or is the channel already serving this? (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 12s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Python tests (push) Successful in 12s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / integration (push) Successful in 43s
CI & Build / Build & push image (push) Successful in 1m0s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m28s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 6m22s
Desktop (Tauri) / Update manifest (push) Successful in 10s
rename: the sign-in screen shows Inkwell's mark, and every tab says Inkwell
The login and register screens still drew a hard-coded "TS" tile. They now
use /icon.svg, the same mark the shell's header shows.

Browser tabs took index.html's static <title> and never changed it, so every
tab read the same, and some browsers showed the URL instead. usePageTitle,
mounted once in App.vue, sets "<page> · <site name>". Routes outside the shell
name themselves with meta.title. Board lenses use the lens name the header
already shows, now in useLensName so the tab and the header read from one
place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 17:52:50 -04:00

108 lines
3.6 KiB
Vue

<script setup lang="ts">
import { ref } from "vue";
import { useRouter, useRoute } from "vue-router";
import { useSessionStore } from "../stores/session";
import { useConfigStore } from "../stores/config";
import BaseInput from "../components/BaseInput.vue";
import BaseButton from "../components/BaseButton.vue";
import type { ApiError } from "../api/client";
const session = useSessionStore();
const config = useConfigStore();
const router = useRouter();
const route = useRoute();
const email = ref("");
const password = ref("");
const error = ref("");
const loading = ref(false);
// Only follow an in-app absolute path from ?redirect= — reject protocol-relative
// ("//host") and backslash ("/\\host") forms a browser may treat as an off-site URL,
// so a crafted login link can't bounce the user elsewhere after they sign in.
function safeRedirect(raw: unknown): string {
if (typeof raw !== "string" || !raw.startsWith("/")) return "/";
if (raw.startsWith("//") || raw.startsWith("/\\")) return "/";
return raw;
}
async function submit() {
error.value = "";
loading.value = true;
try {
await session.login(email.value, password.value);
await router.replace(safeRedirect(route.query.redirect));
} catch (e) {
error.value = (e as ApiError).error ?? "Could not sign in.";
} finally {
loading.value = false;
}
}
</script>
<template>
<main class="flex min-h-full items-center justify-center px-4 py-12">
<div class="w-full max-w-sm">
<div class="mb-8 text-center">
<img
src="/icon.svg"
:alt="config.siteName"
class="mx-auto mb-3 h-12 w-12 rounded-xl"
width="48"
height="48"
/>
<h1 class="text-2xl font-bold tracking-tight">Welcome back</h1>
<p class="mt-1 text-sm text-neutral-500 dark:text-neutral-400">Sign in to your thoughts.</p>
</div>
<form class="flex flex-col gap-4" novalidate @submit.prevent="submit">
<BaseInput
id="email"
v-model="email"
label="Email"
type="email"
autocomplete="email"
placeholder="you@example.com"
required
/>
<BaseInput
id="password"
v-model="password"
label="Password"
type="password"
autocomplete="current-password"
placeholder="••••••••"
required
/>
<p
v-if="error"
role="alert"
class="rounded-lg bg-red-50 px-3 py-2 text-sm text-red-700 dark:bg-red-950/50 dark:text-red-300"
>
{{ error }}
</p>
<BaseButton type="submit" :loading="loading">Sign in</BaseButton>
</form>
<p v-if="config.allowRegistration" class="mt-6 text-center text-sm text-neutral-500 dark:text-neutral-400">
No account?
<RouterLink to="/register" class="font-semibold text-brand-700 hover:underline dark:text-brand"
>Create one</RouterLink
>
</p>
<!-- The build, on the one screen a person can reach WITHOUT an account.
"I can't sign in" is a bug report like any other and it needs a build
number; requiring a login to read one would withhold it from exactly the
people who cannot get past this page. `/api/config` is public, so this
costs nothing that was not already public (#3181). -->
<p
class="mt-8 select-all text-center text-[11px] text-neutral-400 dark:text-neutral-500"
:title="`Inkwell server build ${config.version || 'unknown'}`"
>
{{ config.version || "unknown" }}
</p>
</div>
</main>
</template>