Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
e7add04
fix(auth): magic link flow, funny names, initialError propagation
gaboesquivel Mar 9, 2026
97bde98
refactor(react): move LoginForm to apps/next, hooks-only in @repo/react
gaboesquivel Mar 9, 2026
84acc39
style(next): scale login heading and adjust auth grid layout
gaboesquivel Mar 9, 2026
057f50d
feat(auth): replace magic link token with 6-digit login code
gaboesquivel Mar 9, 2026
233dccd
chore: add JWT_SECRET env samples and misc updates
gaboesquivel Mar 9, 2026
1a5febd
feat(next): use routes for security settings tabs
gaboesquivel Mar 9, 2026
d97c86f
fix(next): constrain dashboard shell to viewport for scroll containment
gaboesquivel Mar 9, 2026
ca38c60
feat(next): add ScrollArea to dashboard, fix security settings UX
gaboesquivel Mar 10, 2026
3615af0
feat(settings): add username support and Vercel-style profile section
gaboesquivel Mar 10, 2026
460b123
fix: address code review feedback
gaboesquivel Mar 10, 2026
c6748a6
fix(auth): apply code review findings - trusted IP, username retries,…
gaboesquivel Mar 10, 2026
33c9d46
feat(next): add terms and privacy pages for OAuth consent
gaboesquivel Mar 10, 2026
ddc42cc
refactor(auth): magic link, oauth twitter, policy pages, migration 0014
gaboesquivel Mar 10, 2026
8d6a139
fix(fastify): Scalar login callback verify token+verificationId serve…
gaboesquivel Mar 10, 2026
79df76c
fix(fastify,next): address code review findings for auth, oauth, env,…
gaboesquivel Mar 10, 2026
dd4301b
chore(utils): remove tsup temp config, add to gitignore
gaboesquivel Mar 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cursor/rules/frontend/stack.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ See @apps/docu/content/docs/architecture/frontend-stack.mdx for complete documen

