PR-A (N-001): Dashboard-backed auth gate and workspace bootstrap#3
Conversation
Wires up the server-side auth contract Pi pinned down with the Dashboard
team: Notebook reads the shared Domain=.knowhere.ai session cookie, calls
users.getCurrentUser oRPC, and lifts {id, email, name} off body.json.user.
No JWT decode, no local user table, no separate auth UI. Anonymous
requests redirect to DASHBOARD_LOGIN_URL with a returnTo that points back
at NOTEBOOK_PUBLIC_URL.
Backing store is Neon Postgres via Drizzle (portable Postgres; no
Neon-only features). Only workspaces ships in this PR; sources /
chat_threads / chat_messages land in PR-B per the plan.
Implementation:
- src/lib/auth.ts: getCurrentUser forwards the Cookie header, extractUser
tolerates shape drift, hasSessionCookie is the cheap Edge-runtime probe,
requireUser throws the redirect.
- src/lib/workspace.ts: ensureWorkspace is idempotent on the user_id
unique index; concurrent first-calls race safely to the same row.
- src/lib/schema.ts + drizzle/0000_perfect_stellaris.sql: workspaces
table with user_id unique, deterministic namespace, btree index.
- src/lib/db.ts: neon-http driver, server-only guarded.
- src/proxy.ts: renamed from middleware.ts (Next.js 16 convention);
short-circuits anonymous requests to /login without the DB roundtrip.
- src/app/page.tsx: server component that gates on requireUser and
passes identity + workspace down to the client shell.
- .env.local.example: documents DATABASE_URL, DASHBOARD_SESSION_URL,
DASHBOARD_LOGIN_URL, NOTEBOOK_PUBLIC_URL, SESSION_COOKIE_NAMES.
Tests (20 passing):
- extractUser against malformed / partial / extra-field envelopes
- sessionCookieNames env override + fallback
- getCurrentUser: no cookie = no roundtrip, exact Cookie forwarding,
503 / non-JSON / network-error / body.json.user=null all map to null,
missing DASHBOARD_SESSION_URL throws
- ensureWorkspace: warm path, cold path with generated namespace,
concurrent first-call idempotency
Dashboard production dep tracked in Ontos-AI/knowhere-dashboard#3
(callback URL sanitization); does not block this PR.
Two contract corrections raised by @pi in review: 1. Redirect parameter: Dashboard already reads `callbackURL`, not `returnTo`. Notebook now sends `callbackURL` everywhere (auth.ts, proxy.ts, inline docs, login preview copy) so post-login lands on Notebook instead of Dashboard's default flow. 2. Cookie domain: for the shared-cookie design to work the browser needs a common parent domain. Dashboard is at knowhereto.ai, so Notebook aligns to notebook.knowhereto.ai (shared parent `.knowhereto.ai`). Local dev mirrors this with /etc/hosts entries for dashboard.local.knowhereto.ai and notebook.local.knowhereto.ai. Dashboard still needs (tracked in knowhere-dashboard#3): - add notebook.knowhereto.ai to the callback allowlist - enable Better Auth advanced.crossSubDomainCookies with domain="knowhereto.ai" so the session cookie is actually shared No code logic change; only the parameter name and the documented hosts shift. All 20 unit tests still pass.
@pi hit 405 METHOD_NOT_SUPPORTED during Chrome MCP E2E. Dashboard's oRPC transport expects POST with a JSON body, even for procedures that take no input. GET was my misreading of the contract. Change: - src/lib/auth.ts: `getCurrentUser` now sends POST <DASHBOARD_SESSION_URL> content-type: application/json cookie: <incoming> body: `{}` - src/lib/auth.test.ts: assertion renamed and tightened to verify method=POST, body="{}", content-type header, and cookie forwarding. Response shape is unchanged (`{ json: { user } }`), so `extractUser` and the rest of the call-site logic need no edit. 20 tests still pass. Keeps the fix on the Notebook side per Pi's recommendation — don't add a GET wrapper in Dashboard that would drift from the oRPC contract.
Neon's HTTP driver only speaks Neon's websocket-over-HTTP dialect, so local dev against a Docker Postgres container fails at module load with a generic "can only connect to remote Neon / Vercel / Supabase instances" warning followed by a hang. Fix: pick the Drizzle driver by env. DATABASE_DRIVER=neon (default) — @neondatabase/serverless DATABASE_DRIVER=pg — postgres-js Prod on Vercel stays on the default (Neon via the Marketplace integration). Local dev sets DATABASE_DRIVER=pg and points DATABASE_URL at whatever Postgres is reachable (the shared e2e container at localhost:55432 on this machine). The Drizzle schema is the same on both paths — switching to AWS Aurora Postgres later is also just a DATABASE_DRIVER + DATABASE_URL swap, no code change. No test changes needed — existing unit tests mock `@/lib/db` directly so they don't exercise the driver selection. Verified: pnpm test (20 passing), pnpm build (clean). Signed-off-by: suguanyang <wangbinqi77@gmail.com>
Signed-off-by: suguanyang <wangbinqi77@gmail.com>
|
Re-review on reopened Notebook PR #3 found one metadata blocker before I can carry over approval. Branch/base are correct:
Git author/committer fields are correct, but GitHub still shows Claude as a PR commit author because the first three commits still contain
Please rewrite those first three commit messages to remove the Claude co-author trailers so visible PR commit authors match the policy. Verification I ran on PR #3 after pulling the reopened branch:
Code behavior remains acceptable from my side; the blocker is the remaining visible commit co-author metadata. |
a4f04b6 to
acef957
Compare
|
Re-review passed on rewritten branch Verified:
The previous functional approval still holds for Notebook PR #3. |
Implements the mobile breakpoint behavior for the Notebook shell. Desktop
three-panel layout is unchanged.
### 2: Mobile shell navigation
- Bottom tab bar (Sources / Content / Chat) on `lg:hidden`, fixed
position with active-state highlighting. Badges show ready source
count and content-section count; a blue dot marks unread messages.
- workspace-shell.tsx exports `PanelId` type and manages `mobilePanel`
state. Mobile wrappers for each panel:
- `panel-sources` / `panel-content` / `panel-chat` with
`role="tabpanel"` and `aria-labelledby` matching the tab bar.
- `pb-14` gutter so the fixed tab bar (h-14) never covers scroll-end
content.
- Only the active panel is visible at a time; the other two are
`hidden`. CSS-based (no JS mount/unmount), so selected source,
focused chunk, and chat state survive tab switches.
- Desktop layout stays as-is inside `hidden lg:flex`.
### 3: Mobile composer and content flow
- Chat: `onCitationClick` auto-switches the mobile panel to `content`
so the user lands on the cited section immediately.
- Sources: sidebar now `w-full lg:w-[260px] xl:w-[320px]` so on
mobile it fills the viewport without a hardcoded narrow width.
- Content sections panel: header copy already uses "content sections"
language. Remains a full declarative view — no duplicate render.
- Error toast: `bottom-18` on mobile so it doesn't overlap the tab
bar; `lg:bottom-4` on desktop.
- Upload dialog: uses existing Dialog primitive which is naturally
centered on mobile (auto-responsive).
### Top nav responsiveness
- Workspace label hidden on mobile; separator hidden.
- User name/tier hidden on mobile, avatar only.
- Padding reduced to `px-4` on mobile.
### Verified
- pnpm build: clean
- pnpm test: 17 pass, 1 skip (integration)
- pnpm lint: pre-existing warning in ApiKeySettings.tsx (set-state-in-
effect), not touched by this PR
Signed-off-by: suguanyang <wangbinqi77@gmail.com>
Implements card N-001 from the MVP requirements pack (
notes/notebook/requirements-cards-v1.md) and the matching section of the technical plan. Does PR-A only — the rest of the schema and the upload/chat flows are deliberately out of scope.Summary
Domain=.knowhereto.aicookie → forward to Dashboardusers.getCurrentUseroRPC (POSTwith{}body) → readbody.json.user. No JWT decode, no local user table.DASHBOARD_LOGIN_URLwith acallbackURLpointing atNOTEBOOK_PUBLIC_URL.ensureWorkspace(userId)is idempotent on theworkspaces.user_idunique index; one workspace per user, one Knowhere namespace per workspace.postgres-jsfor local dev / any plain Postgres (AWS Aurora, Docker) viaDATABASE_DRIVER=pg.src/middleware.tsrenamed tosrc/proxy.tsfor Next.js 16 compat.Domains
Notebook aligns to
.knowhereto.aiso the browser can share the Dashboard session cookie across subdomains.https://knowhereto.aihttps://notebook.knowhereto.aihttp://dashboard.127.0.0.1.nip.io:3000http://notebook.127.0.0.1.nip.io:3001Local uses
nip.iofor shared-parent testing — no/etc/hostsrequired.Test plan
pnpm lint— cleanpnpm build— cleanpnpm test— 20 passingPOST {}togetCurrentUser, authenticated shell render, one workspace row per userOntos-AI/knowhere-dashboard#5Dependencies
Ontos-AI/knowhere-dashboard#5— callback allowlist +advanced.crossSubDomainCookies. Required for prod. This PR can merge without it; only the final production login → Notebook redirect is blocked until that lands.Environment variables
New in
.env.local.example:DATABASE_URL— Postgres (Neon in prod, any Postgres in local)DATABASE_DRIVER—neon(default) orpg(local/Aurora)DASHBOARD_SESSION_URL— oRPCusers.getCurrentUserDASHBOARD_LOGIN_URL— Dashboard login pageNOTEBOOK_PUBLIC_URL— this app's public URL, used ascallbackURLSESSION_COOKIE_NAMES— optional override of Better Auth cookie namesFiles of note
src/lib/auth.ts,src/lib/auth.test.tssrc/lib/workspace.ts,src/lib/workspace.test.tssrc/lib/schema.ts,src/lib/db.tssrc/proxy.tsdrizzle/0000_perfect_stellaris.sqlsrc/app/page.tsx(converted to server component)src/components/workspace-shell.tsx(extracted client shell)What this PR intentionally does NOT do
/api/chatroute, no retrieval, no citations — PR-ESigned-off-by: suguanyang wangbinqi77@gmail.com