diff --git a/apps/app/server/trpc/context.ts b/apps/app/server/trpc/context.ts index d2831f30..85d9e54e 100644 --- a/apps/app/server/trpc/context.ts +++ b/apps/app/server/trpc/context.ts @@ -2,7 +2,7 @@ import { TRPCError, type inferAsyncReturnType } from '@trpc/server'; import { type CreateNextContextOptions } from '@trpc/server/adapters/next'; import { prisma } from '@server/db/client'; import { IncomingHttpHeaders } from 'http'; -import { createRemoteJWKSet, jwtVerify } from 'jose'; +import { JWTPayload, createRemoteJWKSet, jwtVerify } from 'jose'; import { retrieveRawUserInfoServer } from 'identity'; import axios from 'axios'; import { V2Session } from 'identity'; @@ -31,6 +31,10 @@ type CreateContextOptions = { headers?: IncomingHttpHeaders; }; +interface Payload extends JWTPayload { + scope: string; +}; + /** Use this helper for: * - testing, so we dont have to mock Next.js' req/res * - trpc's `createSSGHelpers` where we don't have req/res @@ -48,6 +52,7 @@ export const createContextInner = async (opts: CreateContextOptions) => { * This is the actual context you'll use in your router * @link https://trpc.io/docs/context **/ + export const createContext = async (opts: CreateNextContextOptions) => { const { req, res } = opts; @@ -59,7 +64,7 @@ export const createContext = async (opts: CreateNextContextOptions) => { headers, }); } - const { payload } = await jwtVerify( + const { payload } = await jwtVerify( token, createRemoteJWKSet(new URL(process.env.NEXT_PUBLIC_JWKS_ENDPOINT)), { @@ -67,6 +72,7 @@ export const createContext = async (opts: CreateNextContextOptions) => { audience: process.env.NEXT_PUBLIC_RESOURCE_AUDIENCE, }, ); + console.log("payload", payload) // Create the server session object from varius data endpoints. // grabs the logto user data. const user_data = await retrieveRawUserInfoServer(req.cookies); @@ -98,6 +104,7 @@ export const createContext = async (opts: CreateNextContextOptions) => { blacklisted: waldo_user_data ? waldo_user_data.user.blacklisted : false, + scope: payload.scope.split(" ") }; return await createContextInner({ diff --git a/apps/app/server/trpc/router/site.ts b/apps/app/server/trpc/router/site.ts index 9011cf0a..5e60b807 100644 --- a/apps/app/server/trpc/router/site.ts +++ b/apps/app/server/trpc/router/site.ts @@ -6,44 +6,6 @@ import { router, protectedProcedure, publicProcedure } from '../trpc'; import * as Sentry from '@sentry/nextjs'; export const siteRouter = router({ - getPageData: protectedProcedure - .meta({ openapi: { method: 'GET', path: '/site/page' } }) - .input( - z - .object({ - name: z.string(), - }) - .transform(input => { - return { - name: serverSanitize(input.name), - }; - }), - ) - .output( - z.object({ - maintenance: z.boolean(), - name: z.string(), - parentName: z.string(), - alertDescription: z.string().nullable(), - alertTitle: z.string().nullable(), - isCustomAlert: z.boolean(), - }), - ) - .query(async ({ input, ctx }) => { - const pageData = await ctx.prisma.waldoPage.findFirst({ - where: { - name: input.name, - }, - }); - if (pageData == null) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Waldo Vision Page not found in the database.', - }); - } - // no error checking because the docs will never be deleted. - return pageData; - }), getSiteData: publicProcedure .meta({ openapi: { method: 'GET', path: '/site/site' } }) .input( diff --git a/apps/app/server/trpc/router/site/getPageData.ts b/apps/app/server/trpc/router/site/getPageData.ts new file mode 100644 index 00000000..02129919 --- /dev/null +++ b/apps/app/server/trpc/router/site/getPageData.ts @@ -0,0 +1,43 @@ +import { rbacProtectedProcedure } from '../../trpc'; +import { z } from 'zod'; +import { serverSanitize } from '@utils/sanitize'; +import { TRPCError } from '@trpc/server'; + +export default rbacProtectedProcedure(["read:all", "read:pagemetadata"]) + .meta({ openapi: { method: 'GET', path: '/site/page' } }) + .input( + z + .object({ + name: z.string(), + }) + .transform(input => { + return { + name: serverSanitize(input.name), + }; + }), + ) + .output( + z.object({ + maintenance: z.boolean(), + name: z.string(), + parentName: z.string(), + alertDescription: z.string().nullable(), + alertTitle: z.string().nullable(), + isCustomAlert: z.boolean(), + }), + ) + .query(async ({ input, ctx }) => { + const pageData = await ctx.prisma.waldoPage.findFirst({ + where: { + name: input.name, + }, + }); + if (pageData == null) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'Waldo Vision Page not found in the database.', + }); + } + // no error checking because the docs will never be deleted. + return pageData; + }); \ No newline at end of file diff --git a/apps/app/server/trpc/router/site/index.ts b/apps/app/server/trpc/router/site/index.ts new file mode 100644 index 00000000..1027da9c --- /dev/null +++ b/apps/app/server/trpc/router/site/index.ts @@ -0,0 +1,7 @@ +import { router, protectedProcedure, publicProcedure } from '../../trpc'; + +import getPageData from './getPageData'; + +export const siteRouter = router({ + getPageData +}); \ No newline at end of file diff --git a/apps/app/server/trpc/trpc.ts b/apps/app/server/trpc/trpc.ts index 00e1e21b..d7e1abbc 100644 --- a/apps/app/server/trpc/trpc.ts +++ b/apps/app/server/trpc/trpc.ts @@ -35,7 +35,7 @@ export const publicProcedure = t.procedure.use(sentryMiddleware); * users are logged in * rate limit middleware */ -const isAuthed = t.middleware(async ({ ctx, next }) => { +const isAuthed2 = t.middleware(async ({ ctx, next }) => { if (!ctx.session || !ctx.session.logto_id) throw new TRPCError({ code: 'UNAUTHORIZED' }); @@ -51,6 +51,27 @@ const isAuthed = t.middleware(async ({ ctx, next }) => { }); }); +const isAuthed = (requiredScope: Array) => { + return t.middleware(async ({ctx, next}) => { + if (!ctx.session || !ctx.session.logto_id) + throw new TRPCError({ code: 'UNAUTHORIZED' }); + + const userScope = ctx.session.scope; + const userHasRequiredScope = requiredScope.some(a => userScope.includes(a)); + if(!userHasRequiredScope) + throw new TRPCError({ code: 'UNAUTHORIZED' }); + + // ?set the user on the sentry scope + // ?so we can track effected users + Sentry.getCurrentHub().getScope().setUser({ id: ctx.session.logto_id }); + return next({ + ctx: { + session: {...ctx.session, user: ctx.session} + } + }) + }); +} + /** * Protected procedure **/ @@ -114,5 +135,12 @@ const isApiAuthed = t.middleware(async ({ ctx, next }) => { export const apiProcedure = t.procedure.use(sentryMiddleware).use(isApiAuthed); export const protectedProcedure = t.procedure - .use(sentryMiddleware) - .use(isAuthed); + .use(sentryMiddleware) + .use(isAuthed2); + + +export const rbacProtectedProcedure = (scope: Array) => { + return t.procedure + .use(sentryMiddleware) + .use(isAuthed(scope)); +}