Skip to content

Commit

Permalink
Separate tRPC from Next.js app (#930)
Browse files Browse the repository at this point in the history
  • Loading branch information
junlarsen authored Oct 5, 2024
1 parent f34ff82 commit 7b1ca8a
Show file tree
Hide file tree
Showing 32 changed files with 1,131 additions and 272 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ env:
GTX_AUTH0_CLIENT_ID: ${{ secrets.GTX_AUTH0_CLIENT_ID }}
GTX_AUTH0_CLIENT_SECRET: ${{ secrets.GTX_AUTH0_CLIENT_SECRET }}
GTX_AUTH0_ISSUER: ${{ secrets.GTX_AUTH0_ISSUER }}
RPC_HOST: ${{ secrets.RPC_HOST }}
RPC_ALLOWED_ORIGINS: ${{ secrets.RPC_ALLOWED_ORIGINS }}

jobs:
build:
Expand Down
1 change: 1 addition & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@dotkomonline/auth": "workspace:*",
"@dotkomonline/env": "workspace:*",
"@dotkomonline/gateway-trpc": "workspace:*",
"@dotkomonline/proxy-nextjs": "workspace:*",
"@dotkomonline/types": "workspace:*",
"@dotkomonline/ui": "workspace:*",
"@dotkomonline/utils": "workspace:*",
Expand Down
10 changes: 10 additions & 0 deletions apps/dashboard/src/app/api/trpc/[...path]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { env } from "@dotkomonline/env"
import { createProxyRoute } from "@dotkomonline/proxy-nextjs"

const handler = createProxyRoute({
mountPath: "/api/trpc",
apiEndpoint: env.RPC_HOST,
})

export const GET = handler
export const POST = handler
4 changes: 2 additions & 2 deletions apps/dashboard/src/utils/trpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import superjson from "superjson"

const getBaseUrl = () => {
if (env.NEXT_PUBLIC_NODE_ENV === "production") {
return "https://web.online.ntnu.no"
return "https://rpc.web.online.ntnu.no"
}
return "http://localhost:3000"
return "http://localhost:3002"
}

export const trpcConfig: CreateTRPCClientOptions<AppRouter> = {
Expand Down
4 changes: 4 additions & 0 deletions apps/rpc/biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"extends": ["../../biome.json"]
}
30 changes: 30 additions & 0 deletions apps/rpc/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@dotkomonline/rpc",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsc",
"lint": "biome check . --apply",
"lint-check": "biome check .",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@dotkomonline/env": "workspace:*",
"@dotkomonline/gateway-trpc": "workspace:*",
"@fastify/cors": "^9.0.1",
"@trpc/server": "^10.45.0",
"fastify": "^4.28.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@biomejs/biome": "1.6.4",
"@dotkomonline/config": "workspace:*",
"@dotkomonline/tsconfig": "workspace:*",
"@types/node": "^20.12.7",
"tslib": "^2.6.2",
"typescript": "^5.4.5",
"tsx": "^4.15.6"
}
}
51 changes: 51 additions & 0 deletions apps/rpc/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { env } from "@dotkomonline/env"
import { type AppRouter, JwtService, appRouter, createContext } from "@dotkomonline/gateway-trpc"
import fastifyCors from "@fastify/cors"
import { type FastifyTRPCPluginOptions, fastifyTRPCPlugin } from "@trpc/server/adapters/fastify"
import type { CreateFastifyContextOptions } from "@trpc/server/dist/adapters/fastify"
import fastify from "fastify"

const jwtService = new JwtService(env.WEB_AUTH0_ISSUER, [
env.WEB_AUTH0_CLIENT_ID,
env.DASHBOARD_AUTH0_CLIENT_ID,
env.GTX_AUTH0_CLIENT_ID,
])

const allowedOrigins = env.RPC_ALLOWED_ORIGINS.split(",")

export async function createFastifyContext({ req }: CreateFastifyContextOptions) {
const bearer = req.headers.authorization
if (bearer !== undefined) {
const token = bearer.substring("Bearer ".length)
const principal = await jwtService.verify(token)
return createContext({ principal: principal.payload.sub ?? null })
}

return createContext({
principal: null,
})
}

const server = fastify({
maxParamLength: 5000,
})
server.register(fastifyCors, {
origin: allowedOrigins,
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
})

server.register(fastifyTRPCPlugin, {
prefix: "/api/trpc",
trpcOptions: {
router: appRouter,
createContext: createFastifyContext,
onError: ({ path, error }) => {
// report to error monitoring
console.error(`Error in tRPC handler on path '${path}':`, error)
},
} satisfies FastifyTRPCPluginOptions<AppRouter>["trpcOptions"],
})