## Core Libraries
- `@tanstack/react-query`: Data fetching/async (use `@lukemorales/query-key-factory`)
- `@repo/react`: Generated React Query hooks from Hey API clients
- `@repo/react`: React Query hooks and helpers only—never UI components. Route-specific UI (e.g. login form) lives in apps, collocated by route
- `nuqs`: URL-based state (search/filters/tabs/pagination)
- `zod`: Schema validation
- `@repo/lib`: Utility library
Expand Down
1 change: 1 addition & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ paths = [
'''\.env\.example$''',
'''\.env\.test\.example$''',
'''\.env-sample$''',
'''^apps/next/\.env\.local\.example$''',
'''\.env\.sample$''',
'''\.env\.schema$''',
'''\.env\.test$''',
Expand Down
37 changes: 26 additions & 11 deletions apps/docu/content/docs/architecture/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,13 @@ Passkeys use WebAuthn for passwordless registration and sign-in. Registration cr

### Magic link (implemented)

Magic link sign-in issues a **single-use token** over email, then exchanges it for JWTs.
Magic link sign-in issues a **6-digit login code** over email, then exchanges it for JWTs. Users can either type the code into the login form or click the magic link button in the email.

**Flow:**
1. User enters email → `POST /auth/magiclink/request` → email sent with subject `{code} - {APP_NAME} verification code`
2. Email contains the 6-digit code prominently and a "Sign in" button (magic link)
3. **Option A (manual):** User types the 6-digit code in the login form → `POST /auth/magiclink/verify` with `{ token: code }` → JWTs returned
4. **Option B (link):** User clicks the button in email → callback page receives `?token={code}` → verify → cookies set, redirect

### OAuth (GitHub)

Expand Down Expand Up @@ -393,26 +399,35 @@ sequenceDiagram

## Magic link flow (web)

The email link points to the callback page, which exchanges the token for JWTs and sets cookies.
A 6-digit code is sent by email. Users can enter the code on the login page or click the magic link in the email. Both paths exchange the code for JWTs.

```mermaid
sequenceDiagram
participant Browser
participant LoginForm
participant CallbackPage as /auth/callback/magiclink
participant FastifyAPI as Fastify API
participant DB as PostgreSQL
participant Email as Email provider

Browser->>FastifyAPI: POST /auth/magiclink/request (email, callbackUrl)
FastifyAPI->>DB: upsert user + store verification token
FastifyAPI->>Email: send email with callbackUrl?token=...
Email-->>Browser: magic link

Browser->>CallbackPage: GET /auth/callback/magiclink?token=...&callbackURL=...
CallbackPage->>FastifyAPI: POST /auth/magiclink/verify (token)
FastifyAPI->>DB: consume token, create session
FastifyAPI-->>CallbackPage: { token, refreshToken }
CallbackPage->>CallbackPage: set cookies, redirect to callbackURL
FastifyAPI->>DB: upsert user + store hash(6-digit code)
FastifyAPI->>Email: send email (code + magic link button)
Email-->>Browser: email with code and link

alt Manual code entry
Browser->>LoginForm: User enters 6-digit code
LoginForm->>FastifyAPI: POST /auth/magiclink/verify (token: code)
FastifyAPI->>DB: consume code, create session
FastifyAPI-->>LoginForm: { token, refreshToken }
LoginForm->>LoginForm: updateAuthTokens, redirect
else Link click
Browser->>CallbackPage: GET /auth/callback/magiclink?token={code}&callbackUrl=...
CallbackPage->>FastifyAPI: POST /auth/magiclink/verify (token)
FastifyAPI->>DB: consume code, create session
FastifyAPI-->>CallbackPage: { token, refreshToken }
CallbackPage->>CallbackPage: set cookies, redirect to callbackUrl
end

Browser->>FastifyAPI: GET /auth/session/user (Bearer from cookie)
FastifyAPI-->>Browser: { user }
Expand Down
2 changes: 1 addition & 1 deletion apps/docu/content/docs/architecture/frontend-stack.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Core technology choices and patterns for frontend apps. See [Frontend Architectu
## Data fetching & async state

- **`@tanstack/react-query`**: default choice for async data and server state (caching, dedupe, retries)
- **`@repo/react`**: generated React Query hooks and integration for the OpenAPI client surface
- **`@repo/react`**: React Query hooks and helpers only (no UI components). Route-specific UI (e.g. login form) lives in apps, collocated by route
- **`@repo/core`**: generated, runtime-agnostic API client and types
- **`@lukemorales/query-key-factory`**: centralized query key factories (for hand-written queries)

Expand Down
5 changes: 3 additions & 2 deletions apps/docu/content/docs/development/package-conventions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ The key idea:
- **Fastify routes + TypeBox schemas** are the source of truth.
- The **OpenAPI spec** is generated from routes.
- `@repo/core` contains the **generated client + types** and a small wrapper API.
- `@repo/react` is a **React Query layer** on top of `@repo/core` (handwritten hooks/components).
- `@repo/react` is a **React Query layer** on top of `@repo/core` (handwritten hooks and helpers only—no UI components).

## Workspace layout

Expand Down Expand Up @@ -122,10 +122,11 @@ React-only helpers using **TanStack Query**.

- React Query hooks that call `@repo/core` (handwritten)
- Shared provider/context to supply a core client instance to hooks
- Small React utilities/components that are reusable across apps
- Hooks and helpers only—no UI components (login forms, cards, etc.). UI lives in apps, collocated by route

**Hard rules**

* ✅ Hooks and helpers only—never add UI components to `@repo/react`
* ✅ React-only dependencies live here
* ✅ `@tanstack/react-query` is a peer dependency (apps own the version)

Expand Down
1 change: 0 additions & 1 deletion apps/docu/content/docs/development/packages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ React-only helpers built on top of `@repo/core`.
- `useUser`
- `useHealthCheck`
- `useMagicLink`
- `LoginForm`
- `createReactApiConfig`

**Usage** (minimal):
Expand Down
3 changes: 3 additions & 0 deletions apps/fastify/.env-sample
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ OLLAMA_BASE_URL=https://ollama.example.com
# Optional: Override default model. Ollama: qwen2.5:3b. Open Router: openrouter/free
# AI_DEFAULT_MODEL=qwen2.5:3b

# JWT signing secret. Must match Next.js. Min 32 chars.
JWT_SECRET=default-jwt-secret-min-32-chars-for-dev

# Database (PGLITE=true uses in-memory PGLite; when false, DATABASE_URL is required)
PGLITE=false
DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
Expand Down
7 changes: 5 additions & 2 deletions apps/fastify/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,11 @@ profile-*
.vscode
*code-workspace

# clinic
profile*
# clinic (profile-*.json etc; exclude account profile route)
profile-*.json
profile-*.txt
!src/routes/account/profile/
!src/routes/account/profile/**
*clinic*
*flamegraph*

Expand Down
207 changes: 207 additions & 0 deletions apps/fastify/openapi/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1749,6 +1749,156 @@
}
}
},
"/account/profile/": {
"patch": {
"operationId": "accountProfileUpdate",
"summary": "Update profile",
"tags": [
"account"
],
"description": "Update profile (name, username)",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"minLength": 1,
"maxLength": 32,
"type": "string"
},
"username": {
"anyOf": [
{
"minLength": 1,
"maxLength": 48,
"pattern": "^[a-zA-Z0-9_-]{1,48}$",
"type": "string"
},
{
"type": "null"
}
]
}
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"user"
],
"properties": {
"user": {
"type": "object",
"required": [
"id",
"email",
"name",
"username"
],
"properties": {
"id": {
"type": "string"
},
"email": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"username": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
}
}
}
}
}
}
},
"401": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "string"
},
"message": {
"type": "string"
}
}
}
}
}
},
"409": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "string"
},
"message": {
"type": "string"
}
}
}
}
}
}
}
}
},
"/ai/chat": {
"post": {
"operationId": "chat",
Expand Down Expand Up @@ -2025,6 +2175,8 @@
],
"properties": {
"token": {
"pattern": "^\\d{6}$",
"description": "6-digit code",
"type": "string"
}
}
Expand Down Expand Up @@ -2056,6 +2208,28 @@
}
}
},
"400": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "string"
},
"message": {
"type": "string"
}
}
}
}
}
},
"401": {
"description": "Default Response",
"content": {
Expand Down Expand Up @@ -2099,6 +2273,28 @@
}
}
}
},
"429": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "string"
},
"message": {
"type": "string"
}
}
}
}
}
}
}
}
Expand Down Expand Up @@ -3653,6 +3849,7 @@
"id",
"email",
"name",
"username",
"emailVerified",
"linkedWallets",
"totpEnabled",
Expand Down Expand Up @@ -3682,6 +3879,16 @@
}
]
},
"username": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"emailVerified": {
"anyOf": [
{
Expand Down
3 changes: 2 additions & 1 deletion apps/fastify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@
"siwe": "^3.0.0",
"tweetnacl": "^1.0.3",
"viem": "^2.45.3",
"zod": "^4.3.6"
"zod": "^4.3.6",
"@faker-js/faker": "^9.4.0"
},
"devDependencies": {
"@electric-sql/pglite": "^0.3.15",
Expand Down
Loading
Loading