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
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>
108 lines
3.6 KiB
Vue
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>
|