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

5.9 KiB

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).