await server.listen({ port: 4444 })
11 changes: 11 additions & 0 deletions apps/rpc/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "../../packages/tsconfig/tsconfig.json",
"include": ["./**/*.ts", "./**/*.tsx"],
"exclude": [],
"compilerOptions": {
"baseUrl": ".",
"jsx": "preserve",
"incremental": true,
"strictNullChecks": true
}
}
3 changes: 1 addition & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@dotkomonline/env": "workspace:*",
"@dotkomonline/gateway-edge-nextjs": "workspace:*",
"@dotkomonline/gateway-trpc": "workspace:*",
"@dotkomonline/proxy-nextjs": "workspace:*",
"@dotkomonline/types": "workspace:*",
"@dotkomonline/ui": "workspace:*",
"@dotkomonline/utils": "workspace:*",
Expand All @@ -38,7 +39,6 @@
"@trpc/client": "^10.45.0",
"@trpc/next": "^10.45.0",
"@trpc/react-query": "^10.45.0",
"@trpc/server": "^10.45.0",
"axios": "1.7.7",
"clsx": "^2.0.0",
"cors": "^2.8.5",
Expand Down Expand Up @@ -71,7 +71,6 @@
"jsdom": "^22.1.0",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.5",
"trpc-playground": "^1.0.4",
"tslib": "^2.6.2",
"typescript": "^5.4.5"
}
Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/app/api/trpc/[...path]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { env } from "@dotkomonline/env"
import { createProxyRoute } from "@dotkomonline/proxy-nextjs"

const handler = createProxyRoute({
mountPath: "/api/trpc",
apiEndpoint: env.RPC_HOST,
})

export const GET = handler
export const POST = handler
19 changes: 0 additions & 19 deletions apps/web/src/pages/api/trpc-playground.ts

This file was deleted.

13 changes: 0 additions & 13 deletions apps/web/src/pages/api/trpc/[...trpc].ts

This file was deleted.

13 changes: 0 additions & 13 deletions apps/web/src/utils/trpc/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { env } from "@dotkomonline/env"
import type { AppRouter } from "@dotkomonline/gateway-trpc"
import { type CreateTRPCClientOptions, httpBatchLink, loggerLink } from "@trpc/client"
import { createTRPCReact } from "@trpc/react-query"
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"
import superjson from "superjson"

