Skip to content

Commit

Permalink
Merge pull request #10436 from hicommonwealth/rotorsoft/refactor-outp…
Browse files Browse the repository at this point in the history
…ut-middleware

Refactors output middleware
  • Loading branch information
Rotorsoft authored Jan 6, 2025
2 parents 18f184c + b735202 commit 1d2648a
Show file tree
Hide file tree
Showing 16 changed files with 450 additions and 400 deletions.
130 changes: 130 additions & 0 deletions libs/adapters/src/trpc/builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import {
logger,
stats,
type AuthStrategies,
type User,
} from '@hicommonwealth/core';
import { TRPCError, initTRPC } from '@trpc/server';
import type { Request } from 'express';
import passport from 'passport';
import type { OpenApiMeta } from 'trpc-swagger';
import { ZodSchema, z } from 'zod';
import { config } from '../config';
import type { BuildProcOptions, Context, Metadata } from './types';

const log = logger(import.meta);

const trpc = initTRPC.meta<OpenApiMeta>().context<Context>().create();
export const router = trpc.router;
export const procedure = trpc.procedure;

const isSecure = <Input extends ZodSchema, Output extends ZodSchema>(
md: Metadata<Input, Output>,
) => md.secure !== false || (md.auth ?? []).length > 0;

const authenticate = async <Input extends ZodSchema>(
req: Request,
rawInput: z.infer<Input>,
authStrategy: AuthStrategies<Input> = { type: 'jwt' },
) => {
// Bypass when user is already authenticated via JWT or token
// Authentication overridden at router level e.g. external-router.ts
if (req.user && authStrategy.type !== 'custom') return;

try {
if (authStrategy.type === 'authtoken') {
switch (req.headers['authorization']) {
case config.NOTIFICATIONS.KNOCK_AUTH_TOKEN:
req.user = {
id: authStrategy.userId,
email: '[email protected]',
};
break;
case config.LOAD_TESTING.AUTH_TOKEN:
req.user = {
id: authStrategy.userId,
email: '[email protected]',
};
break;
default:
throw new Error('Not authenticated');
}
} else if (authStrategy.type === 'custom') {
req.user = await authStrategy.userResolver(rawInput, req.user as User);
} else {
await passport.authenticate(authStrategy.type, { session: false });
}
if (!req.user) throw new Error('Not authenticated');
} catch (error) {
throw new TRPCError({
message: error instanceof Error ? error.message : (error as string),
code: 'UNAUTHORIZED',
});
}
};

/**
* tRPC procedure factory with authentication, traffic stats, and analytics middleware
*/
export const buildproc = <Input extends ZodSchema, Output extends ZodSchema>({
method,
name,
md,
tag,
outMiddlewares,
forceSecure,
}: BuildProcOptions<Input, Output>) => {
const secure = forceSecure ?? isSecure(md);
return trpc.procedure
.use(async ({ ctx, rawInput, next }) => {
if (secure) await authenticate(ctx.req, rawInput, md.authStrategy);
return next({
ctx: {
...ctx,
actor: {
user: ctx.req.user as User,
address: ctx.req.headers['address'] as string,
},
},
});
})
.use(async ({ ctx, rawInput, next }) => {
const start = Date.now();
const result = await next();
const latency = Date.now() - start;
try {
const path = `${ctx.req.method.toUpperCase()} ${ctx.req.path}`;
stats().increment('cw.path.called', { path });
stats().histogram(`cw.path.latency`, latency, {
path,
statusCode: ctx.res.statusCode.toString(),
});
} catch (err) {
err instanceof Error && log.error(err.message, err);
}
if (result.ok && outMiddlewares?.length) {
for (const omw of outMiddlewares) {
await omw(rawInput, result.data, ctx);
}
}
return result;
})
.meta({
openapi: {
method,
path: `/${name}`,
tags: [tag],
headers: [
{
in: 'header',
name: 'address',
required: false,
schema: { type: 'string' },
},
],
protect: secure,
},
})
.input(md.input)
.output(md.output);
};
22 changes: 8 additions & 14 deletions libs/adapters/src/trpc/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
import { Events } from '@hicommonwealth/schemas';
import { TRPCError } from '@trpc/server';
import { ZodSchema, ZodUndefined, z } from 'zod';
import { Commit, Tag, Track, buildproc, procedure } from './middleware';
import { buildproc, procedure } from './builder';
import { Tag, type OutputMiddleware } from './types';

const log = logger(import.meta);

Expand Down Expand Up @@ -48,11 +49,7 @@ const trpcerror = (error: unknown): TRPCError => {
* Builds tRPC command POST endpoint
* @param factory command factory
* @param tag command tag used for OpenAPI spec grouping
* @param track analytics tracking middleware as:
* - tuple of `[event, output mapper]`
* - or `(input,output) => Promise<[event, data]|undefined>`
* @param commit output middleware (best effort), mainly used to commit actions to canvas
* - `(input,output,ctx) => Promise<Record<string,unknown>> | undefined | void`
* @param outMiddlewares output middlewares (best effort), mainly used to commit actions to canvas
* @returns tRPC mutation procedure
*/
export const command = <
Expand All @@ -62,17 +59,15 @@ export const command = <
>(
factory: () => Metadata<Input, Output, Context>,
tag: Tag,
track?: Track<Input, Output>,
commit?: Commit<Input, Output>,
outMiddlewares?: Array<OutputMiddleware<Input, Output>>,
) => {
const md = factory();
return buildproc({
method: 'POST',
name: factory.name,
md,
tag,
track,
commit,
outMiddlewares,
}).mutation(async ({ ctx, input }) => {
try {
return await coreCommand(
Expand All @@ -94,8 +89,7 @@ export const command = <
* @param factory query factory
* @param tag query tag used for OpenAPI spec grouping
* @param options An object with security and caching related configuration
* @param commit output middleware (best effort), mainly used to update statistics
* - `(input,output,ctx) => Promise<Record<string,unknown>> | undefined | void`
* @param outMiddlewares output middlewares (best effort), mainly used to update statistics
* @returns tRPC query procedure
*/
export const query = <
Expand All @@ -109,15 +103,15 @@ export const query = <
forceSecure?: boolean;
ttlSecs?: number;
},
commit?: Commit<Input, Output>,
outMiddlewares?: Array<OutputMiddleware<Input, Output>>,
) => {
const md = factory();
return buildproc({
method: 'GET',
name: factory.name,
md,
tag,
commit,
outMiddlewares,
forceSecure: options?.forceSecure,
}).query(async ({ ctx, input }) => {
try {
Expand Down
2 changes: 2 additions & 0 deletions libs/adapters/src/trpc/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './builder';
export * from './handlers';
export * from './middleware';
export * from './types';
export * from './utils';
Loading

0 comments on commit 1d2648a

Please sign in to comment.