Files
Kordant/tasks/shieldai-unified-restructure/11-trpc-auth-context.md
2026-05-25 12:23:23 -04:00

113 lines
5.9 KiB
Markdown

# 11. tRPC Foundation — Auth Context, Middleware, and Protected Procedures
meta:
id: shieldai-unified-restructure-11
feature: shieldai-unified-restructure
priority: P0
depends_on: [shieldai-unified-restructure-10]
tags: [backend, trpc, auth, api]
objective:
- Establish the tRPC foundation in the unified monolith: router factory, context builder with auth, middleware for protected routes, and error handling. Replace the stub tRPC setup with a production-ready auth-aware API layer.
deliverables:
- `web/src/server/api/utils.ts` — tRPC initialization and helpers:
- `t` object from `initTRPC.context<typeof createTRPCContext>().create()`
- `createTRPCRouter` factory
- `publicProcedure` — unauthenticated
- `protectedProcedure` — requires valid JWT/session
- `adminProcedure` — requires admin role
- `rateLimitedProcedure` — with basic rate limiting middleware
- `web/src/server/api/trpc.ts` — Context builder:
- `createTRPCContext({ req, res })` that extracts auth from:
- Session cookie (SolidStart session)
- `Authorization: Bearer <jwt>` header
- `x-api-key` header (for service-to-service or extension calls)
- Returns `{ db, user, session, apiKey }` in context
- Uses Drizzle `db` instance from `web/src/server/db/index.ts`
- `web/src/server/api/root.ts` — Root router:
- Imports all sub-routers
- Exports `appRouter` and `AppRouter` type
- `web/src/routes/api/trpc/[trpc].ts` — tRPC API handler:
- SolidStart API route that handles all tRPC requests
- Uses `fetchRequestHandler` from `@trpc/server/adapters/fetch`
- Wires `createTRPCContext`
- `web/src/lib/api.ts` — tRPC client:
- Updates existing file to use correct `AppRouter` type
- Adds auth token injection from localStorage/cookie
- Handles unauthorized responses (401 → redirect to login)
- Auth utilities:
- `web/src/server/auth/jwt.ts` — JWT sign/verify using `jose` or `jsonwebtoken`
- `web/src/server/auth/session.ts` — Session creation and validation
- `web/src/server/auth/password.ts` — Password hashing with `bcrypt` or `argon2`
steps:
1. Install dependencies in `web/`:
- `@trpc/server`, `@trpc/client` (already present, verify versions match)
- `jose` (modern JWT library, works in Edge runtime)
- `bcryptjs` or `argon2` for password hashing
- `zod` for input validation (already present via valibot, but standardize on one — recommend `zod` for wider ecosystem compatibility)
2. Create `web/src/server/auth/jwt.ts`:
- `signJWT(payload, secret, expiresIn)`
- `verifyJWT(token, secret)`
- Use `jose` for Edge compatibility
3. Create `web/src/server/auth/password.ts`:
- `hashPassword(password)` → async hash
- `verifyPassword(password, hash)` → boolean
4. Create `web/src/server/api/trpc.ts` (context builder):
- Parse cookies from request headers
- Try session cookie first (SolidStart session)
- Fall back to Bearer JWT
- Fall back to x-api-key
- Look up user in database via Drizzle
- Return context with `user`, `session`, `db`, `apiKey`
5. Update `web/src/server/api/utils.ts`:
- Initialize tRPC with context type
- Define `publicProcedure`
- Define `protectedProcedure` using middleware that checks `ctx.user` and throws `TRPCError.UNAUTHORIZED` if missing
- Define `adminProcedure` that checks `ctx.user.role === 'admin'`
6. Update `web/src/server/api/root.ts`:
- Keep `exampleRouter` for now
- Add placeholder imports for future routers
7. Update `web/src/routes/api/trpc/[trpc].ts`:
- Create SolidStart API route
- Use `fetchRequestHandler` with `router: appRouter`, `createContext: createTRPCContext`
8. Update `web/src/lib/api.ts`:
- Import `AppRouter` from `~/server/api/root`
- Add `httpBatchLink` with `headers` function that reads auth token from cookie/localStorage
- Add error link that handles 401 by redirecting
9. Test with a temporary protected route that returns `ctx.user`.
steps:
- Unit: Context builder returns anonymous user for unauthenticated requests
- Unit: Context builder returns full user for valid JWT
- Unit: `protectedProcedure` rejects unauthenticated requests with 401
- Unit: `adminProcedure` rejects non-admin users with 403
- Integration: tRPC client can call public and protected endpoints
- E2E: Login flow sets cookie/JWT and subsequent requests are authenticated
acceptance_criteria:
- [ ] `createTRPCContext` correctly builds context from session, JWT, or API key
- [ ] `publicProcedure` allows unauthenticated access
- [ ] `protectedProcedure` requires authentication and provides `ctx.user`
- [ ] `adminProcedure` requires admin role
- [ ] tRPC API handler responds correctly at `/api/trpc`
- [ ] Client library (`web/src/lib/api.ts`) connects and handles auth automatically
- [ ] JWT sign/verify works with `jose` library
- [ ] Password hashing uses bcrypt or argon2
validation:
- Create a temporary tRPC router with public and protected queries
- Call public query from browser → should succeed
- Call protected query without auth → should return 401
- Call protected query with valid JWT → should return user data
- Run `cd web && pnpm test` to verify auth unit tests
notes:
- The legacy Fastify auth middleware (`packages/api/src/middleware/auth.middleware.ts`) is the reference for auth logic. Port its JWT verification and API key handling.
- SolidStart sessions are cookie-based. The tRPC context should read the SolidStart session cookie and validate it.
- For the extension (task 27), API key auth will be important. Ensure `x-api-key` handling is robust.
- Consider using `lucia-auth` or `next-auth` SolidStart integration for session management, but a custom JWT approach is fine if it matches the legacy system.
- Standardize on `zod` for input validation across all tRPC routers. The current setup uses valibot — migrate to zod for consistency.
- Keep auth utilities in `web/src/server/auth/` separate from tRPC to allow reuse by other server code (e.g., background jobs).