@terreno/api 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +170 -0
- package/biome.jsonc +22 -0
- package/bunfig.toml +4 -0
- package/dist/api.d.ts +227 -0
- package/dist/api.js +1024 -0
- package/dist/api.test.d.ts +1 -0
- package/dist/api.test.js +2143 -0
- package/dist/auth.d.ts +50 -0
- package/dist/auth.js +512 -0
- package/dist/auth.test.d.ts +1 -0
- package/dist/auth.test.js +778 -0
- package/dist/errors.d.ts +75 -0
- package/dist/errors.js +216 -0
- package/dist/example.d.ts +1 -0
- package/dist/example.js +118 -0
- package/dist/expressServer.d.ts +35 -0
- package/dist/expressServer.js +436 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +30 -0
- package/dist/logger.d.ts +23 -0
- package/dist/logger.js +249 -0
- package/dist/middleware.d.ts +10 -0
- package/dist/middleware.js +52 -0
- package/dist/notifiers/googleChatNotifier.d.ts +5 -0
- package/dist/notifiers/googleChatNotifier.js +130 -0
- package/dist/notifiers/googleChatNotifier.test.d.ts +1 -0
- package/dist/notifiers/googleChatNotifier.test.js +260 -0
- package/dist/notifiers/index.d.ts +3 -0
- package/dist/notifiers/index.js +19 -0
- package/dist/notifiers/slackNotifier.d.ts +5 -0
- package/dist/notifiers/slackNotifier.js +130 -0
- package/dist/notifiers/slackNotifier.test.d.ts +1 -0
- package/dist/notifiers/slackNotifier.test.js +259 -0
- package/dist/notifiers/zoomNotifier.d.ts +34 -0
- package/dist/notifiers/zoomNotifier.js +181 -0
- package/dist/notifiers/zoomNotifier.test.d.ts +1 -0
- package/dist/notifiers/zoomNotifier.test.js +370 -0
- package/dist/openApi.d.ts +60 -0
- package/dist/openApi.js +441 -0
- package/dist/openApi.test.d.ts +1 -0
- package/dist/openApi.test.js +445 -0
- package/dist/openApiBuilder.d.ts +419 -0
- package/dist/openApiBuilder.js +424 -0
- package/dist/openApiBuilder.test.d.ts +1 -0
- package/dist/openApiBuilder.test.js +509 -0
- package/dist/openApiEtag.d.ts +7 -0
- package/dist/openApiEtag.js +38 -0
- package/dist/permissions.d.ts +26 -0
- package/dist/permissions.js +331 -0
- package/dist/permissions.test.d.ts +1 -0
- package/dist/permissions.test.js +413 -0
- package/dist/plugins.d.ts +67 -0
- package/dist/plugins.js +315 -0
- package/dist/plugins.test.d.ts +1 -0
- package/dist/plugins.test.js +639 -0
- package/dist/populate.d.ts +14 -0
- package/dist/populate.js +315 -0
- package/dist/populate.test.d.ts +1 -0
- package/dist/populate.test.js +133 -0
- package/dist/response.d.ts +0 -0
- package/dist/response.js +1 -0
- package/dist/tests/bunSetup.d.ts +1 -0
- package/dist/tests/bunSetup.js +297 -0
- package/dist/tests/index.d.ts +1 -0
- package/dist/tests/index.js +17 -0
- package/dist/tests.d.ts +99 -0
- package/dist/tests.js +273 -0
- package/dist/transformers.d.ts +25 -0
- package/dist/transformers.js +217 -0
- package/dist/transformers.test.d.ts +1 -0
- package/dist/transformers.test.js +370 -0
- package/dist/utils.d.ts +11 -0
- package/dist/utils.js +143 -0
- package/dist/utils.test.d.ts +1 -0
- package/dist/utils.test.js +14 -0
- package/index.ts +1 -0
- package/package.json +88 -0
- package/src/__snapshots__/openApi.test.ts.snap +4814 -0
- package/src/__snapshots__/openApiBuilder.test.ts.snap +1485 -0
- package/src/api.test.ts +1661 -0
- package/src/api.ts +1036 -0
- package/src/auth.test.ts +550 -0
- package/src/auth.ts +408 -0
- package/src/errors.ts +225 -0
- package/src/example.ts +99 -0
- package/src/express.d.ts +5 -0
- package/src/expressServer.ts +387 -0
- package/src/index.ts +14 -0
- package/src/logger.ts +190 -0
- package/src/middleware.ts +18 -0
- package/src/notifiers/googleChatNotifier.test.ts +114 -0
- package/src/notifiers/googleChatNotifier.ts +47 -0
- package/src/notifiers/index.ts +3 -0
- package/src/notifiers/slackNotifier.test.ts +113 -0
- package/src/notifiers/slackNotifier.ts +55 -0
- package/src/notifiers/zoomNotifier.test.ts +207 -0
- package/src/notifiers/zoomNotifier.ts +111 -0
- package/src/openApi.test.ts +331 -0
- package/src/openApi.ts +494 -0
- package/src/openApiBuilder.test.ts +442 -0
- package/src/openApiBuilder.ts +636 -0
- package/src/openApiEtag.ts +40 -0
- package/src/permissions.test.ts +219 -0
- package/src/permissions.ts +228 -0
- package/src/plugins.test.ts +390 -0
- package/src/plugins.ts +289 -0
- package/src/populate.test.ts +65 -0
- package/src/populate.ts +258 -0
- package/src/response.ts +0 -0
- package/src/tests/bunSetup.ts +234 -0
- package/src/tests/index.ts +1 -0
- package/src/tests.ts +218 -0
- package/src/transformers.test.ts +202 -0
- package/src/transformers.ts +170 -0
- package/src/utils.test.ts +14 -0
- package/src/utils.ts +47 -0
- package/tsconfig.json +60 -0
- package/types.d.ts +17 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import {beforeEach, describe, expect, it} from "bun:test";
|
|
2
|
+
import type express from "express";
|
|
3
|
+
import supertest from "supertest";
|
|
4
|
+
import type TestAgent from "supertest/lib/agent";
|
|
5
|
+
|
|
6
|
+
import {modelRouter} from "./api";
|
|
7
|
+
import {addAuthRoutes, setupAuth} from "./auth";
|
|
8
|
+
import {Permissions} from "./permissions";
|
|
9
|
+
import {
|
|
10
|
+
authAsUser,
|
|
11
|
+
type Food,
|
|
12
|
+
FoodModel,
|
|
13
|
+
getBaseServer,
|
|
14
|
+
RequiredModel,
|
|
15
|
+
setupDb,
|
|
16
|
+
UserModel,
|
|
17
|
+
} from "./tests";
|
|
18
|
+
|
|
19
|
+
describe("permissions", () => {
|
|
20
|
+
let server: TestAgent;
|
|
21
|
+
let app: express.Application;
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
process.env.REFRESH_TOKEN_SECRET = "testsecret1234";
|
|
25
|
+
|
|
26
|
+
const [admin, notAdmin] = await setupDb();
|
|
27
|
+
|
|
28
|
+
await Promise.all([
|
|
29
|
+
FoodModel.create({
|
|
30
|
+
calories: 1,
|
|
31
|
+
created: new Date(),
|
|
32
|
+
name: "Spinach",
|
|
33
|
+
ownerId: notAdmin._id,
|
|
34
|
+
}),
|
|
35
|
+
FoodModel.create({
|
|
36
|
+
calories: 100,
|
|
37
|
+
created: Date.now() - 10,
|
|
38
|
+
name: "Apple",
|
|
39
|
+
ownerId: admin._id,
|
|
40
|
+
}),
|
|
41
|
+
]);
|
|
42
|
+
app = getBaseServer();
|
|
43
|
+
setupAuth(app, UserModel as any);
|
|
44
|
+
addAuthRoutes(app, UserModel as any);
|
|
45
|
+
app.use(
|
|
46
|
+
"/food",
|
|
47
|
+
modelRouter(FoodModel, {
|
|
48
|
+
allowAnonymous: true,
|
|
49
|
+
permissions: {
|
|
50
|
+
create: [Permissions.IsAuthenticated],
|
|
51
|
+
delete: [Permissions.IsAdmin],
|
|
52
|
+
list: [Permissions.IsAny],
|
|
53
|
+
read: [Permissions.IsAny],
|
|
54
|
+
update: [Permissions.IsOwner],
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
app.use(
|
|
59
|
+
"/required",
|
|
60
|
+
modelRouter(RequiredModel, {
|
|
61
|
+
permissions: {
|
|
62
|
+
create: [Permissions.IsAuthenticated],
|
|
63
|
+
delete: [Permissions.IsAdmin],
|
|
64
|
+
list: [Permissions.IsAny],
|
|
65
|
+
read: [Permissions.IsAny],
|
|
66
|
+
update: [Permissions.IsOwner],
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
server = supertest(app);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("anonymous food", () => {
|
|
74
|
+
it("list", async () => {
|
|
75
|
+
const res = await server.get("/food").expect(200);
|
|
76
|
+
expect(res.body.data).toHaveLength(2);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("get", async () => {
|
|
80
|
+
const res = await server.get("/food").expect(200);
|
|
81
|
+
expect(res.body.data).toHaveLength(2);
|
|
82
|
+
const res2 = await server.get(`/food/${res.body.data[0]._id}`).expect(200);
|
|
83
|
+
expect(res.body.data[0]._id).toBe(res2.body.data._id);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("post", async () => {
|
|
87
|
+
const res = await server.post("/food").send({
|
|
88
|
+
calories: 15,
|
|
89
|
+
name: "Broccoli",
|
|
90
|
+
});
|
|
91
|
+
expect(res.status).toBe(405);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("patch", async () => {
|
|
95
|
+
const res = await server.get("/food");
|
|
96
|
+
const res2 = await server.patch(`/food/${res.body.data[0]._id}`).send({
|
|
97
|
+
name: "Broccoli",
|
|
98
|
+
});
|
|
99
|
+
expect(res2.status).toBe(403);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("delete", async () => {
|
|
103
|
+
const res = await server.get("/food");
|
|
104
|
+
const res2 = await server.delete(`/food/${res.body.data[0]._id}`);
|
|
105
|
+
expect(res2.status).toBe(405);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("non admin food", () => {
|
|
110
|
+
let agent: TestAgent;
|
|
111
|
+
|
|
112
|
+
beforeEach(async () => {
|
|
113
|
+
agent = await authAsUser(app, "notAdmin");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("list", async () => {
|
|
117
|
+
const res = await agent.get("/food").expect(200);
|
|
118
|
+
expect(res.body.data).toHaveLength(2);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("get", async () => {
|
|
122
|
+
const res = await agent.get("/food").expect(200);
|
|
123
|
+
expect(res.body.data).toHaveLength(2);
|
|
124
|
+
const res2 = await server.get(`/food/${res.body.data[0]._id}`).expect(200);
|
|
125
|
+
expect(res.body.data[0]._id).toBe(res2.body.data._id);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("post", async () => {
|
|
129
|
+
await agent
|
|
130
|
+
.post("/food")
|
|
131
|
+
.send({
|
|
132
|
+
calories: 15,
|
|
133
|
+
name: "Broccoli",
|
|
134
|
+
})
|
|
135
|
+
.expect(201);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("patch own item", async () => {
|
|
139
|
+
const res = await agent.get("/food");
|
|
140
|
+
const spinach = res.body.data.find((food: Food) => food.name === "Spinach");
|
|
141
|
+
const res2 = await agent
|
|
142
|
+
.patch(`/food/${spinach._id}`)
|
|
143
|
+
.send({
|
|
144
|
+
name: "Broccoli",
|
|
145
|
+
})
|
|
146
|
+
.expect(200);
|
|
147
|
+
expect(res2.body.data.name).toBe("Broccoli");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("patch other item", async () => {
|
|
151
|
+
const res = await agent.get("/food");
|
|
152
|
+
const spinach = res.body.data.find((food: Food) => food.name === "Apple");
|
|
153
|
+
await agent
|
|
154
|
+
.patch(`/food/${spinach._id}`)
|
|
155
|
+
.send({
|
|
156
|
+
name: "Broccoli",
|
|
157
|
+
})
|
|
158
|
+
.expect(403);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("delete", async () => {
|
|
162
|
+
const res = await agent.get("/food");
|
|
163
|
+
const res2 = await agent.delete(`/food/${res.body.data[0]._id}`);
|
|
164
|
+
expect(res2.status).toBe(405);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("admin food", () => {
|
|
169
|
+
let agent: TestAgent;
|
|
170
|
+
|
|
171
|
+
beforeEach(async () => {
|
|
172
|
+
agent = await authAsUser(app, "admin");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("list", async () => {
|
|
176
|
+
const res = await agent.get("/food");
|
|
177
|
+
expect(res.body.data).toHaveLength(2);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("get", async () => {
|
|
181
|
+
const res = await agent.get("/food");
|
|
182
|
+
expect(res.body.data).toHaveLength(2);
|
|
183
|
+
const res2 = await agent.get(`/food/${res.body.data[0]._id}`);
|
|
184
|
+
expect(res.body.data[0]._id).toBe(res2.body.data._id);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("post", async () => {
|
|
188
|
+
const res = await agent.post("/food").send({
|
|
189
|
+
calories: 15,
|
|
190
|
+
name: "Broccoli",
|
|
191
|
+
});
|
|
192
|
+
expect(res.status).toBe(201);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("patch", async () => {
|
|
196
|
+
const res = await agent.get("/food");
|
|
197
|
+
await agent
|
|
198
|
+
.patch(`/food/${res.body.data[0]._id}`)
|
|
199
|
+
.send({
|
|
200
|
+
name: "Broccoli",
|
|
201
|
+
})
|
|
202
|
+
.expect(200);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("delete", async () => {
|
|
206
|
+
const res = await agent.get("/food");
|
|
207
|
+
await agent.delete(`/food/${res.body.data[0]._id}`).expect(204);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("handles validation errors", async () => {
|
|
211
|
+
await agent
|
|
212
|
+
.post("/required")
|
|
213
|
+
.send({
|
|
214
|
+
about: "Whoops forgot required",
|
|
215
|
+
})
|
|
216
|
+
.expect(400);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// Defaults closed
|
|
2
|
+
import * as Sentry from "@sentry/node";
|
|
3
|
+
import type express from "express";
|
|
4
|
+
import type {NextFunction} from "express";
|
|
5
|
+
import mongoose, {type Model} from "mongoose";
|
|
6
|
+
|
|
7
|
+
import {addPopulateToQuery, getModel, type modelRouterOptions, type RESTMethod} from "./api";
|
|
8
|
+
import type {User} from "./auth";
|
|
9
|
+
import {APIError} from "./errors";
|
|
10
|
+
import {logger} from "./logger";
|
|
11
|
+
|
|
12
|
+
export type PermissionMethod<T> = (
|
|
13
|
+
method: RESTMethod,
|
|
14
|
+
user?: User,
|
|
15
|
+
obj?: T
|
|
16
|
+
) => boolean | Promise<boolean>;
|
|
17
|
+
|
|
18
|
+
export interface RESTPermissions<T> {
|
|
19
|
+
create: PermissionMethod<T>[];
|
|
20
|
+
list: PermissionMethod<T>[];
|
|
21
|
+
read: PermissionMethod<T>[];
|
|
22
|
+
update: PermissionMethod<T>[];
|
|
23
|
+
delete: PermissionMethod<T>[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const OwnerQueryFilter = (user?: User) => {
|
|
27
|
+
if (user) {
|
|
28
|
+
return {ownerId: user?.id};
|
|
29
|
+
}
|
|
30
|
+
// Return a null, so we know to return no results.
|
|
31
|
+
return null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const Permissions = {
|
|
35
|
+
IsAdmin: (_method: RESTMethod, user?: User) => {
|
|
36
|
+
return Boolean(user?.admin);
|
|
37
|
+
},
|
|
38
|
+
IsAny: () => {
|
|
39
|
+
return true;
|
|
40
|
+
},
|
|
41
|
+
IsAuthenticated: (_method: RESTMethod, user?: User) => {
|
|
42
|
+
if (!user) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return Boolean(user.id);
|
|
46
|
+
},
|
|
47
|
+
IsAuthenticatedOrReadOnly: (method: RESTMethod, user?: User) => {
|
|
48
|
+
if (user?.id && !user?.isAnonymous) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return method === "list" || method === "read";
|
|
52
|
+
},
|
|
53
|
+
IsOwner: (_method: RESTMethod, user?: User, obj?: any) => {
|
|
54
|
+
// When checking if we can possibly perform the action, return true.
|
|
55
|
+
if (!obj) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
if (!user) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (user?.admin) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
const ownerId = obj?.ownerId?._id || obj?.ownerId;
|
|
65
|
+
return user?.id && ownerId && String(ownerId) === String(user?.id);
|
|
66
|
+
},
|
|
67
|
+
IsOwnerOrReadOnly: (method: RESTMethod, user?: User, obj?: any) => {
|
|
68
|
+
// When checking if we can possibly perform the action, return true.
|
|
69
|
+
if (!obj) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (user?.admin) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (user?.id && obj?.ownerId && String(obj?.ownerId) === String(user?.id)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
return method === "list" || method === "read";
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export async function checkPermissions<T>(
|
|
84
|
+
method: RESTMethod,
|
|
85
|
+
permissions: PermissionMethod<T>[],
|
|
86
|
+
user?: User,
|
|
87
|
+
obj?: T
|
|
88
|
+
): Promise<boolean> {
|
|
89
|
+
let anyTrue = false;
|
|
90
|
+
for (const perm of permissions) {
|
|
91
|
+
// May or may not be a promise.
|
|
92
|
+
if (!(await perm(method, user, obj))) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
anyTrue = true;
|
|
96
|
+
}
|
|
97
|
+
return anyTrue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check the permissions for a given model and method. If the method is a read, update, or delete,
|
|
101
|
+
// finds the relevant object, checks the permissions, and attaches the object to the request as
|
|
102
|
+
// req.obj.
|
|
103
|
+
export function permissionMiddleware<T>(
|
|
104
|
+
baseModel: Model<T>,
|
|
105
|
+
options: Pick<modelRouterOptions<T>, "permissions" | "populatePaths" | "discriminatorKey">
|
|
106
|
+
) {
|
|
107
|
+
return async (req: express.Request, _res: express.Response, next: NextFunction) => {
|
|
108
|
+
if (req.method === "OPTIONS") {
|
|
109
|
+
return next();
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
let method: "list" | "create" | "read" | "update" | "delete";
|
|
113
|
+
|
|
114
|
+
const reqMethod = req.method.toLowerCase();
|
|
115
|
+
if (reqMethod === "post") {
|
|
116
|
+
method = "create";
|
|
117
|
+
} else if (reqMethod === "get") {
|
|
118
|
+
if (req.params.id) {
|
|
119
|
+
method = "read";
|
|
120
|
+
} else {
|
|
121
|
+
method = "list";
|
|
122
|
+
}
|
|
123
|
+
} else if (reqMethod === "patch") {
|
|
124
|
+
method = "update";
|
|
125
|
+
} else if (reqMethod === "delete") {
|
|
126
|
+
method = "delete";
|
|
127
|
+
} else {
|
|
128
|
+
throw new APIError({
|
|
129
|
+
status: 405,
|
|
130
|
+
title: `Method ${req.method} not allowed`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const model = getModel(baseModel, req.body, options);
|
|
135
|
+
|
|
136
|
+
// All methods check for permissions.
|
|
137
|
+
if (!(await checkPermissions(method, options.permissions[method], req.user))) {
|
|
138
|
+
throw new APIError({
|
|
139
|
+
status: 405,
|
|
140
|
+
title:
|
|
141
|
+
`Access to ${method.toUpperCase()} on ${model.modelName} ` +
|
|
142
|
+
`denied for ${req.user?.id}`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (method === "create" || method === "list") {
|
|
147
|
+
return next();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const builtQuery = model.findById(req.params.id);
|
|
151
|
+
const populatedQuery = addPopulateToQuery(builtQuery as any, options.populatePaths);
|
|
152
|
+
let data;
|
|
153
|
+
try {
|
|
154
|
+
data = await populatedQuery.exec();
|
|
155
|
+
} catch (error: any) {
|
|
156
|
+
throw new APIError({
|
|
157
|
+
error,
|
|
158
|
+
status: 500,
|
|
159
|
+
title: `GET failed on ${req.params.id}`,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (!data || (["update", "delete"].includes(method) && data?.__t && !req.body?.__t)) {
|
|
163
|
+
// For discriminated models, return 404 without checking hidden state
|
|
164
|
+
if (["update", "delete"].includes(method) && data?.__t && !req.body?.__t) {
|
|
165
|
+
throw new APIError({
|
|
166
|
+
status: 404,
|
|
167
|
+
title: `Document ${req.params.id} not found for model ${model.modelName}`,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Check if document exists but is hidden. Completely skip plugins.
|
|
172
|
+
const hiddenDoc = await model.collection.findOne({
|
|
173
|
+
_id: new mongoose.Types.ObjectId(req.params.id),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (!hiddenDoc) {
|
|
177
|
+
Sentry.captureMessage(`Document ${req.params.id} not found for model ${model.modelName}`);
|
|
178
|
+
const error = new APIError({
|
|
179
|
+
status: 404,
|
|
180
|
+
title: `Document ${req.params.id} not found for model ${model.modelName}`,
|
|
181
|
+
});
|
|
182
|
+
error.meta = undefined;
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Document exists but is hidden
|
|
187
|
+
const reason: {[key: string]: string} | null = hiddenDoc.deleted
|
|
188
|
+
? {deleted: "true"}
|
|
189
|
+
: hiddenDoc.disabled
|
|
190
|
+
? {disabled: "true"}
|
|
191
|
+
: hiddenDoc.archived
|
|
192
|
+
? {archived: "true"}
|
|
193
|
+
: null;
|
|
194
|
+
|
|
195
|
+
// If no reason found, treat as not found
|
|
196
|
+
if (!reason) {
|
|
197
|
+
const error = new APIError({
|
|
198
|
+
status: 404,
|
|
199
|
+
title: `Document ${req.params.id} not found for model ${model.modelName}`,
|
|
200
|
+
});
|
|
201
|
+
error.meta = undefined;
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
throw new APIError({
|
|
205
|
+
// We don't want to send this to Sentry because it's expected behavior.
|
|
206
|
+
disableExternalErrorTracking: true,
|
|
207
|
+
meta: reason,
|
|
208
|
+
status: 404,
|
|
209
|
+
title: `Document ${req.params.id} not found for model ${model.modelName}`,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!(await checkPermissions(method, options.permissions[method], req.user, data))) {
|
|
214
|
+
throw new APIError({
|
|
215
|
+
status: 403,
|
|
216
|
+
title: `Access to GET on ${model.modelName}:${req.params.id} denied for ${req.user?.id}`,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
(req as any).obj = data;
|
|
221
|
+
|
|
222
|
+
return next();
|
|
223
|
+
} catch (error) {
|
|
224
|
+
logger.error(`Permissions error: ${error instanceof Error ? error.message : error}`);
|
|
225
|
+
return next(error);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|