aboutsummaryrefslogtreecommitdiffstats
path: root/packages/trpc/index.ts
blob: 90f37ae47a72ab29b5682cb9298852e2b430bd15 (plain) (blame)
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
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 {
  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;
  });
export const publicProcedure = procedure;

export const authedProcedure = procedure.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);
});