const getBaseUrl = () => {
Expand Down Expand Up @@ -35,15 +34,3 @@ export const trpcConfig: CreateTRPCClientOptions<AppRouter> = {

// React query trpc
export const trpc = createTRPCReact<AppRouter>({})

/**
* Inference helpers for input types
* @example type HelloInput = RouterInputs['example']['hello']
**/
export type RouterInputs = inferRouterInputs<AppRouter>

/**
* Inference helpers for output types
* @example type HelloOutput = RouterOutputs['example']['hello']
**/
export type RouterOutputs = inferRouterOutputs<AppRouter>
6 changes: 3 additions & 3 deletions apps/web/src/utils/trpc/serverClient.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"use server"

import { appRouter, createCallerFactory, createContextInner } from "@dotkomonline/gateway-trpc"
import { appRouter, createCallerFactory, createContext } from "@dotkomonline/gateway-trpc"

const createCaller = createCallerFactory(appRouter)
// TODO: Add a way to get the userId from the request
export const getServerClient = async () => createCaller(await createContextInner({ auth: null }))
export const getUnauthorizedServerClient = async () => createCaller(await createContextInner({ auth: null }))
export const getServerClient = async () => createCaller(await createContext({ principal: null }))
export const getUnauthorizedServerClient = async () => createCaller(await createContext({ principal: null }))
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
},
"javascript": {
"formatter": {
"trailingCommas": "es5",
"trailingComma": "es5",
"semicolons": "asNeeded",
"lineWidth": 120,
"quoteProperties": "asNeeded"
Expand Down
22 changes: 22 additions & 0 deletions packages/auth/src/auth-options.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ServiceLayer } from "@dotkomonline/core"
import type { DefaultSession, DefaultUser, NextAuthOptions, User } from "next-auth"
import type { DefaultJWT, JWT } from "next-auth/jwt"
import Auth0Provider from "next-auth/providers/auth0"

interface Auth0IdTokenClaims {
Expand Down Expand Up @@ -37,6 +38,13 @@ declare module "next-auth" {
}
}

declare module "next-auth/jwt" {
interface JWT extends DefaultJWT, Record<string, unknown> {
accessToken?: string
refreshToken?: string
}
}

export interface AuthOptions {
auth0ClientId: string
auth0ClientSecret: string
Expand Down Expand Up @@ -66,12 +74,26 @@ export const getAuthOptions = ({
// givenName: profile.given_name,
// familyName: profile.family_name,
}),
authorization: {
params: {
scope: "openid profile email offline_access",
},
},
}),
],
session: {
strategy: "jwt",
},
callbacks: {
async jwt({ token, account }): Promise<JWT> {
if (account?.access_token) {
token.accessToken = account.access_token
}
if (account?.refresh_token) {
token.refreshToken = account.refresh_token
}
return token
},
async session({ session, token }) {
if (token.sub) {
await core.auth0SynchronizationService.populateUserWithFakeData(token.sub, token.email) // Remove when we have real data
Expand Down
5 changes: 2 additions & 3 deletions packages/env/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@ export declare type EnvironmentKeys =
| "WEB_AUTH0_CLIENT_ID"
| "WEB_AUTH0_CLIENT_SECRET"
| "WEB_AUTH0_ISSUER"
| "GTX_AUTH0_CLIENT_ID"
| "GTX_AUTH0_CLIENT_SECRET"
| "GTX_AUTH0_ISSUER"
| "RPC_HOST"
| "RPC_ALLOWED_ORIGINS"

export declare type Environment = Record<EnvironmentKeys, string>

Expand Down
6 changes: 6 additions & 0 deletions packages/env/src/env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export const createEnvironment = () =>
FAGKOM_STRIPE_SECRET_KEY: environmentVariableSchema,
FAGKOM_STRIPE_WEBHOOK_SECRET: environmentVariableSchema,

RPC_ALLOWED_ORIGINS: environmentVariableSchema,
RPC_HOST: environmentVariableSchema,

S3_BUCKET_MONOWEB: environmentVariableSchema,
},
client: {
Expand Down Expand Up @@ -69,6 +72,9 @@ export const createEnvironment = () =>
FAGKOM_STRIPE_SECRET_KEY: process.env.FAGKOM_STRIPE_SECRET_KEY,
FAGKOM_STRIPE_WEBHOOK_SECRET: process.env.FAGKOM_STRIPE_WEBHOOK_SECRET,

RPC_ALLOWED_ORIGINS: process.env.RPC_ALLOWED_ORIGINS,
RPC_HOST: process.env.RPC_HOST,

S3_BUCKET_MONOWEB: process.env.S3_BUCKET_MONOWEB,
},
skipValidation: process.env.DOCKER_BUILD !== undefined,
Expand Down
6 changes: 3 additions & 3 deletions packages/gateway-edge-nextjs/src/cal/cal.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { appRouter, createContextInner, transformer } from "@dotkomonline/gateway-trpc"
import { appRouter, createContext, transformer } from "@dotkomonline/gateway-trpc"
import type { Event } from "@dotkomonline/types"
import { createServerSideHelpers } from "@trpc/react-query/server"
import ical, { type ICalEventData } from "ical-generator"
Expand All @@ -9,8 +9,8 @@ import { authOptions } from "../../../auth/src/web.app"

const helpers = createServerSideHelpers({
router: appRouter,
ctx: await createContextInner({
auth: null,
ctx: await createContext({
principal: null,
}),
transformer, // optional - adds superjson serialization
})
Expand Down
1 change: 1 addition & 0 deletions packages/gateway-trpc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@dotkomonline/db": "workspace:*",
"@trpc/server": "^10.45.0",
"superjson": "^2.0.0",
"jose": "^5.4.0",
"zod": "^3.22.4"
},
"peerDependencies": {
Expand Down
23 changes: 3 additions & 20 deletions packages/gateway-trpc/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,17 @@
import { authOptions } from "@dotkomonline/auth/src/web.app"
import { createServiceLayer } from "@dotkomonline/core"
import { kysely } from "@dotkomonline/db"
import type { inferAsyncReturnType } from "@trpc/server"
import type { CreateNextContextOptions } from "@trpc/server/adapters/next"
import { getServerSession } from "next-auth"

interface AuthContextProps {
auth: {
userId: string
} | null
principal: string | null
}

export const createContextInner = async (opts: AuthContextProps) => {
export const createContext = async (opts: AuthContextProps) => {
const services = await createServiceLayer({ db: kysely })
return {
...services,
auth: opts.auth,
principal: opts.principal,
}
}

export const createContext = async (opts: CreateNextContextOptions) => {
const session = await getServerSession(opts.req, opts.res, authOptions)
if (session !== null) {
return createContextInner({
auth: {
userId: session.user.id,
},
})
}
return createContextInner({ auth: null })
}

export type Context = inferAsyncReturnType<typeof createContext>
Loading

0 comments on commit 7b1ca8a

Please sign in to comment.