From 33362f83d08b820c9922f39ff7ee4ba837ddbc0c Mon Sep 17 00:00:00 2001 From: Nisarg Bhatt Date: Sat, 22 Nov 2025 22:42:40 +0530 Subject: [PATCH 1/5] Clerk auth added in backend and frontend --- apps/backend/.env.template | 4 +- apps/backend/package.json | 2 + apps/backend/src/db/schema.ts | 1 + apps/backend/src/index.ts | 7 +- apps/backend/src/lib/implementor.ts | 9 +- apps/backend/src/lib/procedures.ts | 44 ++--- apps/backend/src/modules/definition.ts | 77 ++++---- apps/frontend/.env.template | 3 +- apps/frontend/package.json | 1 + .../frontend/src/components/layout/Layout.tsx | 16 +- apps/frontend/src/lib/orpc.ts | 6 + apps/frontend/src/main.tsx | 27 ++- apps/frontend/src/routeTree.gen.ts | 168 ++++++++++++------ .../-components/RunNowAction.tsx | 2 +- .../$definitionId/-components/RuntimeCard.tsx | 0 .../$definitionId/-components/RuntimeList.tsx | 4 +- .../definitions/$definitionId/index.tsx | 5 +- .../-components/RuntimeStartAction.tsx | 0 .../-components/RuntimeTaskCard.tsx | 0 .../runtime/$runtimeId/index.tsx | 4 +- .../-components/DefinitionCard.tsx | 0 .../-components/DefinitionDeleteButton.tsx | 0 .../-components/DefinitionBasicDialog.tsx | 0 .../create/-components/DefinitionFetcher.tsx | 0 .../create/-components/DefinitionForm.tsx | 0 .../definitions/create/index.tsx | 2 +- .../definitions/index.tsx | 2 +- .../src/routes/_authenticated/route.tsx | 15 ++ apps/frontend/src/routes/sign-in/index.tsx | 20 +++ bun.lock | 33 +++- 30 files changed, 297 insertions(+), 155 deletions(-) rename apps/frontend/src/routes/{ => _authenticated}/definitions/$definitionId/-components/RunNowAction.tsx (98%) rename apps/frontend/src/routes/{ => _authenticated}/definitions/$definitionId/-components/RuntimeCard.tsx (100%) rename apps/frontend/src/routes/{ => _authenticated}/definitions/$definitionId/-components/RuntimeList.tsx (92%) rename apps/frontend/src/routes/{ => _authenticated}/definitions/$definitionId/index.tsx (95%) rename apps/frontend/src/routes/{ => _authenticated}/definitions/$definitionId/runtime/$runtimeId/-components/RuntimeStartAction.tsx (100%) rename apps/frontend/src/routes/{ => _authenticated}/definitions/$definitionId/runtime/$runtimeId/-components/RuntimeTaskCard.tsx (100%) rename apps/frontend/src/routes/{ => _authenticated}/definitions/$definitionId/runtime/$runtimeId/index.tsx (97%) rename apps/frontend/src/routes/{ => _authenticated}/definitions/-components/DefinitionCard.tsx (100%) rename apps/frontend/src/routes/{ => _authenticated}/definitions/-components/DefinitionDeleteButton.tsx (100%) rename apps/frontend/src/routes/{ => _authenticated}/definitions/create/-components/DefinitionBasicDialog.tsx (100%) rename apps/frontend/src/routes/{ => _authenticated}/definitions/create/-components/DefinitionFetcher.tsx (100%) rename apps/frontend/src/routes/{ => _authenticated}/definitions/create/-components/DefinitionForm.tsx (100%) rename apps/frontend/src/routes/{ => _authenticated}/definitions/create/index.tsx (94%) rename apps/frontend/src/routes/{ => _authenticated}/definitions/index.tsx (97%) create mode 100644 apps/frontend/src/routes/_authenticated/route.tsx create mode 100644 apps/frontend/src/routes/sign-in/index.tsx diff --git a/apps/backend/.env.template b/apps/backend/.env.template index 11d717c..54708eb 100644 --- a/apps/backend/.env.template +++ b/apps/backend/.env.template @@ -1,3 +1,5 @@ SERVER_KEY= DATABASE_URL= -PORT=3000 \ No newline at end of file +PORT=3000 +CLERK_PUBLISHABLE_KEY= +CLERK_SECRET_KEY= \ No newline at end of file diff --git a/apps/backend/package.json b/apps/backend/package.json index 250d4f5..3960c46 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -10,6 +10,8 @@ "check-types": "tsc --noEmit" }, "dependencies": { + "@clerk/backend": "^2.23.2", + "@hono/clerk-auth": "^3.0.3", "@orpc/contract": "catalog:", "@orpc/openapi": "catalog:", "@orpc/server": "catalog:", diff --git a/apps/backend/src/db/schema.ts b/apps/backend/src/db/schema.ts index 9931de0..4b6ea1f 100644 --- a/apps/backend/src/db/schema.ts +++ b/apps/backend/src/db/schema.ts @@ -23,6 +23,7 @@ export const definitionTable = pgTable("definitions", { .notNull(), uiObject: json(), tasks: json().$type>().notNull(), + userId: text(), createdAt: date().defaultNow().notNull(), }); diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 508b6b4..239f049 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -4,6 +4,7 @@ import { CORSPlugin } from "@orpc/server/plugins"; import { ZodToJsonSchemaConverter } from "@orpc/zod"; import { OpenAPIGenerator } from "@orpc/openapi"; import { OpenAPIHandler } from "@orpc/openapi/fetch"; +import { clerkMiddleware } from "@hono/clerk-auth"; const app = new Hono(); @@ -44,12 +45,12 @@ const html = ` `; +app.use("*", clerkMiddleware()); + app.use(async (c) => { const { matched, response } = await openAPIHandler.handle(c.req.raw, { prefix: "/rpc", - context: { - req: c.req, - }, // Provide initial context if needed + context: c, // Provide initial context if needed }); if (matched) { diff --git a/apps/backend/src/lib/implementor.ts b/apps/backend/src/lib/implementor.ts index 2ecb213..708d83d 100644 --- a/apps/backend/src/lib/implementor.ts +++ b/apps/backend/src/lib/implementor.ts @@ -1,10 +1,5 @@ import { implement } from "@orpc/server"; import { contractRouter } from "@repo/orpc"; -import type { HonoRequest } from "hono"; +import type { Context } from "hono"; -type HonoContext = { - req?: HonoRequest; - internal?: boolean; -}; - -export const contractOpenSpec = implement(contractRouter).$context(); +export const contractOpenSpec = implement(contractRouter).$context(); diff --git a/apps/backend/src/lib/procedures.ts b/apps/backend/src/lib/procedures.ts index 1ce6228..97d0853 100644 --- a/apps/backend/src/lib/procedures.ts +++ b/apps/backend/src/lib/procedures.ts @@ -1,20 +1,12 @@ +import { getAuth } from "@hono/clerk-auth"; import { oo } from "@orpc/openapi"; import { ORPCError, os } from "@orpc/server"; -import type { HonoRequest } from "hono"; - -type HonoContext = { - internal?: boolean; - req?: HonoRequest; -}; +import type { Context } from "hono"; const internalApiKey = process.env.SERVER_KEY; export const internalAuth = oo.spec( - os.$context().middleware(({ context, next }) => { - if (context?.internal === true) { - return next(); - } - + os.$context().middleware(({ context, next }) => { const apiKey = context?.req?.header("x-api-key"); if (!apiKey) { @@ -37,36 +29,30 @@ export const internalAuth = oo.spec( ); export const privateAuth = oo.spec( - os.$context().middleware(({ context, next }) => { - if (context?.internal === true) { - return next(); - } - - const authHeader = context?.req?.header("authorization")?.split(" ")?.at(1); + os.$context().middleware(({ context, next }) => { + const auth = getAuth(context); - if (!authHeader) { + if (!auth?.userId) { throw new ORPCError("UNAUTHORIZED", { - message: "No Auth Header Found", + message: "Unauthorized", }); } - if (authHeader !== "token") { - throw new ORPCError("UNAUTHORIZED", { - message: "Invalid Token", - }); - } - - return next(); + return next({ + context: { + auth: auth, + }, + }); }), { security: [{ bearerAuth: [] }], } ); -export const publicProcedures = oo.spec(os.$context(), { +export const publicProcedures = oo.spec(os.$context(), { security: [], }); -export const privateProcedures = os.$context().use(privateAuth); +export const privateProcedures = os.$context().use(privateAuth); -export const internalProcedures = os.$context().use(internalAuth); +export const internalProcedures = os.$context().use(internalAuth); diff --git a/apps/backend/src/modules/definition.ts b/apps/backend/src/modules/definition.ts index e8a27c6..a4c35bf 100644 --- a/apps/backend/src/modules/definition.ts +++ b/apps/backend/src/modules/definition.ts @@ -4,9 +4,11 @@ import { contractOpenSpec } from "@lib/implementor"; import { safeAsync } from "@repo/utils"; import { and, asc, count, desc, eq } from "drizzle-orm"; import { definitionTaskList } from "@repo/engine/types"; +import { privateAuth } from "@lib/procedures"; -export const createDefinition = contractOpenSpec.definition.create.handler( - async ({ input, errors }) => { +export const createDefinition = contractOpenSpec.definition.create + .use(privateAuth) + .handler(async ({ input, errors }) => { const parsedTask = definitionTaskList.safeParse(input.tasks); if (!parsedTask.success) { @@ -53,8 +55,7 @@ export const createDefinition = contractOpenSpec.definition.create.handler( id: createdDefinitionId, }, }; - } -); + }); export const editDefinition = contractOpenSpec.definition.edit.handler( async ({ input, errors }) => { @@ -132,40 +133,44 @@ export const editDefinition = contractOpenSpec.definition.edit.handler( } ); -export const listDefinition = contractOpenSpec.definition.list.handler(async ({ input }) => { - const list = await db.query.definition.findMany({ - offset: (input.page - 1) * input.limit, - limit: input.limit, - where: eq(definitionTable.type, "definition"), - orderBy: [asc(definitionTable.status), desc(definitionTable.createdAt)], - columns: { - id: true, - name: true, - description: true, - status: true, - createdAt: true, - }, - }); +export const listDefinition = contractOpenSpec.definition.list + .use(privateAuth) + .handler(async ({ input, context }) => { + const userId = context.auth.userId; + + const list = await db.query.definition.findMany({ + offset: (input.page - 1) * input.limit, + limit: input.limit, + where: and(eq(definitionTable.type, "definition"), eq(definitionTable.userId, userId)), + orderBy: [asc(definitionTable.status), desc(definitionTable.createdAt)], + columns: { + id: true, + name: true, + description: true, + status: true, + createdAt: true, + }, + }); - const totalCount = await db - .select({ - count: count(), - }) - .from(definitionTable) - .where(and(eq(definitionTable.status, "active"), eq(definitionTable.type, "definition"))); - - return { - message: "Definition listed successfully", - data: { - list, - pagination: { - total: totalCount?.at(0)?.count ?? 0, - page: input.page, - size: input.limit, + const totalCount = await db + .select({ + count: count(), + }) + .from(definitionTable) + .where(and(eq(definitionTable.status, "active"), eq(definitionTable.type, "definition"))); + + return { + message: "Definition listed successfully", + data: { + list, + pagination: { + total: totalCount?.at(0)?.count ?? 0, + page: input.page, + size: input.limit, + }, }, - }, - }; -}); + }; + }); export const fetchEditDefinition = contractOpenSpec.definition.fetchEdit.handler( async ({ input, errors }) => { diff --git a/apps/frontend/.env.template b/apps/frontend/.env.template index 43275c7..0a2a748 100644 --- a/apps/frontend/.env.template +++ b/apps/frontend/.env.template @@ -1 +1,2 @@ -VITE_BACKEND_URL=http://localhost:3000 \ No newline at end of file +VITE_BACKEND_URL=http://localhost:3000 +VITE_CLERK_PUBLISHABLE_KEY= \ No newline at end of file diff --git a/apps/frontend/package.json b/apps/frontend/package.json index ecac7c0..29771af 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -10,6 +10,7 @@ "check-types": "tsc --noEmit" }, "dependencies": { + "@clerk/clerk-react": "^5.56.2", "@hookform/resolvers": "^5.2.2", "@monaco-editor/react": "^4.7.0", "@orpc/client": "catalog:", diff --git a/apps/frontend/src/components/layout/Layout.tsx b/apps/frontend/src/components/layout/Layout.tsx index 4883ff1..b328f28 100644 --- a/apps/frontend/src/components/layout/Layout.tsx +++ b/apps/frontend/src/components/layout/Layout.tsx @@ -5,6 +5,7 @@ import ContextFactory from "@/contexts/ContextFactory"; import { Button } from "../ui/button"; import type { FC, ReactNode } from "react"; import { Link } from "@tanstack/react-router"; +import { SignedIn, SignedOut, UserButton } from "@clerk/clerk-react"; interface Props { children: ReactNode; @@ -18,7 +19,7 @@ const Layout: FC = ({ children }) => ( -