- Create src/app/api/auth/profile/route.ts with PUT handler - Validates user is authenticated (returns 401 if not) - Validates request body has a non-empty name field - Updates user's name in the database - Returns 200 with updated user data Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
203 lines
10 KiB
Markdown
203 lines
10 KiB
Markdown
## Context
|
|
|
|
CandleAnnotator is a single-user full-stack app: Next.js 16 (App Router) + PostgreSQL (Drizzle ORM) + Python FastAPI ML service. All data is global — no users table, no auth, no data isolation. The entire app lives at `/` as a single page with 13 API routes.
|
|
|
|
We need to add multi-user support: registration, login, per-user data isolation. Lovable design mockups exist for landing, login, and register pages. The existing app workspace moves behind auth.
|
|
|
|
**Current stack**: Next.js 16, React 19, Drizzle ORM, PostgreSQL 16, Tailwind CSS, shadcn/ui, lightweight-charts, FastAPI (Python).
|
|
|
|
## Goals / Non-Goals
|
|
|
|
**Goals:**
|
|
- Email/password registration and login
|
|
- Google OAuth login
|
|
- Per-user data isolation (charts, annotations, models, settings)
|
|
- Landing page, login page, register page (from Lovable designs)
|
|
- User settings page (change password, display name, delete account)
|
|
- Auth middleware protecting app routes and API routes
|
|
- Smooth migration path for existing single-user data
|
|
|
|
**Non-Goals:**
|
|
- Email verification (defer — add later)
|
|
- Password reset via email (defer — requires email service)
|
|
- Admin panel / user management
|
|
- Role-based access control
|
|
- Rate limiting
|
|
- Two-factor authentication
|
|
- Social login beyond Google (GitHub, Discord, etc.)
|
|
- Team/organization accounts
|
|
- API key authentication for external integrations
|
|
|
|
## Decisions
|
|
|
|
### 1. Auth library: Auth.js v5 (NextAuth v5)
|
|
|
|
**Choice**: Auth.js v5 (`next-auth@5`) with JWT strategy
|
|
|
|
**Why**: Native App Router support, built-in Credentials + Google OAuth providers, `@auth/drizzle-adapter` exists for our ORM. JWT strategy avoids extra session table and keeps the ML service integration simple (pass user ID in headers).
|
|
|
|
**Alternatives considered**:
|
|
- **Lucia Auth**: Good but deprecated in favor of rolling your own. More manual setup.
|
|
- **Clerk/Auth0**: SaaS dependency, cost, overkill for this project.
|
|
- **Custom JWT**: More control but reinventing auth middleware, CSRF protection, etc.
|
|
|
|
**Session strategy**: JWT (not database sessions). User ID embedded in token. Stateless, no session table needed.
|
|
|
|
### 2. Password hashing: bcryptjs
|
|
|
|
**Choice**: `bcryptjs` (pure JS bcrypt implementation)
|
|
|
|
**Why**: No native dependencies (works in Docker without build tools), well-tested, sufficient for this use case.
|
|
|
|
**Alternatives considered**:
|
|
- **bcrypt** (native): Requires build tools in Docker, fails on some platforms.
|
|
- **argon2**: Better algorithm but requires native bindings. Overhead not justified here.
|
|
|
|
### 3. Database schema: `users` table + `user_id` FK on all tables
|
|
|
|
**Choice**: Add `users` table. Add `user_id` column (nullable initially for migration, then NOT NULL) to: `charts`, `annotations`, `annotation_types`, `span_annotations`, `span_label_types`.
|
|
|
|
**Users table**:
|
|
```
|
|
users
|
|
├── id: uuid (PK, default gen_random_uuid())
|
|
├── name: text (nullable)
|
|
├── email: text (unique, not null)
|
|
├── email_verified: timestamp (nullable)
|
|
├── password_hash: text (nullable — null for OAuth-only users)
|
|
├── image: text (nullable)
|
|
├── provider: text (default 'credentials') — 'credentials' | 'google'
|
|
├── provider_account_id: text (nullable) — Google sub ID
|
|
├── created_at: timestamp
|
|
├── updated_at: timestamp
|
|
```
|
|
|
|
**Why UUID for user IDs**: Avoids enumeration attacks, safe to expose in URLs/tokens, standard for auth systems.
|
|
|
|
**Why NOT use Drizzle adapter's default schema**: The `@auth/drizzle-adapter` creates `users`, `accounts`, `sessions`, `verification_tokens` tables. We'll use a simplified custom schema since we're using JWT (no sessions table) and deferring email verification. We'll handle the adapter mapping manually.
|
|
|
|
**Alternatives considered**:
|
|
- **Drizzle adapter default schema**: Creates 4 tables, adds complexity for features we don't need yet (verification tokens, separate accounts table). We can adopt it later if we add email verification or more OAuth providers.
|
|
- **Serial integer IDs**: Enumerable, less secure for user-facing IDs.
|
|
|
|
### 4. Routing structure
|
|
|
|
**Choice**:
|
|
```
|
|
/ → Landing page (public)
|
|
/login → Login page (public)
|
|
/register → Register page (public)
|
|
/app → Main workspace (protected, current page.tsx moves here)
|
|
/app/settings → User settings (protected)
|
|
/api/auth/* → NextAuth API routes
|
|
/api/* → Existing API routes (protected)
|
|
```
|
|
|
|
**Why**: Clean separation of public marketing pages and protected app. `/app` prefix makes middleware rules simple. Existing API routes stay at `/api/*` — just add auth check.
|
|
|
|
**Alternatives considered**:
|
|
- **Keep app at `/` and use `/landing`**: Unconventional, confuses SEO.
|
|
- **Subdomain (`app.candleannotator.com`)**: Adds deployment complexity.
|
|
|
|
### 5. Auth middleware approach
|
|
|
|
**Choice**: Next.js `proxy.ts` at project root. Uses Auth.js `auth()` to check session. Redirects:
|
|
- Unauthenticated users hitting `/app/*` or `/api/*` (except `/api/auth/*` and `/api/health`) → redirect to `/login`
|
|
- Authenticated users hitting `/login` or `/register` → redirect to `/app`
|
|
|
|
**Why**: Single enforcement point, runs on edge before page/API renders. Clean and standard Next.js pattern.
|
|
|
|
### 6. API route auth pattern
|
|
|
|
**Choice**: Helper function `getAuthUser(request)` that extracts user from JWT session. Every API route calls this at the top:
|
|
|
|
```typescript
|
|
const user = await getAuthUser();
|
|
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
// All queries filter by user.id
|
|
```
|
|
|
|
All Drizzle queries add `.where(eq(table.user_id, user.id))` for reads, and set `user_id: user.id` on inserts.
|
|
|
|
**Why**: Simple, explicit, no magic. Every route clearly shows it's user-scoped.
|
|
|
|
### 7. ML Service user context
|
|
|
|
**Choice**: Pass `X-User-ID` header from Next.js API routes to FastAPI ML service. The ML service trusts this header (internal network only, not exposed to clients).
|
|
|
|
**Why**: ML service doesn't need its own auth — it's internal. The Next.js layer validates auth and forwards user context. MLflow experiment names include user ID for isolation.
|
|
|
|
**Alternatives considered**:
|
|
- **JWT forwarding**: Requires Python JWT validation, adds complexity.
|
|
- **Separate user DB in Python**: Duplicates user management.
|
|
|
|
### 8. User data seeding on registration
|
|
|
|
**Choice**: On successful registration, seed default `annotation_types` and `span_label_types` for the new user (copy from a hardcoded default set).
|
|
|
|
**Why**: New users need at least the standard annotation types (break_up, break_down, line) and span label types to start working. Without seeding, the app would be empty/broken.
|
|
|
|
### 9. Frontend auth state
|
|
|
|
**Choice**: Use `next-auth/react` `SessionProvider` wrapping the app layout. Components use `useSession()` hook. Server components use `auth()` from `@/auth`.
|
|
|
|
**Why**: Standard Auth.js pattern. SessionProvider handles token refresh automatically.
|
|
|
|
### 10. Lovable design integration
|
|
|
|
**Choice**: Convert Lovable HTML mockups to React/Next.js components using existing Tailwind + shadcn/ui patterns. Extract the structure and classes, adapt to Next.js `<Link>`, form actions, etc. Keep the visual design but rewrite as proper React.
|
|
|
|
**Why**: Lovable generates static HTML with Tailwind. Direct reuse of CSS classes is possible, but the HTML structure needs to become React components with proper event handling.
|
|
|
|
### 11. Settings page features
|
|
|
|
**Choice**: `/app/settings` page with sections:
|
|
- **Profile**: Display name, email (read-only for OAuth users)
|
|
- **Security**: Change password (credentials users only)
|
|
- **Danger zone**: Delete account (with confirmation dialog)
|
|
|
|
**Why**: Covers essential user management needs. Change password is table stakes. Delete account is GDPR-friendly. Keep it minimal — add more settings later as needed.
|
|
|
|
## Risks / Trade-offs
|
|
|
|
**[Data migration for existing deployments]** → Create a migration script that assigns all existing data to a default "admin" user. Document in DEPLOYMENT.md. The migration adds `user_id` as nullable first, runs the script, then makes it NOT NULL.
|
|
|
|
**[OAuth users can't set password]** → If a user registers via Google, they have no password. The settings page should show "Signed in via Google" instead of password change form. If they want password login later, they'd need a "set password" flow (defer to future).
|
|
|
|
**[JWT secret rotation]** → If `AUTH_SECRET` is changed, all existing sessions invalidate. Document this. Not a risk per se, but users will be logged out.
|
|
|
|
**[ML service trusts X-User-ID header]** → The ML service is only accessible internally (bound to 127.0.0.1 in docker-compose). If it were exposed publicly, this would be a security hole. Document that ML service must never be publicly accessible.
|
|
|
|
**[No email verification initially]** → Users can register with any email. This means no password reset via email. Acceptable for initial release — add email verification later.
|
|
|
|
**[Unique constraint on annotation_types.name becomes per-user]** → Currently `annotation_types.name` has a unique constraint. With multi-user, the same name can exist for different users. Change to unique constraint on `(user_id, name)`. Same for `span_label_types.name`.
|
|
|
|
## Migration Plan
|
|
|
|
### Database migration steps:
|
|
1. Add `users` table
|
|
2. Add `user_id` column (nullable) to: `charts`, `annotation_types`, `span_label_types`, `annotations`, `span_annotations`
|
|
3. Create a default admin user
|
|
4. UPDATE all existing rows to set `user_id = admin_user_id`
|
|
5. ALTER columns to NOT NULL
|
|
6. Add foreign key constraints
|
|
7. Drop old unique constraints, add new composite unique constraints (e.g., `(user_id, name)` for annotation_types)
|
|
8. Add indexes on `user_id` columns for query performance
|
|
|
|
### Deployment steps:
|
|
1. Update `.env` with new variables: `AUTH_SECRET`, `AUTH_GOOGLE_ID`, `AUTH_GOOGLE_SECRET`
|
|
2. Run database migrations
|
|
3. Run data migration script (assign existing data to admin user)
|
|
4. Deploy new app version
|
|
5. First login as admin user to verify data
|
|
|
|
### Rollback strategy:
|
|
- Keep `user_id` nullable during testing phase
|
|
- If rollback needed: revert code, `user_id` columns remain but are ignored
|
|
- No data loss on rollback
|
|
|
|
## Open Questions
|
|
|
|
1. **Default admin credentials**: What email/password should the default admin user have for migrating existing data? Suggest: configurable via env var `DEFAULT_ADMIN_EMAIL` / `DEFAULT_ADMIN_PASSWORD`.
|
|
2. **Google OAuth project**: Do we have a Google Cloud project with OAuth credentials ready, or should we document setup steps?
|
|
3. **ML model isolation**: Should trained models be fully isolated per user (separate MLflow experiments), or is a shared model registry acceptable initially?
|