From 92371806086baee841b85844b6d3223d8370994d Mon Sep 17 00:00:00 2001 From: jay Date: Wed, 3 Jun 2026 01:19:30 +0000 Subject: [PATCH] Accounts Phase 1c: sign-in UI, magic-link landing, auth store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Shared reactive auth store (auth.user) + postJSON helper (sends the cookie). - SignIn modal: email -> "check your inbox" (calm, no password); Google slots in here in Phase 2. - /auth/verify route exchanges the magic-link token for a session, then home. - Header shows "Sign in" or an account avatar; the You sheet gains "Signed in as …" + Sign out (or a Sign in row). Anonymous browsing is unchanged. --- frontend/src/lib/api.js | 20 +++ frontend/src/lib/auth.svelte.js | 35 +++++ frontend/src/lib/components/Header.svelte | 19 ++- frontend/src/lib/components/SignIn.svelte | 149 +++++++++++++++++++ frontend/src/routes/+page.svelte | 35 ++++- frontend/src/routes/auth/verify/+page.svelte | 54 +++++++ 6 files changed, 310 insertions(+), 2 deletions(-) create mode 100644 frontend/src/lib/auth.svelte.js create mode 100644 frontend/src/lib/components/SignIn.svelte create mode 100644 frontend/src/routes/auth/verify/+page.svelte diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js index 5649389..ff5334e 100644 --- a/frontend/src/lib/api.js +++ b/frontend/src/lib/api.js @@ -3,3 +3,23 @@ export async function getJSON(url) { if (!r.ok) throw new Error((await r.text().catch(() => '')) || r.statusText); return r.json(); } + +export async function postJSON(url, body) { + const r = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body ?? {}), + credentials: 'same-origin', // send/receive the session cookie + }); + if (!r.ok) { + let msg = r.statusText; + try { + const j = await r.json(); + if (j && j.detail) msg = j.detail; + } catch { + /* non-JSON error body */ + } + throw new Error(msg); + } + return r.json(); +} diff --git a/frontend/src/lib/auth.svelte.js b/frontend/src/lib/auth.svelte.js new file mode 100644 index 0000000..fdefa6a --- /dev/null +++ b/frontend/src/lib/auth.svelte.js @@ -0,0 +1,35 @@ +// Shared, reactive auth state (Svelte 5 runes in a module). Components read +// `auth.user` and it stays reactive across the app. +import { getJSON, postJSON } from './api.js'; + +export const auth = $state({ user: null, ready: false }); + +export async function refresh() { + try { + auth.user = await getJSON('/api/auth/me'); + } catch { + auth.user = null; + } finally { + auth.ready = true; + } +} + +// Request a magic link. Reply is intentionally identical for any address. +export function startEmail(email) { + return postJSON('/api/auth/email/start', { email }); +} + +// Exchange a magic-link token for a session (cookie set server-side). +export async function verifyToken(token) { + const res = await postJSON('/api/auth/email/verify', { token }); + auth.user = res.user; + return res; +} + +export async function logout() { + try { + await postJSON('/api/auth/logout', {}); + } finally { + auth.user = null; + } +} diff --git a/frontend/src/lib/components/Header.svelte b/frontend/src/lib/components/Header.svelte index b49df73..256c86b 100644 --- a/frontend/src/lib/components/Header.svelte +++ b/frontend/src/lib/components/Header.svelte @@ -1,5 +1,6 @@
@@ -20,6 +21,13 @@ stroke="currentColor" stroke-width="1.8" stroke-linecap="round" /> History + {#if user} + + {:else} + + {/if}
@@ -49,6 +57,15 @@ .utils button svg { width: 17px; height: 17px; } .utils button:hover { background: var(--accent-soft); color: var(--accent-deep); } .utils button.on { color: var(--accent-deep); } + .utils .signin { border-color: var(--line); color: var(--accent-deep); } + .utils .signin:hover { background: var(--accent-soft); } + .acct { padding: 4px; } + .avatar { + display: inline-flex; align-items: center; justify-content: center; + width: 30px; height: 30px; border-radius: 999px; + background: var(--accent); color: #fff; font-weight: 600; font-size: 0.8rem; + font-family: var(--label); + } /* On phones the utilities live in the bottom tab bar ("You") instead. */ @media (max-width: 720px) { diff --git a/frontend/src/lib/components/SignIn.svelte b/frontend/src/lib/components/SignIn.svelte new file mode 100644 index 0000000..fb8cf2f --- /dev/null +++ b/frontend/src/lib/components/SignIn.svelte @@ -0,0 +1,149 @@ + + + + + diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index b714dd2..a1c43b0 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -7,6 +7,8 @@ import MoodNav from '$lib/components/MoodNav.svelte'; import ArticleCard from '$lib/components/ArticleCard.svelte'; import BoundariesPanel from '$lib/components/BoundariesPanel.svelte'; + import SignIn from '$lib/components/SignIn.svelte'; + import { auth, refresh as refreshAuth, logout as authLogout } from '$lib/auth.svelte.js'; let moods = $state([]); let topics = $state([]); @@ -19,6 +21,17 @@ let showBoundaries = $state(false); let showHistory = $state(false); let showYou = $state(false); // mobile "You" sheet + let showSignIn = $state(false); + + function openAccount() { + showYou = false; + if (auth.user) showYou = true; // account lives in the You sheet + else showSignIn = true; + } + async function signOut() { + await authLogout(); + showYou = false; + } let loading = $state(true); let error = $state(''); @@ -227,6 +240,7 @@ seenIds = new Set(P.loadJSON(SEEN_KEY, [])); dismissed = new Set(P.loadJSON(DISMISSED_KEY, [])); history = P.loadJSON(HISTORY_KEY, []); + refreshAuth(); // resolve any existing session (non-blocking) try { moods = await getJSON('/api/moods'); topics = (await getJSON('/api/categories')).topics; @@ -242,7 +256,15 @@ }); -
(showBoundaries = !showBoundaries)} onHistory={() => (showHistory = !showHistory)} {filtersOn} /> +
(showBoundaries = !showBoundaries)} + onHistory={() => (showHistory = !showHistory)} + onaccount={openAccount} + user={auth.user} + {filtersOn} +/> + +{#if showSignIn} (showSignIn = false)} />{/if}
{#if moods.length} @@ -278,12 +300,22 @@ {#if showYou}

You

+ {#if auth.user} +
Signed in as {auth.user.email}
+ {/if} + {#if auth.user} + + {:else} + + {/if}
{/if} @@ -413,6 +445,7 @@ } .youmenu .yourow:last-child { border-bottom: none; } .youmenu .dot { background: var(--accent-soft); color: var(--accent-deep); border-radius: 999px; padding: 1px 9px; font-size: 0.78rem; } + .youmenu .acctline { color: var(--muted); font-size: 0.85rem; padding: 4px 2px 10px; border-bottom: 1px solid var(--line); } .notice { text-align: center; color: var(--accent-deep); background: var(--accent-soft); diff --git a/frontend/src/routes/auth/verify/+page.svelte b/frontend/src/routes/auth/verify/+page.svelte new file mode 100644 index 0000000..b83bb5c --- /dev/null +++ b/frontend/src/routes/auth/verify/+page.svelte @@ -0,0 +1,54 @@ + + +
+ {#if status === 'verifying'} +

Signing you in…

+ {:else} +

Couldn't sign you in

+

{error}

+ ← Back to Upbeat Bytes + {/if} +
+ +