Skip to content

PR-A (N-001): Dashboard-backed auth gate and workspace bootstrap#3

Merged
suguanYang merged 5 commits into
feat/wangbinqi/notebook-mvpfrom
feat/wangbinqi/n-001-auth-gate
May 7, 2026
Merged

PR-A (N-001): Dashboard-backed auth gate and workspace bootstrap#3
suguanYang merged 5 commits into
feat/wangbinqi/notebook-mvpfrom
feat/wangbinqi/n-001-auth-gate

Conversation

@suguanYang
Copy link
Copy Markdown
Contributor

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.

Previously opened as PR #2 on feat/n-001-auth-gate. Closed & replaced to adopt the new branch-name convention (xxx/wangbinqi/xxx) and retarget staging. Commit authors are now suguanyang <wangbinqi77@gmail.com>. Same logic.

Summary

  • Server-side auth contract: shared Domain=.knowhereto.ai cookie → forward to Dashboard users.getCurrentUser oRPC (POST with {} body) → read body.json.user. No JWT decode, no local user table.
  • Anonymous requests redirect to DASHBOARD_LOGIN_URL with a callbackURL pointing at NOTEBOOK_PUBLIC_URL.
  • ensureWorkspace(userId) is idempotent on the workspaces.user_id unique index; one workspace per user, one Knowhere namespace per workspace.
  • Drizzle schema; Neon HTTP driver in prod (default) and postgres-js for local dev / any plain Postgres (AWS Aurora, Docker) via DATABASE_DRIVER=pg.
  • src/middleware.ts renamed to src/proxy.ts for Next.js 16 compat.

Domains

Notebook aligns to .knowhereto.ai so the browser can share the Dashboard session cookie across subdomains.

Env Dashboard Notebook
prod https://knowhereto.ai https://notebook.knowhereto.ai
local http://dashboard.127.0.0.1.nip.io:3000 http://notebook.127.0.0.1.nip.io:3001

Local uses nip.io for shared-parent testing — no /etc/hosts required.

Test plan

  • pnpm lint — clean
  • pnpm build — clean
  • pnpm test — 20 passing
  • Chrome MCP E2E (by @pi): anonymous → login redirect, shared-cookie on Notebook origin, POST {} to getCurrentUser, authenticated shell render, one workspace row per user
  • Cross-subdomain production login flow — blocked on Ontos-AI/knowhere-dashboard#5

Dependencies

  • 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_DRIVERneon (default) or pg (local/Aurora)
  • DASHBOARD_SESSION_URL — oRPC users.getCurrentUser
  • DASHBOARD_LOGIN_URL — Dashboard login page
  • NOTEBOOK_PUBLIC_URL — this app's public URL, used as callbackURL
  • SESSION_COOKIE_NAMES — optional override of Better Auth cookie names

Files of note

  • src/lib/auth.ts, src/lib/auth.test.ts
  • src/lib/workspace.ts, src/lib/workspace.test.ts
  • src/lib/schema.ts, src/lib/db.ts
  • src/proxy.ts
  • drizzle/0000_perfect_stellaris.sql
  • src/app/page.tsx (converted to server component)
  • src/components/workspace-shell.tsx (extracted client shell)

What this PR intentionally does NOT do

  • No sources / chat_threads / chat_messages tables — PR-B
  • No upload flow, no parsing handoff — PR-C
  • No chunks viewer wiring — PR-D
  • No /api/chat route, no retrieval, no citations — PR-E

Signed-off-by: suguanyang wangbinqi77@gmail.com

suguanYang added 5 commits May 6, 2026 10:53
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>
@suguanYang
Copy link
Copy Markdown
Contributor Author

Re-review on reopened Notebook PR #3 found one metadata blocker before I can carry over approval.

Branch/base are correct:

  • feat/wangbinqi/n-001-auth-gate -> staging

Git author/committer fields are correct, but GitHub still shows Claude as a PR commit author because the first three commits still contain Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> trailers.

gh pr view 3 --json commits shows:

  • a62ae0c authors: suguanyang <wangbinqi77@gmail.com>, Claude Opus 4.6 <noreply@anthropic.com>
  • 19ee5ee authors: suguanyang <wangbinqi77@gmail.com>, Claude Opus 4.6 <noreply@anthropic.com>
  • aca9c8d authors: suguanyang <wangbinqi77@gmail.com>, Claude Opus 4.6 <noreply@anthropic.com>
  • faf9329 authors: suguanyang <wangbinqi77@gmail.com> only
  • a4f04b6 authors: suguanyang <wangbinqi77@gmail.com> only

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:

  • pnpm install --frozen-lockfile passed.
  • pnpm lint && pnpm test passed: 20 tests.
  • DATABASE_DRIVER=pg DATABASE_URL=postgres://postgres:postgres@localhost:55432/knowhere_notebook_e2e pnpm db:push passed; no schema changes detected.
  • DATABASE_DRIVER=pg ... pnpm build passed.

Code behavior remains acceptable from my side; the blocker is the remaining visible commit co-author metadata.

@suguanYang suguanYang force-pushed the feat/wangbinqi/n-001-auth-gate branch from a4f04b6 to acef957 Compare May 7, 2026 03:21
@suguanYang
Copy link
Copy Markdown
Contributor Author

Re-review passed on rewritten branch acef957.

Verified:

  • PR base/head remain staging <- feat/wangbinqi/n-001-auth-gate
  • GitHub PR commit authors now show only suguanyang <wangbinqi77@gmail.com>
  • no Co-Authored-By: Claude trailers remain in PR commit messages
  • pnpm lint passed
  • pnpm test passed: 20 tests
  • DATABASE_DRIVER=pg ... pnpm db:push passed with no schema changes
  • DATABASE_DRIVER=pg ... pnpm build passed

The previous functional approval still holds for Notebook PR #3.

@suguanYang suguanYang changed the base branch from staging to feat/wangbinqi/notebook-mvp May 7, 2026 03:39
@suguanYang suguanYang merged commit 1faf377 into feat/wangbinqi/notebook-mvp May 7, 2026
suguanYang added a commit that referenced this pull request May 8, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant