mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-21 10:03:29 +00:00
0ab6d3ed10
* feat(web): show signed-in account chip on CLI auth approval page (TASK-836)
The CLI auth approval page (/auth/cli/{code}) previously showed only an
"Approve" button with no indication of WHICH account was about to grant
the CLI access. For OAuth users on Pad Cloud — most of whom have
multiple GitHub/Google accounts — wrong-account approval was a silent
footgun, recoverable only by revoking the CLI token after the fact.
This change renders an account chip above the Approve button when the
session is pending, showing:
- The user's avatar (when avatar_url is present)
- Display name (or username fallback if name is empty)
- Email
Below the chip, a "I'm not <Name> — switch accounts" link button calls
api.auth.logout() and navigates to /login?redirect=/auth/cli/{code}, so
after re-login the user lands back on this same approval page (the
login page already validates relative-only redirects to prevent open
redirects).
Graceful degradation: api.auth.me() is wrapped in its own try/catch.
If it fails, currentUser stays null and the chip simply doesn't
render — the Approve flow still works. The Approve button is also
disabled while a switch-accounts call is in flight to avoid
double-action races.
Works for both email/password (self-hosted) and OAuth (Cloud)
sessions because api.auth.me() and api.auth.logout() operate on the
unified pad session regardless of how it was established.
Parent: PLAN-833. Source: IDEA-831 issue #3.
* fix(web): plumb redirect through OAuth login + surface logout failures
Codex round-1 findings on TASK-836:
- MEDIUM: The login page already preserved ?redirect= for password and
2FA login but the GitHub/Google OAuth buttons were plain anchors with
hardcoded hrefs. A user clicking "Switch accounts" on the CLI auth
approval page and then signing in via OAuth would land at /console
instead of back at /auth/cli/{code}. Added a $derived oauthRedirectQuery
rune that reuses the existing getRedirectTarget() validation and
appends ?redirect=<encoded> to both OAuth links when the redirect is
non-default. Whether pad-cloud's /auth/github and /auth/google handlers
honor the redirect param is an out-of-tree concern and tracked
separately if needed; the client side now consistently passes it.
- LOW: handleSwitchAccount silently swallowed logout failures and then
navigated to /login. If the server didn't actually invalidate the
session cookie (network/CSRF), login's onMount would see an
authenticated session and bounce the user right back to the approval
page — making "switch accounts" appear to be a no-op. The handler now
surfaces the error in the page error slot and stays on the approval
page, giving the user a clear next step (retry or close the tab) and
also resets switchingAccount so the UI isn't stuck in a "Switching..."
state.
A defensive code check was also added to handleSwitchAccount to mirror
handleApprove's "Missing CLI session code" guard, even though the button
only renders when status === 'pending'.
Parent: PLAN-833.
* fix(web): tighten redirect validation + cover OAuth banner buttons
Codex round-2 findings on TASK-836:
- MEDIUM: getRedirectTarget() accepted protocol-relative URLs (`//host`
and `/\host`) because the bare `startsWith('/')` check passes for
both. Browsers and most server-side redirect handlers treat those as
cross-origin destinations, so a crafted `?redirect=//evil.example`
could become an open redirect once forwarded through the OAuth
handler. Now also rejects strings that start with `//` or `/\`. This
was a pre-existing bug in the password/2FA redirect path; the OAuth
link change made the surface area worth tightening.
- LOW: The "Use a different GitHub/Google account" banner buttons that
appear on `oauth_provider_not_linked` errors hardcoded `?force=1` and
dropped the redirect target. Added a sibling `oauthRedirectAmpQuery`
derived value (`&redirect=...`) so those links compose properly with
`?force=1`. When the redirect is the default `/console` it stays
empty so we don't add redundant query noise.
Both changes live in cmd/pad/... no, in web/src/routes/login/+page.svelte
and don't affect the password / 2FA paths beyond the validation
tightening (which they were already passing through silently).
Parent: PLAN-833.
Pad Web UI
SvelteKit 2 + Svelte 5 frontend for Pad, compiled to static files and embedded into the Go binary.
Development
npm install
npm run dev # Dev server at localhost:5173 (proxies API to localhost:7777)
npm run build # Production build to build/
npm run check # Type checking with svelte-check
When developing, run the Go backend separately with make dev from the project root.
Building for Production
Do not build in isolation. Always use make build from the project root — this builds the web frontend, then compiles the Go binary with the build output embedded via //go:embed.
Stack
- Svelte 5 with runes (
$state,$derived,$effect) - SvelteKit 2 with
adapter-static(SPA mode) - Tiptap block editor with markdown round-trip
- svelte-dnd-action for drag-and-drop in board/list views
- SSE for real-time updates
- TypeScript throughout
Structure
src/
routes/ SvelteKit pages
+layout.svelte App shell (sidebar + main)
+page.svelte Landing/redirect
[workspace]/
+page.svelte Dashboard (collections, phases, activity)
+layout.svelte SSE connection per workspace
[collection]/
+page.svelte Collection view (board/list)
[collection]/[item]/
+page.svelte Item detail + editor
conventions/ Purpose-built conventions page
playbooks/ Purpose-built playbooks page
settings/ Workspace settings
lib/
api/client.ts HTTP API client
components/
layout/ Sidebar, navigation
editor/ Tiptap editor, raw markdown editor
fields/ FieldEditor, relation picker
items/ ItemCard, ItemDetail
collections/ BoardView, ListView
common/ StatusBadge, badges, modals
search/ CommandPalette
activity/ ActivityFeed
stores/ Svelte 5 reactive stores
workspace.svelte.ts Workspace state
collections.svelte.ts Collection + item state
ui.svelte.ts Sidebar, mobile state
types/index.ts TypeScript types and constants
app.css Global styles and design tokens