-
Notifications
You must be signed in to change notification settings - Fork 143
/
Copy pathtrpc.ts
248 lines (220 loc) Β· 6.54 KB
/
trpc.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
/**
* YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
* 1. You want to modify request context (see Part 1).
* 2. You want to create a new middleware or type of procedure (see Part 3).
*
* TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
* need to use are documented accordingly near the end.
*/
import { Prisma } from '@prisma/client';
import { TRPCError, initTRPC } from '@trpc/server';
import type { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch';
import { getHTTPStatusCodeFromError } from '@trpc/server/http';
import { cookies } from 'next/headers';
import { randomUUID } from 'node:crypto';
import superjson from 'superjson';
import { OpenApiMeta } from 'trpc-openapi';
import { ZodError } from 'zod';
import { env } from '@/env.mjs';
import { UserAuthorization } from '@/features/users/schemas';
import { db } from '@/server/config/db';
import { logger } from '@/server/config/logger';
import { AUTH_COOKIE_NAME, getCurrentSession } from '@/server/config/session';
/**
* 1. CONTEXT
*
* This section defines the "contexts" that are available in the backend API.
*
* These allow you to access things when processing a request, like the database, the session, etc.
*/
export type AppContext = Awaited<ReturnType<typeof createTRPCContext>>;
/**
* This is the actual context you will use in your router. It will be used to process every request
* that goes through your tRPC endpoint.
*
* @see https://trpc.io/docs/context
*/
export const createTRPCContext = async ({
req,
resHeaders,
}: FetchCreateContextFnOptions) => {
const { session, user } = await getCurrentSession();
const apiType: 'REST' | 'TRPC' = new URL(req.url).pathname.startsWith(
'/api/rest'
)
? 'REST'
: 'TRPC';
return {
user,
session,
apiType,
logger,
db,
req,
resHeaders,
};
};
/**
* 2. INITIALIZATION
*
* This is where the tRPC API is initialized, connecting the context and transformer. We also parse
* ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
* errors on the backend.
*/
const t = initTRPC
.meta<OpenApiMeta>()
.context<typeof createTRPCContext>()
.create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
prismaError:
error.cause instanceof Prisma.PrismaClientKnownRequestError
? error.cause.meta
: null,
},
};
},
});
/**
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
*
* These are the pieces you use to build your tRPC API. You should import these a lot in the
* "/src/server/api/routers" directory.
*/
/**
* This is how you create new routers and sub-routers in your tRPC API.
*
* @see https://trpc.io/docs/router
*/
export const createTRPCRouter = t.router;
const loggerMiddleware = t.middleware(async (opts) => {
const start = Date.now();
// Those are the default informations we want to print for all the requests
const meta = {
path: opts.path,
type: opts.type,
requestId: randomUUID(),
userId: opts.ctx.user?.id,
apiType: opts.ctx.apiType,
};
logger.debug(
{ ...meta, input: opts.rawInput },
`${opts.rawInput ? 'π¨ With' : 'π₯ No'} input`
);
// We are doing the next operation in tRPC
const result = await opts.next({
ctx: {
logger: logger.child({ ...meta, scope: 'procedure' }),
},
});
// This should be after the operation so we can really compute the duration
const durationMs = Date.now() - start;
const extendedMeta = {
...meta,
durationMs,
};
if (result.ok) {
logger.info(extendedMeta, 'β
OK');
} else {
const logLevel = () => {
const errorCode = getHTTPStatusCodeFromError(result.error);
if (errorCode >= 500) return 'error';
if (errorCode >= 400) return 'warn';
if (errorCode >= 300) return 'info';
return 'error';
};
logger[logLevel()](
{
...extendedMeta,
errorCode: result.error.code,
errorMessage: result.error.message,
},
`β KO`
);
}
// Finally, return the result so tRPC can continue
return result;
});
/** Demo Middleware */
const enforceDemo = t.middleware(({ ctx, next, type, path }) => {
if (!env.NEXT_PUBLIC_IS_DEMO) {
return next({
ctx,
});
}
if (
type !== 'mutation' ||
path === 'auth.login' ||
path === 'auth.loginValidate' ||
path === 'auth.logout'
) {
return next({
ctx,
});
}
throw new TRPCError({
code: 'FORBIDDEN',
message: '[DEMO] You cannot run mutation in Demo mode',
});
});
/**
* Public (unauthenticated) procedure
*
* This is the base piece you use to build new queries and mutations on your tRPC API. It does not
* guarantee that a user querying is authorized, but you can still access user session data if they
* are logged in.
*/
export const publicProcedure = () =>
t.procedure.use(enforceDemo).use(loggerMiddleware);
/**
* Protected (authenticated) procedure
*
* If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
* the session is valid and guarantees `ctx.session.user` is not null.
*
* @see https://trpc.io/docs/procedures
*/
export const protectedProcedure = (
options: {
authorizations?: UserAuthorization[];
} = {}
) =>
publicProcedure().use(
t.middleware(({ ctx, next }) => {
const { user, session, req, resHeaders } = ctx;
if (!user || !session || user.accountStatus !== 'ENABLED') {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: user?.email ?? undefined,
});
}
// Check if the user has at least one of the authorizations
if (
options.authorizations &&
!options.authorizations.some((a) => user.authorizations.includes(a))
) {
throw new TRPCError({ code: 'FORBIDDEN' });
}
// Refresh the session cookie on GET requests by extending its expiration
const token = cookies().get(AUTH_COOKIE_NAME)?.value;
if (token && req.method === 'GET') {
resHeaders.append(
'Set-Cookie',
`${AUTH_COOKIE_NAME}=${token}; Path=/; Expires=${session.expiresAt.toISOString()}; SameSite=Lax; HttpOnly; Secure=${env.NODE_ENV === 'production'}`
);
}
return next({
ctx: {
// infers the `user` and `session `as non-null
session,
user,
},
});
})
);