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
|
import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";
import { ZodError } from "zod";
import type { db } from "@karakeep/db";
import serverConfig from "@karakeep/shared/config";
import { createRateLimitMiddleware } from "./rateLimit";
import {
apiErrorsTotalCounter,
apiRequestDurationSummary,
apiRequestsTotalCounter,
} from "./stats";
interface User {
id: string;
name?: string | null | undefined;
email?: string | null | undefined;
role: "admin" | "user" | null;
}
export interface Context {
user: User | null;
db: typeof db;
req: {
ip: string | null;
};
}
export interface AuthedContext {
user: User;
db: typeof db;
req: {
ip: string | null;
};
}
// Avoid exporting the entire t-object
// since it's not very descriptive.
// For instance, the use of a t variable
// is common in i18n libraries.
const t = initTRPC.context<Context>().create({
transformer: superjson,
errorFormatter(opts) {
const { shape, error } = opts;
apiErrorsTotalCounter.inc({
type: opts.type,
path: opts.path,
code: error.code,
});
return {
...shape,
data: {
...shape.data,
zodError:
error.code === "BAD_REQUEST" && error.cause instanceof ZodError
? error.cause.flatten()
: null,
},
};
},
});
export const createCallerFactory = t.createCallerFactory;
// Base router and procedure helpers
export const router = t.router;
export const procedure = t.procedure
.use(function isDemoMode(opts) {
if (serverConfig.demoMode && opts.type == "mutation") {
throw new TRPCError({
message: "Mutations are not allowed in demo mode",
code: "FORBIDDEN",
});
}
return opts.next();
})
.use(async (opts) => {
const end = apiRequestDurationSummary.startTimer({
path: opts.path,
type: opts.type,
});
const res = await opts.next();
apiRequestsTotalCounter.inc({
type: opts.type,
path: opts.path,
is_error: res.ok ? 0 : 1,
});
end();
return res;
});
// Default public procedure rate limiting
export const publicProcedure = procedure.use(
createRateLimitMiddleware({
name: "globalPublic",
windowMs: 60 * 1000,
maxRequests: 1000,
}),
);
export const authedProcedure = procedure
// Default authed procedure rate limiting
.use(
createRateLimitMiddleware({
name: "globalAuthed",
windowMs: 60 * 1000,
maxRequests: 3000,
}),
)
.use(function isAuthed(opts) {
const user = opts.ctx.user;
if (!user?.id) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return opts.next({
ctx: {
user,
},
});
});
export const adminProcedure = authedProcedure.use(function isAdmin(opts) {
const user = opts.ctx.user;
if (user.role != "admin") {
throw new TRPCError({ code: "FORBIDDEN" });
}
return opts.next(opts);
});
// Export the rate limiting utilities for use in routers
export { createRateLimitMiddleware };
|