diff --git a/apps/web-api/package.json b/apps/web-api/package.json index c522229c..9c6f7e93 100644 --- a/apps/web-api/package.json +++ b/apps/web-api/package.json @@ -13,6 +13,7 @@ "@codepod/prisma": "workspace:*", "@kubernetes/client-node": "^0.17.1", "@prisma/client": "4.3.1", + "@trpc/server": "^10.38.5", "apollo-server": "^3.5.0", "apollo-server-core": "^3.10.3", "apollo-server-express": "3.10.2", @@ -34,7 +35,8 @@ "ws": "^8.2.3", "y-prosemirror": "^1.2.1", "y-protocols": "^1.0.5", - "yjs": "^13.6.7" + "yjs": "^13.6.7", + "zod": "^3.22.2" }, "devDependencies": { "@jest/globals": "^29.6.4", diff --git a/apps/web-api/src/resolver.test.ts b/apps/web-api/src/resolver.test.ts deleted file mode 100644 index 773853b8..00000000 --- a/apps/web-api/src/resolver.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { typeDefs } from "./typedefs"; -import { resolvers } from "./resolver"; -import { ApolloServer, gql } from "apollo-server-express"; -// import { gql } from "@apollo/client"; - -import { describe, expect, test } from "@jest/globals"; - -describe("sum module", () => { - test("adds 1 + 2 to equal 3", () => { - expect(1 + 2).toBe(3); - }); - - test("returns hello with the provided name", async () => { - const testServer = new ApolloServer({ - typeDefs, - resolvers, - }); - - const result = await testServer.executeOperation({ - query: gql` - query hello { - hello - } - `, - // query: "query hello() { hello }", - variables: { name: "world" }, - }); - - expect(result.errors).toBeUndefined(); - expect(result.data?.hello).toBe("Hello world!"); - }); -}); diff --git a/apps/web-api/src/resolver_repo.ts b/apps/web-api/src/resolver_repo.ts index 8ef26b2d..96e0f28a 100644 --- a/apps/web-api/src/resolver_repo.ts +++ b/apps/web-api/src/resolver_repo.ts @@ -1,3 +1,6 @@ +import { t } from "./trpc"; +import { z } from "zod"; + // nanoid v4 does not work with nodejs. https://github.com/ai/nanoid/issues/365 import { customAlphabet } from "nanoid/async"; import { lowercase, numbers } from "nanoid-dictionary"; @@ -41,7 +44,7 @@ async function ensurePodEditAccess({ id, userId }) { } } -async function getDashboardRepos(_, __, { userId }) { +const getDashboardRepos = t.procedure.query(async ({ ctx: { userId } }) => { if (!userId) throw Error("Unauthenticated"); const repos = await prisma.repo.findMany({ where: { @@ -76,7 +79,7 @@ async function getDashboardRepos(_, __, { userId }) { : repo.updatedAt, }; }); -} +}); async function updateUserRepoData({ userId, repoId }) { // FIXME I should probably rename this from query to mutation? @@ -110,27 +113,29 @@ async function updateUserRepoData({ userId, repoId }) { } } -async function repo(_, { id }, { userId }) { - // a user can only access a private repo if he is the owner or a collaborator - const repo = await prisma.repo.findFirst({ - where: { - OR: [ - { id, public: true }, - { id, owner: { id: userId || "undefined" } }, - { id, collaborators: { some: { id: userId || "undefined" } } }, - ], - }, - include: { - owner: true, - collaborators: true, - }, +const repo = t.procedure + .input(z.object({ id: z.string() })) + .query(async ({ input: { id }, ctx: { userId } }) => { + // a user can only access a private repo if he is the owner or a collaborator + const repo = await prisma.repo.findFirst({ + where: { + OR: [ + { id, public: true }, + { id, owner: { id: userId || "undefined" } }, + { id, collaborators: { some: { id: userId || "undefined" } } }, + ], + }, + include: { + owner: true, + collaborators: true, + }, + }); + if (!repo) throw Error("Repo not found"); + await updateUserRepoData({ userId, repoId: id }); + return repo; }); - if (!repo) throw Error("Repo not found"); - await updateUserRepoData({ userId, repoId: id }); - return repo; -} -async function createRepo(_, {}, { userId }) { +const createRepo = t.procedure.mutation(async ({ ctx: { userId } }) => { if (!userId) throw Error("Unauthenticated"); const repo = await prisma.repo.create({ data: { @@ -146,270 +151,303 @@ async function createRepo(_, {}, { userId }) { }, }); return repo; -} +}); -async function updateVisibility(_, { repoId, isPublic }, { userId }) { - if (!userId) throw Error("Unauthenticated"); - const repo = await prisma.repo.findFirst({ - where: { - id: repoId, - owner: { id: userId || "undefined" }, - }, - }); - if (!repo) throw Error("Repo not found"); - await prisma.repo.update({ - where: { - id: repoId, - }, - data: { - public: isPublic, - }, +const updateVisibility = t.procedure + .input(z.object({ repoId: z.string(), isPublic: z.boolean() })) + .mutation(async ({ input: { repoId, isPublic }, ctx: { userId } }) => { + if (!userId) throw Error("Unauthenticated"); + const repo = await prisma.repo.findFirst({ + where: { + id: repoId, + owner: { id: userId || "undefined" }, + }, + }); + if (!repo) throw Error("Repo not found"); + await prisma.repo.update({ + where: { + id: repoId, + }, + data: { + public: isPublic, + }, + }); + return true; }); - return true; -} -async function updateRepo(_, { id, name }, { userId }) { - if (!userId) throw Error("Unauthenticated"); - const repo = await prisma.repo.findFirst({ - where: { - id, - owner: { - id: userId, +const updateRepo = t.procedure + .input(z.object({ id: z.string(), name: z.string() })) + .mutation(async ({ input: { id, name }, ctx: { userId } }) => { + if (!userId) throw Error("Unauthenticated"); + const repo = await prisma.repo.findFirst({ + where: { + id, + owner: { + id: userId, + }, }, - }, - }); - if (!repo) throw new Error("Repo not found"); - const updatedRepo = await prisma.repo.update({ - where: { - id, - }, - data: { - name, - }, + }); + if (!repo) throw new Error("Repo not found"); + const updatedRepo = await prisma.repo.update({ + where: { + id, + }, + data: { + name, + }, + }); + return true; }); - return true; -} -async function deleteRepo(_, { id }, { userId }) { - if (!userId) throw Error("Unauthenticated"); - // only a repo owner can delete a repo. - const repo = await prisma.repo.findFirst({ - where: { - id, - owner: { - id: userId, +const deleteRepo = t.procedure + .input(z.object({ id: z.string() })) + .mutation(async ({ input: { id }, ctx: { userId } }) => { + if (!userId) throw Error("Unauthenticated"); + // only a repo owner can delete a repo. + const repo = await prisma.repo.findFirst({ + where: { + id, + owner: { + id: userId, + }, }, - }, - }); - if (!repo) throw new Error("Repo not found"); - // 1. delete all pods - await prisma.pod.deleteMany({ - where: { - repo: { - id: repo.id, + }); + if (!repo) throw new Error("Repo not found"); + // 1. delete all pods + await prisma.pod.deleteMany({ + where: { + repo: { + id: repo.id, + }, }, - }, - }); - // 2. delete UserRepoData - await prisma.userRepoData.deleteMany({ - where: { - repo: { + }); + // 2. delete UserRepoData + await prisma.userRepoData.deleteMany({ + where: { + repo: { + id: repo.id, + }, + }, + }); + // 3. delete the repo itself + await prisma.repo.delete({ + where: { id: repo.id, }, - }, - }); - // 3. delete the repo itself - await prisma.repo.delete({ - where: { - id: repo.id, - }, + }); + return true; }); - return true; -} -async function addCollaborator(_, { repoId, email }, { userId }) { - // make sure the repo is writable by this user - if (!userId) throw new Error("Not authenticated."); - // 1. find the repo - const repo = await prisma.repo.findFirst({ - where: { - id: repoId, - owner: { id: userId }, - }, - include: { - collaborators: true, - }, - }); - if (!repo) throw new Error("Repo not found or you are not the owner."); - // 2. find the user - const other = await prisma.user.findFirst({ - where: { - email, - }, - }); - if (!other) throw new Error("User not found"); - if (other.id === userId) throw new Error("You are already the owner."); - if (repo.collaborators.findIndex((user) => user.id === other.id) !== -1) - throw new Error("The user is already a collaborator."); - // 3. add the user to the repo - const res = await prisma.repo.update({ - where: { - id: repoId, - }, - data: { - collaborators: { connect: { id: other.id } }, - }, +const addCollaborator = t.procedure + .input(z.object({ repoId: z.string(), email: z.string() })) + .mutation(async ({ input: { repoId, email }, ctx: { userId } }) => { + // make sure the repo is writable by this user + if (!userId) throw new Error("Not authenticated."); + // 1. find the repo + const repo = await prisma.repo.findFirst({ + where: { + id: repoId, + owner: { id: userId }, + }, + include: { + collaborators: true, + }, + }); + if (!repo) throw new Error("Repo not found or you are not the owner."); + // 2. find the user + const other = await prisma.user.findFirst({ + where: { + email, + }, + }); + if (!other) throw new Error("User not found"); + if (other.id === userId) throw new Error("You are already the owner."); + if (repo.collaborators.findIndex((user) => user.id === other.id) !== -1) + throw new Error("The user is already a collaborator."); + // 3. add the user to the repo + const res = await prisma.repo.update({ + where: { + id: repoId, + }, + data: { + collaborators: { connect: { id: other.id } }, + }, + }); + return true; }); - return true; -} -async function deleteCollaborator(_, { repoId, collaboratorId }, { userId }) { - if (!userId) throw new Error("Not authenticated."); - // 1. find the repo - const repo = await prisma.repo.findFirst({ - where: { - id: repoId, - owner: { id: userId }, - }, - }); - // 2. delete the user from the repo - if (!repo) throw new Error("Repo not found or you are not the owner."); - const res = await prisma.repo.update({ - where: { - id: repoId, - }, - data: { - collaborators: { disconnect: { id: collaboratorId } }, - }, +const deleteCollaborator = t.procedure + .input(z.object({ repoId: z.string(), collaboratorId: z.string() })) + .mutation(async ({ input: { repoId, collaboratorId }, ctx: { userId } }) => { + if (!userId) throw new Error("Not authenticated."); + // 1. find the repo + const repo = await prisma.repo.findFirst({ + where: { + id: repoId, + owner: { id: userId }, + }, + }); + // 2. delete the user from the repo + if (!repo) throw new Error("Repo not found or you are not the owner."); + const res = await prisma.repo.update({ + where: { + id: repoId, + }, + data: { + collaborators: { disconnect: { id: collaboratorId } }, + }, + }); + return true; }); - return true; -} -async function star(_, { repoId }, { userId }) { - // make sure the repo is visible by this user - if (!userId) throw new Error("Not authenticated."); - let repo = await prisma.repo.findFirst({ - where: { - id: repoId, - OR: [ - { owner: { id: userId || "undefined" } }, - { collaborators: { some: { id: userId || "undefined" } } }, - { public: true }, - ], - }, - }); - if (!repo) throw new Error("Repo not found."); - // 3. add the user to the repo - await prisma.repo.update({ - where: { - id: repoId, - }, - data: { - stargazers: { connect: { id: userId } }, - }, +const star = t.procedure + .input(z.object({ repoId: z.string() })) + .mutation(async ({ input: { repoId }, ctx: { userId } }) => { + // make sure the repo is visible by this user + if (!userId) throw new Error("Not authenticated."); + let repo = await prisma.repo.findFirst({ + where: { + id: repoId, + OR: [ + { owner: { id: userId || "undefined" } }, + { collaborators: { some: { id: userId || "undefined" } } }, + { public: true }, + ], + }, + }); + if (!repo) throw new Error("Repo not found."); + // 3. add the user to the repo + await prisma.repo.update({ + where: { + id: repoId, + }, + data: { + stargazers: { connect: { id: userId } }, + }, + }); + return true; }); - return true; -} -async function unstar(_, { repoId }, { userId }) { - if (!userId) throw new Error("Not authenticated."); - // 1. find the repo - const repo = await prisma.repo.findFirst({ - where: { - id: repoId, - }, - }); - // 2. delete the user from the repo - if (!repo) throw new Error("Repo not found."); - await prisma.repo.update({ - where: { - id: repoId, - }, - data: { - stargazers: { disconnect: { id: userId } }, - }, +const unstar = t.procedure + .input(z.object({ repoId: z.string() })) + .mutation(async ({ input: { repoId }, ctx: { userId } }) => { + if (!userId) throw new Error("Not authenticated."); + // 1. find the repo + const repo = await prisma.repo.findFirst({ + where: { + id: repoId, + }, + }); + // 2. delete the user from the repo + if (!repo) throw new Error("Repo not found."); + await prisma.repo.update({ + where: { + id: repoId, + }, + data: { + stargazers: { disconnect: { id: userId } }, + }, + }); + return true; }); - return true; -} -async function copyRepo(_, { repoId }, { userId }) { - // Find the repo - const repo = await prisma.repo.findFirst({ - where: { - id: repoId, - }, - include: { - pods: { - include: { - parent: true, +const copyRepo = t.procedure + .input(z.object({ repoId: z.string() })) + .mutation(async ({ input: { repoId }, ctx: { userId } }) => { + if (!userId) throw Error("Unauthenticated"); + // Find the repo + const repo = await prisma.repo.findFirst({ + where: { + id: repoId, + }, + include: { + pods: { + include: { + parent: true, + }, }, }, - }, - }); - if (!repo) throw new Error("Repo not found"); + }); + if (!repo) throw new Error("Repo not found"); - // Create a new repo - const { id } = await createRepo(_, {}, { userId }); - // update the repo name - await prisma.repo.update({ - where: { - id, - }, - data: { - name: repo.name ? `Copy of ${repo.name}` : `Copy of ${repo.id}`, - }, - }); + // Create a new repo + const { id } = await prisma.repo.create({ + data: { + id: await nanoid(), + owner: { + connect: { + id: userId, + }, + }, + }, + include: { + owner: true, + }, + }); - // Create new id for each pod - const sourcePods = repo.pods; - const idMap = await sourcePods.reduce(async (acc, pod) => { - const map = await acc; - const newId = await nanoid(); - map.set(pod.id, newId); - return map; - }, Promise.resolve(new Map())); + // update the repo name + await prisma.repo.update({ + where: { + id, + }, + data: { + name: repo.name ? `Copy of ${repo.name}` : `Copy of ${repo.id}`, + }, + }); - // Update the parent/child relationship with their new ids - const targetPods = sourcePods.map((pod) => { - return { - ...pod, - id: idMap.get(pod.id), - parent: pod.parent ? { id: idMap.get(pod.parent.id) } : undefined, - repoId: id, - parentId: pod.parentId ? idMap.get(pod.parentId) : undefined, - }; - }); + // Create new id for each pod + const sourcePods = repo.pods; + const idMap = await sourcePods.reduce(async (acc, pod) => { + const map = await acc; + const newId = await nanoid(); + map.set(pod.id, newId); + return map; + }, Promise.resolve(new Map())); - // Add all nodes without parent/child relationship to the new repo. - // - // TODO: it updates the parent/child relationship automatically somehow,maybe - // because the parentId? Try to figure out why, then refactor addPods method. - await prisma.pod.createMany({ - data: targetPods.map((pod) => ({ - ...pod, - id: pod.id, - index: 0, - parent: undefined, - })), + // Update the parent/child relationship with their new ids + const targetPods = sourcePods.map((pod) => { + return { + ...pod, + id: idMap.get(pod.id), + parent: pod.parent ? { id: idMap.get(pod.parent.id) } : undefined, + repoId: id, + parentId: pod.parentId ? idMap.get(pod.parentId) : undefined, + }; + }); + + // Add all nodes without parent/child relationship to the new repo. + // + // TODO: it updates the parent/child relationship automatically somehow,maybe + // because the parentId? Try to figure out why, then refactor addPods method. + await prisma.pod.createMany({ + data: targetPods.map((pod) => ({ + ...pod, + id: pod.id, + index: 0, + parent: undefined, + })), + }); + + return id; }); - return id; -} +export const repoRouter = t.router({ + hello: t.procedure + .input(z.string().nullish()) + .query(({ input, ctx }) => `hello ${input ?? ctx.userId ?? "world"}`), + + // The actual resolvers + repo, + getDashboardRepos, -export const RepoResolver = { - Query: { - repo, - getDashboardRepos, - }, - Mutation: { - createRepo, - updateRepo, - deleteRepo, - copyRepo, - addCollaborator, - updateVisibility, - deleteCollaborator, - star, - unstar, - }, -}; + // mutations + createRepo, + updateRepo, + deleteRepo, + copyRepo, + addCollaborator, + updateVisibility, + deleteCollaborator, + star, + unstar, +}); diff --git a/apps/web-api/src/resolver_user.ts b/apps/web-api/src/resolver_user.ts index d04be227..536f7dbc 100644 --- a/apps/web-api/src/resolver_user.ts +++ b/apps/web-api/src/resolver_user.ts @@ -1,3 +1,6 @@ +import { t } from "./trpc"; +import { z } from "zod"; + import bcrypt from "bcryptjs"; import jwt from "jsonwebtoken"; import { OAuth2Client } from "google-auth-library"; @@ -8,21 +11,31 @@ import { lowercase, numbers } from "nanoid-dictionary"; import prisma from "@codepod/prisma"; +import { ENV } from "./utils"; + const nanoid = customAlphabet(lowercase + numbers, 20); -export function createUserResolver({ jwtSecret, googleClientId }) { - async function me(_, __, { userId }) { - if (!userId) throw Error("Unauthenticated"); - const user = await prisma.user.findFirst({ - where: { - id: userId, - }, - }); - if (!user) throw Error("Authorization token is not valid"); - return user; - } +const me = t.procedure.query(async ({ ctx: { userId } }) => { + if (!userId) throw Error("Unauthenticated"); + const user = await prisma.user.findFirst({ + where: { + id: userId, + }, + }); + if (!user) throw Error("Authorization token is not valid"); + return user; +}); - async function signup(_, { email, password, firstname, lastname }) { +const signup = t.procedure + .input( + z.object({ + email: z.string(), + password: z.string(), + firstname: z.string(), + lastname: z.string(), + }) + ) + .mutation(async ({ input: { email, password, firstname, lastname } }) => { const salt = await bcrypt.genSalt(10); const hashed = await bcrypt.hash(password, salt); const user = await prisma.user.create({ @@ -35,38 +48,46 @@ export function createUserResolver({ jwtSecret, googleClientId }) { }, }); return { - token: jwt.sign({ id: user.id }, jwtSecret, { + token: jwt.sign({ id: user.id }, ENV.JWT_SECRET, { expiresIn: "30d", }), }; - } + }); - async function updateUser(_, { email, firstname, lastname }, { userId }) { - if (!userId) throw Error("Unauthenticated"); - let user = await prisma.user.findFirst({ - where: { - id: userId, - }, - }); - if (!user) throw Error("User not found."); - if (user.id !== userId) { - throw new Error("You do not have access to the user."); +const updateUser = t.procedure + .input( + z.object({ email: z.string(), firstname: z.string(), lastname: z.string() }) + ) + .mutation( + async ({ input: { email, firstname, lastname }, ctx: { userId } }) => { + if (!userId) throw Error("Unauthenticated"); + let user = await prisma.user.findFirst({ + where: { + id: userId, + }, + }); + if (!user) throw Error("User not found."); + if (user.id !== userId) { + throw new Error("You do not have access to the user."); + } + // do the udpate + await prisma.user.update({ + where: { + id: userId, + }, + data: { + firstname, + lastname, + email, + }, + }); + return true; } - // do the udpate - await prisma.user.update({ - where: { - id: userId, - }, - data: { - firstname, - lastname, - email, - }, - }); - return true; - } + ); - async function login(_, { email, password }) { +const login = t.procedure + .input(z.object({ email: z.string(), password: z.string() })) + .mutation(async ({ input: { email, password } }) => { // FIXME findUnique seems broken https://github.com/prisma/prisma/issues/5071 const user = await prisma.user.findFirst({ where: { @@ -82,19 +103,21 @@ export function createUserResolver({ jwtSecret, googleClientId }) { return { id: user.id, email: user.email, - token: jwt.sign({ id: user.id }, jwtSecret, { + token: jwt.sign({ id: user.id }, ENV.JWT_SECRET, { expiresIn: "30d", }), }; } - } + }); - const client = new OAuth2Client(googleClientId); +const client = new OAuth2Client(ENV.GOOGLE_CLIENT_ID); - async function loginWithGoogle(_, { idToken }) { +const loginWithGoogle = t.procedure + .input(z.object({ idToken: z.string() })) + .mutation(async ({ input: { idToken } }) => { const ticket = await client.verifyIdToken({ idToken: idToken, - audience: googleClientId, // Specify the CLIENT_ID of the app that accesses the backend + audience: ENV.GOOGLE_CLIENT_ID, // Specify the CLIENT_ID of the app that accesses the backend // Or, if multiple clients access the backend: //[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3] }); @@ -122,21 +145,21 @@ export function createUserResolver({ jwtSecret, googleClientId }) { return { id: user.id, email: user.email, - token: jwt.sign({ id: user.id }, jwtSecret, { + token: jwt.sign({ id: user.id }, ENV.JWT_SECRET, { expiresIn: "30d", }), }; - } + }); - return { - Query: { - me, - }, - Mutation: { - login, - loginWithGoogle, - signup, - updateUser, - }, - }; -} +export const userRouter = t.router({ + hello: t.procedure + .input(z.string().nullish()) + .query(({ input, ctx }) => `hello ${input ?? ctx.userId ?? "world"}`), + + // The actual resolvers + me, + login, + loginWithGoogle, + signup, + updateUser, +}); diff --git a/apps/web-api/src/server.ts b/apps/web-api/src/server.ts index e79d3502..4c621c15 100644 --- a/apps/web-api/src/server.ts +++ b/apps/web-api/src/server.ts @@ -1,83 +1,13 @@ -import { WebSocketServer } from "ws"; - import express from "express"; import http from "http"; -import { ApolloServer, gql } from "apollo-server-express"; -import jwt from "jsonwebtoken"; - -import { - ApolloServerPluginLandingPageProductionDefault, - ApolloServerPluginLandingPageLocalDefault, -} from "apollo-server-core"; - -import { typeDefs } from "./typedefs"; -import { createUserResolver } from "./resolver_user"; -import { RepoResolver } from "./resolver_repo"; - -if (!process.env.JWT_SECRET) { - throw new Error("JWT_SECRET env variable is not set."); -} -// FIXME even if this is undefined, the token verification still works. Looks -// like I only need to set client ID in the frontend? -if (!process.env.GOOGLE_CLIENT_ID) { - console.log("WARNING: GOOGLE_CLIENT_ID env variable is not set."); -} - -const UserResolver = createUserResolver({ - jwtSecret: process.env.JWT_SECRET, - googleClientId: process.env.GOOGLE_CLIENT_ID, -}); - -export const resolvers = { - Query: { - hello: () => { - return "Hello world!"; - }, - ...UserResolver.Query, - ...RepoResolver.Query, - }, - Mutation: { - ...UserResolver.Mutation, - ...RepoResolver.Mutation, - }, -}; - -interface TokenInterface { - id: string; -} +import { expressMiddleware } from "./trpc"; export async function startServer({ port }) { - const apollo = new ApolloServer({ - typeDefs, - resolvers, - context: ({ req }) => { - const token = req?.headers?.authorization?.slice(7); - let userId; - - console.log("in context", token); - - if (token) { - const decoded = jwt.verify( - token, - process.env.JWT_SECRET as string - ) as TokenInterface; - userId = decoded.id; - } - return { - userId, - }; - }, - plugins: [ApolloServerPluginLandingPageLocalDefault({ embed: true })], - }); const expapp = express(); expapp.use(express.json({ limit: "20mb" })); + expapp.use("/trpc", expressMiddleware); const http_server = http.createServer(expapp); - const wss = new WebSocketServer({ server: http_server }); - // graphql api will be available at /graphql - - await apollo.start(); - apollo.applyMiddleware({ app: expapp }); http_server.listen({ port }, () => { console.log(`🚀 Server ready at http://localhost:${port}`); diff --git a/apps/web-api/src/trpc.ts b/apps/web-api/src/trpc.ts new file mode 100644 index 00000000..9bdcf527 --- /dev/null +++ b/apps/web-api/src/trpc.ts @@ -0,0 +1,62 @@ +import { z } from "zod"; +import { inferAsyncReturnType, initTRPC } from "@trpc/server"; +import * as trpcExpress from "@trpc/server/adapters/express"; +import jwt from "jsonwebtoken"; + +import { ENV } from "./utils"; + +import { userRouter } from "./resolver_user"; +import { repoRouter } from "./resolver_repo"; + +// created for each request +const createContext = ({ + req, + res, +}: trpcExpress.CreateExpressContextOptions) => { + const token = req?.headers?.authorization?.slice(7); + let userId; + + console.log("in context", token); + + if (token) { + const decoded = jwt.verify(token, ENV.JWT_SECRET) as { + id: string; + }; + userId = decoded.id; + } + return { + userId, + }; +}; // no context + +type Context = inferAsyncReturnType; +export const t = initTRPC.context().create(); +// export const t = initTRPC.create(); + +export const appRouter = t.router({ + hello: t.procedure + .input(z.string().nullish()) + .query(({ input, ctx }) => `hello ${input ?? ctx.userId ?? "world"}`), + getUser: t.procedure.input(z.string()).query((opts) => { + opts.input; // string + return { id: opts.input, name: "Bilbo" }; + }), + createUser: t.procedure + .input(z.object({ name: z.string().min(5) })) + .mutation(async (opts) => { + // use your ORM of choice + // return await UserModel.create({ + // data: opts.input, + // }); + }), + user: userRouter, + repo: repoRouter, +}); + +// export type definition of API +export type AppRouter = typeof appRouter; + +export const expressMiddleware = trpcExpress.createExpressMiddleware({ + router: appRouter, + createContext, +}); diff --git a/apps/web-api/src/utils.ts b/apps/web-api/src/utils.ts new file mode 100644 index 00000000..a313c1f5 --- /dev/null +++ b/apps/web-api/src/utils.ts @@ -0,0 +1,10 @@ +import { z } from "zod"; + +const envSchema = z.object({ + JWT_SECRET: z.string(), + // FIXME even if this is undefined, the token verification still works. Looks + // like I only need to set client ID in the frontend? + GOOGLE_CLIENT_ID: z.string().optional(), +}); + +export const ENV = envSchema.parse(process.env); diff --git a/bun.lockb b/bun.lockb index c8adb5c1..8a0c0429 100755 Binary files a/bun.lockb and b/bun.lockb differ