-
-
Notifications
You must be signed in to change notification settings - Fork 33
feature(account): user menu backup login #900
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
escapedcat
wants to merge
32
commits into
main
Choose a base branch
from
feature/user-menu-backup-login
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
936a519
feat(session): store password for account backup
escapedcat 16821d0
feat(nav): add UserMenu dropdown replacing bookmark icon
escapedcat c069b07
fix: add password backfill test and aria-haspopup on UserMenu
escapedcat 201da3d
feat(nav): show UserMenu always with login/logout states
escapedcat c15c603
refactor(nav): remove logout, users always have an account
escapedcat 75f4ae3
feat(auth): add login page and server route
escapedcat 8045aeb
feat(nav): add "Switch account" to logged-in user menu
escapedcat 6a6f945
feat(nav): show username as tooltip on account icon
escapedcat b1df7bc
feat(save): show toast on first account creation
escapedcat bb11dc6
fix(nav): match user icon button size to theme toggle
escapedcat e62dfd8
fix(session): hydrate session immediately instead of in onMount
escapedcat f78a7df
Revert "fix(session): hydrate session immediately instead of in onMount"
escapedcat e4a5328
fix: confirm before switch account, remove dead logout key
escapedcat 66986a6
feat(auth): add BackupModal and remove switch account confirm
escapedcat d6497b7
feat(auth): only show backup for auto-generated accounts
escapedcat 3c4d8dd
fix(auth): don't store password in localStorage for known accounts
escapedcat 0860faf
fix(saved): show empty state instead of redirecting to /map
escapedcat 64364bf
fix(saved): redirect to /login instead of showing empty state
escapedcat 766371c
fix(auth): add input length validation on login server route
escapedcat b81fc87
feat(login): add link to developer portal for account creation
escapedcat de4aa68
fix(backup): add Escape key handling, remove deprecated copy fallback
escapedcat 9beafbc
fix(nav): replace "Switch account" with "Log out"
escapedcat a9abb2c
fix(nav): use unique IDs for UserMenu trigger buttons
escapedcat e69ea5a
fix(backup): handle clipboard errors and use i18n for copy toast
escapedcat 1d6d4b2
docs(session): clarify password storage differs by account type
escapedcat a20c35e
fix(auth): don't trim passwords, sanitize error logs
escapedcat fcd4e93
fix(nav): add type=button to UserMenu action buttons
escapedcat 074f7e0
fix(backup): replace a11y ignores with role=presentation, add type=bu…
escapedcat 1d357ca
fix(nav): use deterministic IDs for UserMenu instances
escapedcat 5b57045
refactor(routes): move /saved to /user/saved
escapedcat 1334b2d
style(icon): use bookmark with checkmark for saved state
escapedcat c203730
feat(account): flow auth gate (#912)
escapedcat File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| <script lang="ts"> | ||
| import Icon from "$components/Icon.svelte"; | ||
| import { _ } from "$lib/i18n"; | ||
| import { errToast, successToast } from "$lib/utils"; | ||
|
|
||
| export let username: string; | ||
| export let password: string | null; | ||
| // idPrefix avoids element-id collisions if two instances ever mount at once. | ||
| export let idPrefix = "backup"; | ||
|
|
||
| let showPassword = false; | ||
|
|
||
| async function copyToClipboard(text: string) { | ||
| try { | ||
| await navigator.clipboard.writeText(text); | ||
| successToast($_("backup.copied")); | ||
| } catch (err) { | ||
| errToast($_("backup.copyFailed")); | ||
| console.error("Clipboard write failed", err); | ||
| } | ||
| } | ||
| </script> | ||
|
|
||
| <div class="space-y-3"> | ||
| <div> | ||
| <label | ||
| for="{idPrefix}-username" | ||
| class="mb-1 block text-xs font-semibold text-body dark:text-white/70" | ||
| > | ||
| {$_("backup.username")} | ||
| </label> | ||
| <div class="flex items-center gap-2"> | ||
| <input | ||
| id="{idPrefix}-username" | ||
| type="text" | ||
| readonly | ||
| value={username} | ||
| class="w-full rounded-lg border border-gray-300 bg-gray-50 px-3 py-2 text-sm text-primary dark:border-white/20 dark:bg-white/5 dark:text-white" | ||
| /> | ||
| <button | ||
| type="button" | ||
| on:click={() => copyToClipboard(username)} | ||
| class="shrink-0 rounded-lg border border-gray-300 p-2 transition-colors hover:bg-gray-100 dark:border-white/20 dark:hover:bg-white/10" | ||
| title={$_("backup.copy")} | ||
| > | ||
| <Icon | ||
| type="material" | ||
| icon="content_copy" | ||
| w="16" | ||
| h="16" | ||
| class="text-primary dark:text-white" | ||
| /> | ||
| </button> | ||
| </div> | ||
| </div> | ||
| <div> | ||
| <label | ||
| for="{idPrefix}-password" | ||
| class="mb-1 block text-xs font-semibold text-body dark:text-white/70" | ||
| > | ||
| {$_("backup.password")} | ||
| </label> | ||
| <div class="flex items-center gap-2"> | ||
| <input | ||
| id="{idPrefix}-password" | ||
| type={showPassword ? "text" : "password"} | ||
| readonly | ||
| value={password || $_("backup.unavailable")} | ||
| class="w-full rounded-lg border border-gray-300 bg-gray-50 px-3 py-2 text-sm text-primary dark:border-white/20 dark:bg-white/5 dark:text-white" | ||
| /> | ||
| <button | ||
| type="button" | ||
| on:click={() => (showPassword = !showPassword)} | ||
| class="shrink-0 rounded-lg border border-gray-300 p-2 transition-colors hover:bg-gray-100 dark:border-white/20 dark:hover:bg-white/10" | ||
| title={showPassword ? $_("backup.hide") : $_("backup.show")} | ||
| > | ||
| <Icon | ||
| type="material" | ||
| icon={showPassword ? "visibility_off" : "visibility"} | ||
| w="16" | ||
| h="16" | ||
| class="text-primary dark:text-white" | ||
| /> | ||
| </button> | ||
| {#if password} | ||
| <button | ||
| type="button" | ||
| on:click={() => copyToClipboard(password ?? "")} | ||
| class="shrink-0 rounded-lg border border-gray-300 p-2 transition-colors hover:bg-gray-100 dark:border-white/20 dark:hover:bg-white/10" | ||
| title={$_("backup.copy")} | ||
| > | ||
| <Icon | ||
| type="material" | ||
| icon="content_copy" | ||
| w="16" | ||
| h="16" | ||
| class="text-primary dark:text-white" | ||
| /> | ||
| </button> | ||
| {/if} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <p class="mt-4 text-xs text-body dark:text-white/50"> | ||
| {$_("backup.warning")} | ||
| </p> | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| <script lang="ts"> | ||
| import BackupCredentials from "$components/auth/BackupCredentials.svelte"; | ||
| import Modal from "$components/Modal.svelte"; | ||
| import { _ } from "$lib/i18n"; | ||
| import { session } from "$lib/session"; | ||
|
|
||
| export let open = false; | ||
| </script> | ||
|
|
||
| <Modal bind:open title={$_("backup.title")} titleId="backup-modal-title"> | ||
| <p class="mb-4 text-sm text-body dark:text-white/70"> | ||
| {$_("backup.description")} | ||
| </p> | ||
|
|
||
| {#if $session} | ||
| <BackupCredentials | ||
| username={$session.username} | ||
| password={$session.password} | ||
| /> | ||
| {/if} | ||
| </Modal> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| <script lang="ts"> | ||
| import { get } from "svelte/store"; | ||
|
|
||
| import api from "$lib/axios"; | ||
| import { _ } from "$lib/i18n"; | ||
| import type { Session } from "$lib/session"; | ||
| import { session } from "$lib/session"; | ||
| import { errToast } from "$lib/utils"; | ||
|
|
||
| // Caller receives the new session after a successful login. This keeps the | ||
| // form reusable: /login navigates, the save-flow modal completes a pending | ||
| // save, both without baking navigation/save logic into the form. | ||
| export let onSuccess: (session: Session) => void | Promise<void>; | ||
|
|
||
| // When true, render without the "Don't have an account?" link (e.g. inside | ||
| // a modal that already frames the login choice). | ||
| export let compact = false; | ||
|
|
||
| let username = ""; | ||
| let password = ""; | ||
| let loading = false; | ||
|
|
||
| async function handleSubmit() { | ||
| if (!username.trim() || !password) return; | ||
| loading = true; | ||
|
|
||
| try { | ||
| const res = await api.post("/api/session/login", { | ||
| username: username.trim(), | ||
| password, | ||
| }); | ||
|
|
||
| const token = res.data?.token; | ||
| if (typeof token !== "string") { | ||
| throw new Error("Login did not return a token"); | ||
| } | ||
|
|
||
| // Don't store the password — the user already knows their own credentials. | ||
| session.login(username.trim(), "", token); | ||
|
|
||
| // Pull the new session value so callers get a concrete Session object | ||
| // instead of having to subscribe. | ||
| const current = get(session); | ||
| if (!current) throw new Error("session.login did not populate the store"); | ||
|
|
||
| await onSuccess(current); | ||
| } catch (err) { | ||
| const status = (err as { response?: { status?: number } })?.response | ||
| ?.status; | ||
| errToast(status === 401 ? $_("login.failed") : $_("login.error")); | ||
| console.error("Login failed:", status ?? "unknown"); | ||
| } finally { | ||
| loading = false; | ||
| } | ||
| } | ||
| </script> | ||
|
|
||
| <form on:submit|preventDefault={handleSubmit} class="space-y-4"> | ||
| <div> | ||
| <label | ||
| for="login-username" | ||
| class="mb-1 block text-sm font-semibold text-primary dark:text-white" | ||
| > | ||
| {$_("login.username")} | ||
| </label> | ||
| <input | ||
| id="login-username" | ||
| type="text" | ||
| bind:value={username} | ||
| autocomplete="username" | ||
| maxlength="100" | ||
| class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-primary dark:border-white/20 dark:bg-dark dark:text-white" | ||
| /> | ||
| </div> | ||
|
|
||
| <div> | ||
| <label | ||
| for="login-password" | ||
| class="mb-1 block text-sm font-semibold text-primary dark:text-white" | ||
| > | ||
| {$_("login.password")} | ||
| </label> | ||
| <input | ||
| id="login-password" | ||
| type="password" | ||
| bind:value={password} | ||
| autocomplete="current-password" | ||
| maxlength="200" | ||
| class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-primary dark:border-white/20 dark:bg-dark dark:text-white" | ||
| /> | ||
| </div> | ||
|
|
||
| <button | ||
| type="submit" | ||
| disabled={loading || !username.trim() || !password} | ||
| class="w-full rounded-lg bg-link px-4 py-2 font-semibold text-white transition-colors hover:bg-hover disabled:opacity-50" | ||
| > | ||
| {loading ? $_("login.loggingIn") : $_("login.submit")} | ||
| </button> | ||
| </form> | ||
|
|
||
| {#if !compact} | ||
| <p class="mt-4 text-center text-sm text-body dark:text-white/70"> | ||
| {$_("login.noAccount")} | ||
| <a | ||
| href="https://developer.btcmap.org" | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| class="text-link transition-colors hover:text-hover" | ||
| > | ||
| {$_("login.createAccount")} | ||
| </a> | ||
| </p> | ||
| {/if} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add accessible names to icon-only action buttons.
On Line 40, Line 71, and Line 86 button controls are icon-only and currently rely on
title. Screen readers may not consistently announcetitleas the button name. Addaria-label(andaria-pressedfor the visibility toggle) to make these controls reliably discoverable.Suggested patch
<button type="button" on:click={() => copyToClipboard(username)} class="shrink-0 rounded-lg border border-gray-300 p-2 transition-colors hover:bg-gray-100 dark:border-white/20 dark:hover:bg-white/10" title={$_("backup.copy")} + aria-label={$_("backup.copy")} > @@ <button type="button" on:click={() => (showPassword = !showPassword)} class="shrink-0 rounded-lg border border-gray-300 p-2 transition-colors hover:bg-gray-100 dark:border-white/20 dark:hover:bg-white/10" title={showPassword ? $_("backup.hide") : $_("backup.show")} + aria-label={showPassword ? $_("backup.hide") : $_("backup.show")} + aria-pressed={showPassword} > @@ <button type="button" on:click={() => copyToClipboard(password ?? "")} class="shrink-0 rounded-lg border border-gray-300 p-2 transition-colors hover:bg-gray-100 dark:border-white/20 dark:hover:bg-white/10" title={$_("backup.copy")} + aria-label={$_("backup.copy")} >Also applies to: 71-76, 86-91
🤖 Prompt for AI Agents