@vex-chat/spire 3.0.1 → 4.1.0
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/README.md +9 -8
- package/dist/Database.d.ts +20 -11
- package/dist/Database.js +229 -118
- package/dist/Database.js.map +1 -1
- package/dist/Spire.d.ts +4 -2
- package/dist/Spire.js +155 -72
- package/dist/Spire.js.map +1 -1
- package/dist/db/schema.d.ts +0 -2
- package/dist/migrations/2026-04-06_initial-schema.js +4 -5
- package/dist/migrations/2026-04-06_initial-schema.js.map +1 -1
- package/dist/migrations/{2026-04-14_argon2id-password-hashing.d.ts → 2026-07-14_query-indexes.d.ts} +1 -1
- package/dist/migrations/2026-07-14_query-indexes.js +31 -0
- package/dist/migrations/2026-07-14_query-indexes.js.map +1 -0
- package/dist/server/avatar.js +33 -6
- package/dist/server/avatar.js.map +1 -1
- package/dist/server/cliPasskeyPage.js +109 -11
- package/dist/server/cliPasskeyPage.js.map +1 -1
- package/dist/server/errors.js +8 -0
- package/dist/server/errors.js.map +1 -1
- package/dist/server/file.js +35 -12
- package/dist/server/file.js.map +1 -1
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.js +319 -56
- package/dist/server/index.js.map +1 -1
- package/dist/server/passkey.js +367 -29
- package/dist/server/passkey.js.map +1 -1
- package/dist/server/passkeyDevices.js +1 -0
- package/dist/server/passkeyDevices.js.map +1 -1
- package/dist/server/password.d.ts +7 -0
- package/dist/server/password.js +58 -0
- package/dist/server/password.js.map +1 -0
- package/dist/server/permissions.d.ts +1 -0
- package/dist/server/permissions.js +11 -2
- package/dist/server/permissions.js.map +1 -1
- package/dist/server/rateLimit.d.ts +11 -0
- package/dist/server/rateLimit.js +43 -4
- package/dist/server/rateLimit.js.map +1 -1
- package/dist/server/serverIcon.d.ts +10 -0
- package/dist/server/serverIcon.js +158 -0
- package/dist/server/serverIcon.js.map +1 -0
- package/dist/server/user.d.ts +1 -1
- package/dist/server/user.js +40 -9
- package/dist/server/user.js.map +1 -1
- package/dist/types/express.d.ts +3 -4
- package/dist/types/express.js.map +1 -1
- package/dist/utils/authJwt.d.ts +8 -0
- package/dist/utils/authJwt.js +25 -0
- package/dist/utils/authJwt.js.map +1 -0
- package/dist/utils/loadEnv.js +4 -0
- package/dist/utils/loadEnv.js.map +1 -1
- package/dist/utils/preKeySignature.d.ts +1 -1
- package/dist/utils/preKeySignature.js +7 -11
- package/dist/utils/preKeySignature.js.map +1 -1
- package/package.json +7 -7
- package/src/Database.ts +294 -152
- package/src/Spire.ts +412 -285
- package/src/__tests__/Database.spec.ts +126 -3
- package/src/__tests__/browserPasskeyAuthentication.spec.ts +192 -0
- package/src/__tests__/browserPasskeyRegistration.spec.ts +182 -0
- package/src/__tests__/cliPasskeyPage.spec.ts +6 -0
- package/src/__tests__/connectAuth.spec.ts +146 -4
- package/src/__tests__/deviceTokenRevalidation.spec.ts +57 -3
- package/src/__tests__/passkeyDevices.spec.ts +94 -0
- package/src/__tests__/passkeys.spec.ts +7 -2
- package/src/__tests__/password.spec.ts +223 -0
- package/src/__tests__/permissions.spec.ts +50 -0
- package/src/__tests__/preKeySignature.spec.ts +20 -3
- package/src/__tests__/serverManagement.spec.ts +262 -0
- package/src/db/schema.ts +0 -2
- package/src/migrations/2026-04-06_initial-schema.ts +4 -5
- package/src/migrations/2026-07-14_query-indexes.ts +34 -0
- package/src/server/avatar.ts +32 -6
- package/src/server/cliPasskeyPage.ts +109 -11
- package/src/server/errors.ts +7 -0
- package/src/server/file.ts +34 -13
- package/src/server/index.ts +494 -139
- package/src/server/passkey.ts +531 -31
- package/src/server/passkeyDevices.ts +1 -0
- package/src/server/password.ts +96 -0
- package/src/server/permissions.ts +20 -2
- package/src/server/rateLimit.ts +47 -4
- package/src/server/serverIcon.ts +199 -0
- package/src/server/user.ts +44 -9
- package/src/types/express.ts +3 -4
- package/src/utils/authJwt.ts +34 -0
- package/src/utils/loadEnv.ts +4 -0
- package/src/utils/preKeySignature.ts +12 -15
- package/dist/migrations/2026-04-14_argon2id-password-hashing.js +0 -17
- package/dist/migrations/2026-04-14_argon2id-password-hashing.js.map +0 -1
- package/src/migrations/2026-04-14_argon2id-password-hashing.ts +0 -24
package/src/server/index.ts
CHANGED
|
@@ -13,12 +13,16 @@ import * as fsp from "node:fs/promises";
|
|
|
13
13
|
import express from "express";
|
|
14
14
|
|
|
15
15
|
import { type KeyPair, XUtils } from "@vex-chat/crypto";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
MAX_FILE_UPLOAD_ENCODED_BODY_BYTES,
|
|
18
|
+
PreKeysWSSchema,
|
|
19
|
+
TokenScopes,
|
|
20
|
+
UserSchema,
|
|
21
|
+
} from "@vex-chat/types";
|
|
17
22
|
|
|
18
23
|
import cors from "cors";
|
|
19
24
|
import { fileTypeFromBuffer, fileTypeFromFile } from "file-type";
|
|
20
25
|
import helmet from "helmet";
|
|
21
|
-
import jwt from "jsonwebtoken";
|
|
22
26
|
import multer from "multer";
|
|
23
27
|
import parseDuration from "parse-duration";
|
|
24
28
|
import { stringify as uuidStringify } from "uuid";
|
|
@@ -26,7 +30,7 @@ import { z } from "zod/v4";
|
|
|
26
30
|
|
|
27
31
|
import { POWER_LEVELS } from "../ClientManager.ts";
|
|
28
32
|
import { JWT_EXPIRY } from "../Spire.ts";
|
|
29
|
-
import {
|
|
33
|
+
import { signAuthJwt, verifyAuthJwt } from "../utils/authJwt.ts";
|
|
30
34
|
import { msgpack } from "../utils/msgpack.ts";
|
|
31
35
|
import { verifyPreKeyWsSignature } from "../utils/preKeySignature.ts";
|
|
32
36
|
import { spireXSignOpenAsync } from "../utils/spireXSignOpenAsync.ts";
|
|
@@ -41,12 +45,15 @@ import { getInviteRouter } from "./invite.ts";
|
|
|
41
45
|
import { setupDocs } from "./openapi.ts";
|
|
42
46
|
import { getPasskeyRouter } from "./passkey.ts";
|
|
43
47
|
import { getPasskeyDeviceRouter } from "./passkeyDevices.ts";
|
|
48
|
+
import { getPasswordRouter } from "./password.ts";
|
|
44
49
|
import {
|
|
50
|
+
canDeletePermission,
|
|
45
51
|
hasAnyPermission,
|
|
46
52
|
hasPermission,
|
|
47
53
|
userHasPermission,
|
|
48
54
|
} from "./permissions.ts";
|
|
49
|
-
import { globalLimiter, keyBundleLimiter } from "./rateLimit.ts";
|
|
55
|
+
import { globalLimiter, keyBundleLimiter, uploadLimiter } from "./rateLimit.ts";
|
|
56
|
+
import { deleteServerIconFile, getServerIconRouter } from "./serverIcon.ts";
|
|
50
57
|
import { getUserRouter } from "./user.ts";
|
|
51
58
|
import { censorUser, getParam, getUser } from "./utils.ts";
|
|
52
59
|
import { getWellKnownRouter } from "./wellKnown.ts";
|
|
@@ -60,34 +67,59 @@ export const ALLOWED_IMAGE_TYPES = [
|
|
|
60
67
|
"image/gif",
|
|
61
68
|
"image/apng",
|
|
62
69
|
"image/avif",
|
|
70
|
+
"image/webp",
|
|
63
71
|
];
|
|
64
72
|
|
|
65
73
|
// ── Zod schemas for trust-boundary validation ──────────────────────────
|
|
66
74
|
const invitePayload = z.object({
|
|
67
|
-
duration: z.string().min(1),
|
|
68
|
-
serverID: z.string().min(1),
|
|
75
|
+
duration: z.string().min(1).max(32),
|
|
76
|
+
serverID: z.string().min(1).max(128),
|
|
69
77
|
});
|
|
70
78
|
|
|
71
79
|
const channelPayload = z.object({
|
|
72
|
-
name: z.string().min(1).max(
|
|
80
|
+
name: z.string().trim().min(1).max(100),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const serverUpdatePayload = z.object({
|
|
84
|
+
name: z.string().trim().min(1).max(100),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const permissionRolePayload = z.object({
|
|
88
|
+
powerLevel: z.union([z.literal(0), z.literal(50), z.literal(100)]),
|
|
73
89
|
});
|
|
74
90
|
|
|
75
|
-
const deviceListPayload = z.array(z.string());
|
|
91
|
+
const deviceListPayload = z.array(z.string().min(1).max(128)).max(256);
|
|
76
92
|
|
|
77
93
|
const connectPayload = z.object({
|
|
78
|
-
signed: z.custom<Uint8Array>(
|
|
94
|
+
signed: z.custom<Uint8Array>(
|
|
95
|
+
(val) => val instanceof Uint8Array && val.byteLength <= 16_384,
|
|
96
|
+
),
|
|
79
97
|
});
|
|
80
98
|
|
|
81
99
|
const safePathParam = z.string().regex(/^[a-zA-Z0-9._-]+$/);
|
|
82
100
|
|
|
83
101
|
const emojiPayload = z.object({
|
|
84
|
-
file: z.string().optional(),
|
|
85
|
-
name: z.string().min(1),
|
|
86
|
-
signed: z.string().optional(),
|
|
102
|
+
file: z.string().max(350_000).optional(),
|
|
103
|
+
name: z.string().trim().min(1).max(64),
|
|
104
|
+
signed: z.string().max(8192).optional(),
|
|
87
105
|
});
|
|
106
|
+
const emojiUpload = multer({
|
|
107
|
+
limits: { fields: 3, files: 1, fileSize: 256_000, parts: 4 },
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const DEVELOPMENT_CORS_ORIGINS: Array<RegExp | string> = [
|
|
111
|
+
/^https?:\/\/localhost(?::\d{1,5})?$/,
|
|
112
|
+
/^https?:\/\/127\.0\.0\.1(?::\d{1,5})?$/,
|
|
113
|
+
/^https?:\/\/\[::1\](?::\d{1,5})?$/,
|
|
114
|
+
"capacitor://localhost",
|
|
115
|
+
"http://tauri.localhost",
|
|
116
|
+
"https://tauri.localhost",
|
|
117
|
+
"tauri://localhost",
|
|
118
|
+
];
|
|
88
119
|
|
|
89
120
|
const jwtUserPayload = z.object({
|
|
90
121
|
exp: z.number().optional(),
|
|
122
|
+
scope: z.literal("user"),
|
|
91
123
|
user: UserSchema,
|
|
92
124
|
});
|
|
93
125
|
|
|
@@ -100,6 +132,7 @@ const jwtDevicePayload = z.object({
|
|
|
100
132
|
owner: z.string(),
|
|
101
133
|
signKey: z.string(),
|
|
102
134
|
}),
|
|
135
|
+
scope: z.literal("device"),
|
|
103
136
|
});
|
|
104
137
|
|
|
105
138
|
const jwtPasskeyPayload = z.object({
|
|
@@ -121,7 +154,7 @@ const checkAuth: express.RequestHandler = (req, _res, next) => {
|
|
|
121
154
|
const token = extractBearer(req);
|
|
122
155
|
if (token) {
|
|
123
156
|
try {
|
|
124
|
-
const result =
|
|
157
|
+
const result = verifyAuthJwt(token);
|
|
125
158
|
// Passkey-scoped JWTs share the bearer slot with regular
|
|
126
159
|
// user JWTs but carry a `scope: "passkey"` discriminator
|
|
127
160
|
// and a `passkey` claim. Populate `req.passkey` and the
|
|
@@ -158,7 +191,7 @@ export function createCheckDevice(
|
|
|
158
191
|
const token = req.headers["x-device-token"];
|
|
159
192
|
if (typeof token === "string" && token) {
|
|
160
193
|
try {
|
|
161
|
-
const result =
|
|
194
|
+
const result = verifyAuthJwt(token);
|
|
162
195
|
const parsed = jwtDevicePayload.safeParse(result);
|
|
163
196
|
if (parsed.success) {
|
|
164
197
|
const device = await currentTokenDevice(
|
|
@@ -180,6 +213,29 @@ export function createCheckDevice(
|
|
|
180
213
|
};
|
|
181
214
|
}
|
|
182
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Revalidate the credential behind a passkey-scoped bearer on every request.
|
|
218
|
+
* Passkey JWTs are short-lived, but removing a credential must revoke its
|
|
219
|
+
* recovery and account-administration access immediately.
|
|
220
|
+
*/
|
|
221
|
+
export function createCheckPasskey(
|
|
222
|
+
db: Pick<Database, "retrievePasskeyInternal">,
|
|
223
|
+
): express.RequestHandler {
|
|
224
|
+
return async (req, _res, next) => {
|
|
225
|
+
if (req.passkey) {
|
|
226
|
+
const passkey = await db.retrievePasskeyInternal(
|
|
227
|
+
req.passkey.passkeyID,
|
|
228
|
+
);
|
|
229
|
+
if (!passkey || !req.user || passkey.userID !== req.user.userID) {
|
|
230
|
+
delete req.bearerToken;
|
|
231
|
+
delete req.passkey;
|
|
232
|
+
delete req.user;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
next();
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
183
239
|
async function currentTokenDevice(
|
|
184
240
|
db: Pick<Database, "retrieveDevice">,
|
|
185
241
|
tokenDevice: Device,
|
|
@@ -192,7 +248,7 @@ async function currentTokenDevice(
|
|
|
192
248
|
}
|
|
193
249
|
|
|
194
250
|
export const protect: express.RequestHandler = (req, res, next) => {
|
|
195
|
-
if (!req.user) {
|
|
251
|
+
if (!req.user || req.passkey) {
|
|
196
252
|
res.sendStatus(401);
|
|
197
253
|
return;
|
|
198
254
|
}
|
|
@@ -200,6 +256,15 @@ export const protect: express.RequestHandler = (req, res, next) => {
|
|
|
200
256
|
next();
|
|
201
257
|
};
|
|
202
258
|
|
|
259
|
+
/** Require either an approved device or a fresh passkey session. */
|
|
260
|
+
export const protectAnyAuth: express.RequestHandler = (req, res, next) => {
|
|
261
|
+
if (!req.user || (!req.device && !req.passkey)) {
|
|
262
|
+
res.sendStatus(401);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
next();
|
|
266
|
+
};
|
|
267
|
+
|
|
203
268
|
/**
|
|
204
269
|
* Restrict a route to passkey-scoped JWTs (used by the parallel
|
|
205
270
|
* `/user/:id/passkey/devices/...` admin/recovery routes). A
|
|
@@ -228,7 +293,7 @@ export const msgpackParser: express.RequestHandler = (req, res, next) => {
|
|
|
228
293
|
next();
|
|
229
294
|
};
|
|
230
295
|
|
|
231
|
-
const directories = ["files", "avatars"];
|
|
296
|
+
const directories = ["files", "avatars", "server-icons"];
|
|
232
297
|
for (const dir of directories) {
|
|
233
298
|
if (!fs.existsSync(dir)) {
|
|
234
299
|
fs.mkdirSync(dir);
|
|
@@ -249,14 +314,35 @@ export const initApp = (
|
|
|
249
314
|
) => void,
|
|
250
315
|
disconnectDevices?: (deviceIDs: string[]) => void,
|
|
251
316
|
) => {
|
|
317
|
+
const notifyServerChange = async (
|
|
318
|
+
serverID: string,
|
|
319
|
+
additionalUserIDs: readonly string[] = [],
|
|
320
|
+
): Promise<void> => {
|
|
321
|
+
const affectedUsers = await db.retrieveAffectedUsers(serverID);
|
|
322
|
+
const userIDs = new Set([
|
|
323
|
+
...affectedUsers.map((user) => user.userID),
|
|
324
|
+
...additionalUserIDs,
|
|
325
|
+
]);
|
|
326
|
+
for (const userID of userIDs) {
|
|
327
|
+
notify(userID, "serverChange", crypto.randomUUID(), serverID);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
252
331
|
// INIT ROUTERS
|
|
253
|
-
const userRouter = getUserRouter(
|
|
332
|
+
const userRouter = getUserRouter(
|
|
333
|
+
db,
|
|
334
|
+
tokenValidator,
|
|
335
|
+
notify,
|
|
336
|
+
disconnectDevices,
|
|
337
|
+
);
|
|
254
338
|
const fileRouter = getFileRouter(db);
|
|
255
339
|
const avatarRouter = getAvatarRouter();
|
|
256
340
|
const billingRouter = getBillingRouter(db, notify);
|
|
257
341
|
const entitlementRouter = getEntitlementRouter(db, notify);
|
|
258
342
|
const inviteRouter = getInviteRouter(db, tokenValidator, notify);
|
|
259
343
|
const passkeyRouter = getPasskeyRouter(db);
|
|
344
|
+
const passwordRouter = getPasswordRouter(db);
|
|
345
|
+
const serverIconRouter = getServerIconRouter(db, notifyServerChange);
|
|
260
346
|
const passkeyDeviceRouter = getPasskeyDeviceRouter(
|
|
261
347
|
db,
|
|
262
348
|
notify,
|
|
@@ -277,6 +363,17 @@ export const initApp = (
|
|
|
277
363
|
// spends any cycles on body parsing, helmet, or auth. See
|
|
278
364
|
// src/server/rateLimit.ts.
|
|
279
365
|
api.use(globalLimiter);
|
|
366
|
+
// Base64 expands a 25 MiB encrypted upload to about 33.4 MiB. Give only
|
|
367
|
+
// the JSON fallback route enough room for that encoded payload and its
|
|
368
|
+
// JSON/msgpack envelope; unrelated routes keep the tighter global cap.
|
|
369
|
+
api.use(
|
|
370
|
+
"/file/json",
|
|
371
|
+
express.json({ limit: MAX_FILE_UPLOAD_ENCODED_BODY_BYTES }),
|
|
372
|
+
express.raw({
|
|
373
|
+
limit: MAX_FILE_UPLOAD_ENCODED_BODY_BYTES,
|
|
374
|
+
type: "application/msgpack",
|
|
375
|
+
}),
|
|
376
|
+
);
|
|
280
377
|
api.use(express.json({ limit: "20mb" }));
|
|
281
378
|
api.use(
|
|
282
379
|
express.raw({
|
|
@@ -310,7 +407,9 @@ export const initApp = (
|
|
|
310
407
|
origin:
|
|
311
408
|
corsOrigins.length > 0
|
|
312
409
|
? corsOrigins
|
|
313
|
-
:
|
|
410
|
+
: process.env["NODE_ENV"] === "production"
|
|
411
|
+
? false
|
|
412
|
+
: DEVELOPMENT_CORS_ORIGINS,
|
|
314
413
|
}),
|
|
315
414
|
);
|
|
316
415
|
|
|
@@ -323,10 +422,20 @@ export const initApp = (
|
|
|
323
422
|
|
|
324
423
|
api.use(msgpackParser);
|
|
325
424
|
api.use(checkAuth);
|
|
425
|
+
api.use(createCheckPasskey(db));
|
|
326
426
|
api.use(createCheckDevice(db));
|
|
327
427
|
|
|
328
428
|
api.get("/server/:id", protect, async (req, res) => {
|
|
329
|
-
const
|
|
429
|
+
const serverID = getParam(req, "id");
|
|
430
|
+
const userDetails = getUser(req);
|
|
431
|
+
const permissions = await db.retrievePermissions(
|
|
432
|
+
userDetails.userID,
|
|
433
|
+
"server",
|
|
434
|
+
);
|
|
435
|
+
if (!hasAnyPermission(permissions, serverID)) {
|
|
436
|
+
return res.sendStatus(403);
|
|
437
|
+
}
|
|
438
|
+
const server = await db.retrieveServer(serverID);
|
|
330
439
|
|
|
331
440
|
if (server) {
|
|
332
441
|
return res.send(msgpack.encode(server));
|
|
@@ -335,11 +444,83 @@ export const initApp = (
|
|
|
335
444
|
}
|
|
336
445
|
});
|
|
337
446
|
|
|
447
|
+
api.patch("/server/:id", protect, async (req, res) => {
|
|
448
|
+
const parsed = serverUpdatePayload.safeParse(req.body);
|
|
449
|
+
if (!parsed.success) {
|
|
450
|
+
res.status(400).json({
|
|
451
|
+
error: "Invalid server update",
|
|
452
|
+
issues: parsed.error.issues,
|
|
453
|
+
});
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const serverID = getParam(req, "id");
|
|
458
|
+
const permissions = await db.retrievePermissions(
|
|
459
|
+
getUser(req).userID,
|
|
460
|
+
"server",
|
|
461
|
+
);
|
|
462
|
+
if (!hasPermission(permissions, serverID, POWER_LEVELS.CREATE)) {
|
|
463
|
+
res.sendStatus(403);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const server = await db.updateServer(serverID, parsed.data);
|
|
468
|
+
if (!server) {
|
|
469
|
+
res.sendStatus(404);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
await notifyServerChange(serverID);
|
|
473
|
+
res.send(msgpack.encode(server));
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
api.post("/servers", protect, async (req, res) => {
|
|
477
|
+
const parsed = serverUpdatePayload.safeParse(req.body);
|
|
478
|
+
if (!parsed.success) {
|
|
479
|
+
res.status(400).json({
|
|
480
|
+
error: "Invalid server",
|
|
481
|
+
issues: parsed.error.issues,
|
|
482
|
+
});
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const server = await db.createServer(
|
|
487
|
+
parsed.data.name,
|
|
488
|
+
getUser(req).userID,
|
|
489
|
+
);
|
|
490
|
+
res.send(msgpack.encode(server));
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// Compatibility route for clients released before server names moved into
|
|
494
|
+
// the request body. New clients use POST /servers so Unicode names work.
|
|
338
495
|
api.post("/server/:name", protect, async (req, res) => {
|
|
339
496
|
const userDetails = getUser(req);
|
|
340
|
-
const
|
|
497
|
+
const encodedName = getParam(req, "name");
|
|
498
|
+
if (encodedName.length > 256) {
|
|
499
|
+
res.sendStatus(400);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
let decodedName: string;
|
|
503
|
+
try {
|
|
504
|
+
decodedName = atob(encodedName);
|
|
505
|
+
} catch {
|
|
506
|
+
res.sendStatus(400);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const parsedName = z
|
|
510
|
+
.string()
|
|
511
|
+
.trim()
|
|
512
|
+
.min(1)
|
|
513
|
+
.max(100)
|
|
514
|
+
.safeParse(decodedName);
|
|
515
|
+
if (!parsedName.success) {
|
|
516
|
+
res.sendStatus(400);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
341
519
|
|
|
342
|
-
const server = await db.createServer(
|
|
520
|
+
const server = await db.createServer(
|
|
521
|
+
parsedName.data,
|
|
522
|
+
userDetails.userID,
|
|
523
|
+
);
|
|
343
524
|
res.send(msgpack.encode(server));
|
|
344
525
|
});
|
|
345
526
|
|
|
@@ -429,11 +610,22 @@ export const initApp = (
|
|
|
429
610
|
"server",
|
|
430
611
|
);
|
|
431
612
|
if (hasPermission(permissions, serverID, POWER_LEVELS.DELETE)) {
|
|
613
|
+
const server = await db.retrieveServer(serverID);
|
|
614
|
+
if (!server) {
|
|
615
|
+
res.sendStatus(404);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const affectedUsers = await db.retrieveAffectedUsers(serverID);
|
|
432
619
|
await db.deleteServer(serverID);
|
|
620
|
+
if (server.icon) await deleteServerIconFile(server.icon);
|
|
621
|
+
await notifyServerChange(
|
|
622
|
+
serverID,
|
|
623
|
+
affectedUsers.map((user) => user.userID),
|
|
624
|
+
);
|
|
433
625
|
res.sendStatus(200);
|
|
434
626
|
return;
|
|
435
627
|
}
|
|
436
|
-
res.sendStatus(
|
|
628
|
+
res.sendStatus(403);
|
|
437
629
|
});
|
|
438
630
|
|
|
439
631
|
api.post("/server/:id/channels", protect, async (req, res) => {
|
|
@@ -456,20 +648,10 @@ export const initApp = (
|
|
|
456
648
|
if (hasPermission(permissions, serverID, POWER_LEVELS.CREATE)) {
|
|
457
649
|
const channel = await db.createChannel(name, serverID);
|
|
458
650
|
res.send(msgpack.encode(channel));
|
|
459
|
-
|
|
460
|
-
const affectedUsers = await db.retrieveAffectedUsers(serverID);
|
|
461
|
-
// tell everyone about server change
|
|
462
|
-
for (const user of affectedUsers) {
|
|
463
|
-
notify(
|
|
464
|
-
user.userID,
|
|
465
|
-
"serverChange",
|
|
466
|
-
crypto.randomUUID(),
|
|
467
|
-
serverID,
|
|
468
|
-
);
|
|
469
|
-
}
|
|
651
|
+
await notifyServerChange(serverID);
|
|
470
652
|
return;
|
|
471
653
|
}
|
|
472
|
-
res.sendStatus(
|
|
654
|
+
res.sendStatus(403);
|
|
473
655
|
});
|
|
474
656
|
|
|
475
657
|
api.get("/server/:id/channels", protect, async (req, res) => {
|
|
@@ -488,7 +670,16 @@ export const initApp = (
|
|
|
488
670
|
});
|
|
489
671
|
|
|
490
672
|
api.get("/server/:serverID/emoji", protect, async (req, res) => {
|
|
491
|
-
const
|
|
673
|
+
const serverID = getParam(req, "serverID");
|
|
674
|
+
const permissions = await db.retrievePermissions(
|
|
675
|
+
getUser(req).userID,
|
|
676
|
+
"server",
|
|
677
|
+
);
|
|
678
|
+
if (!hasAnyPermission(permissions, serverID)) {
|
|
679
|
+
res.sendStatus(403);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const rows = await db.retrieveEmojiList(serverID);
|
|
492
683
|
res.send(msgpack.encode(rows));
|
|
493
684
|
});
|
|
494
685
|
|
|
@@ -506,6 +697,42 @@ export const initApp = (
|
|
|
506
697
|
res.send(msgpack.encode(permissions));
|
|
507
698
|
});
|
|
508
699
|
|
|
700
|
+
api.patch("/channel/:id", protect, async (req, res) => {
|
|
701
|
+
const parsed = channelPayload.safeParse(req.body);
|
|
702
|
+
if (!parsed.success) {
|
|
703
|
+
res.status(400).json({
|
|
704
|
+
error: "Invalid channel update",
|
|
705
|
+
issues: parsed.error.issues,
|
|
706
|
+
});
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const channelID = getParam(req, "id");
|
|
711
|
+
const channel = await db.retrieveChannel(channelID);
|
|
712
|
+
if (!channel) {
|
|
713
|
+
res.sendStatus(404);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
const permissions = await db.retrievePermissions(
|
|
717
|
+
getUser(req).userID,
|
|
718
|
+
"server",
|
|
719
|
+
);
|
|
720
|
+
if (
|
|
721
|
+
!hasPermission(permissions, channel.serverID, POWER_LEVELS.DELETE)
|
|
722
|
+
) {
|
|
723
|
+
res.sendStatus(403);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const updated = await db.updateChannel(channelID, parsed.data.name);
|
|
728
|
+
if (!updated) {
|
|
729
|
+
res.sendStatus(404);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
await notifyServerChange(channel.serverID);
|
|
733
|
+
res.send(msgpack.encode(updated));
|
|
734
|
+
});
|
|
735
|
+
|
|
509
736
|
api.delete("/channel/:id", protect, async (req, res) => {
|
|
510
737
|
const channelID = getParam(req, "id");
|
|
511
738
|
const userDetails = getUser(req);
|
|
@@ -524,40 +751,90 @@ export const initApp = (
|
|
|
524
751
|
for (const permission of permissions) {
|
|
525
752
|
if (
|
|
526
753
|
permission.resourceID === channel.serverID &&
|
|
527
|
-
permission.powerLevel
|
|
754
|
+
permission.powerLevel >= POWER_LEVELS.DELETE
|
|
528
755
|
) {
|
|
529
|
-
await db.
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
);
|
|
536
|
-
// tell everyone about server change
|
|
537
|
-
for (const user of affectedUsers) {
|
|
538
|
-
notify(
|
|
539
|
-
user.userID,
|
|
540
|
-
"serverChange",
|
|
541
|
-
crypto.randomUUID(),
|
|
542
|
-
channel.serverID,
|
|
543
|
-
);
|
|
756
|
+
const deleted = await db.deleteChannelIfNotLast(channelID);
|
|
757
|
+
if (!deleted) {
|
|
758
|
+
res.status(409).json({
|
|
759
|
+
error: "A server must have at least one channel.",
|
|
760
|
+
});
|
|
761
|
+
return;
|
|
544
762
|
}
|
|
763
|
+
await notifyServerChange(channel.serverID);
|
|
764
|
+
res.sendStatus(200);
|
|
545
765
|
return;
|
|
546
766
|
}
|
|
547
767
|
}
|
|
548
|
-
res.sendStatus(
|
|
768
|
+
res.sendStatus(403);
|
|
549
769
|
});
|
|
550
770
|
|
|
551
771
|
api.get("/channel/:id", protect, async (req, res) => {
|
|
552
772
|
const channel = await db.retrieveChannel(getParam(req, "id"));
|
|
553
773
|
|
|
554
774
|
if (channel) {
|
|
775
|
+
const permissions = await db.retrievePermissions(
|
|
776
|
+
getUser(req).userID,
|
|
777
|
+
"server",
|
|
778
|
+
);
|
|
779
|
+
if (!hasAnyPermission(permissions, channel.serverID)) {
|
|
780
|
+
return res.sendStatus(403);
|
|
781
|
+
}
|
|
555
782
|
return res.send(msgpack.encode(channel));
|
|
556
783
|
} else {
|
|
557
784
|
return res.sendStatus(404);
|
|
558
785
|
}
|
|
559
786
|
});
|
|
560
787
|
|
|
788
|
+
api.patch("/permission/:permissionID", protect, async (req, res) => {
|
|
789
|
+
const parsed = permissionRolePayload.safeParse(req.body);
|
|
790
|
+
if (!parsed.success) {
|
|
791
|
+
res.status(400).json({
|
|
792
|
+
error: "Invalid role",
|
|
793
|
+
issues: parsed.error.issues,
|
|
794
|
+
});
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const target = await db.retrievePermission(
|
|
799
|
+
getParam(req, "permissionID"),
|
|
800
|
+
);
|
|
801
|
+
if (!target) {
|
|
802
|
+
res.sendStatus(404);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
if (target.resourceType !== "server") {
|
|
806
|
+
res.sendStatus(400);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const actor = getUser(req);
|
|
811
|
+
if (target.userID === actor.userID) {
|
|
812
|
+
res.status(400).json({
|
|
813
|
+
error: "Use another owner to change your role.",
|
|
814
|
+
});
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
const actorPermissions = await db.retrievePermissions(
|
|
818
|
+
actor.userID,
|
|
819
|
+
"server",
|
|
820
|
+
);
|
|
821
|
+
if (!hasPermission(actorPermissions, target.resourceID, 100)) {
|
|
822
|
+
res.sendStatus(403);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const updated = await db.updatePermissionPowerLevel(
|
|
827
|
+
target.permissionID,
|
|
828
|
+
parsed.data.powerLevel,
|
|
829
|
+
);
|
|
830
|
+
if (!updated) {
|
|
831
|
+
res.sendStatus(404);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
await notifyServerChange(target.resourceID);
|
|
835
|
+
res.send(msgpack.encode(updated));
|
|
836
|
+
});
|
|
837
|
+
|
|
561
838
|
api.delete("/permission/:permissionID", protect, async (req, res) => {
|
|
562
839
|
const permissionID = getParam(req, "permissionID");
|
|
563
840
|
const userDetails = getUser(req);
|
|
@@ -572,19 +849,44 @@ export const initApp = (
|
|
|
572
849
|
permToDelete.resourceType,
|
|
573
850
|
);
|
|
574
851
|
|
|
575
|
-
|
|
852
|
+
if (
|
|
853
|
+
canDeletePermission(
|
|
854
|
+
permissions,
|
|
855
|
+
userDetails.userID,
|
|
856
|
+
permToDelete,
|
|
857
|
+
POWER_LEVELS.DELETE,
|
|
858
|
+
)
|
|
859
|
+
) {
|
|
576
860
|
if (
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
(perm.powerLevel > POWER_LEVELS.DELETE &&
|
|
580
|
-
perm.powerLevel > permToDelete.powerLevel))
|
|
861
|
+
permToDelete.resourceType === "server" &&
|
|
862
|
+
permToDelete.powerLevel >= 100
|
|
581
863
|
) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
864
|
+
const resourcePermissions =
|
|
865
|
+
await db.retrievePermissionsByResourceID(
|
|
866
|
+
permToDelete.resourceID,
|
|
867
|
+
);
|
|
868
|
+
const hasAnotherOwner = resourcePermissions.some(
|
|
869
|
+
(permission) =>
|
|
870
|
+
permission.permissionID !== permToDelete.permissionID &&
|
|
871
|
+
permission.powerLevel >= 100,
|
|
872
|
+
);
|
|
873
|
+
if (!hasAnotherOwner) {
|
|
874
|
+
res.status(409).json({
|
|
875
|
+
error: "Delete the server instead of leaving it without an owner.",
|
|
876
|
+
});
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
585
879
|
}
|
|
880
|
+
await db.deletePermission(permToDelete.permissionID);
|
|
881
|
+
if (permToDelete.resourceType === "server") {
|
|
882
|
+
await notifyServerChange(permToDelete.resourceID, [
|
|
883
|
+
permToDelete.userID,
|
|
884
|
+
]);
|
|
885
|
+
}
|
|
886
|
+
res.sendStatus(200);
|
|
887
|
+
return;
|
|
586
888
|
}
|
|
587
|
-
res.sendStatus(
|
|
889
|
+
res.sendStatus(403);
|
|
588
890
|
});
|
|
589
891
|
|
|
590
892
|
api.post("/userList/:channelID", protect, async (req, res) => {
|
|
@@ -661,6 +963,10 @@ export const initApp = (
|
|
|
661
963
|
res.sendStatus(401);
|
|
662
964
|
return;
|
|
663
965
|
}
|
|
966
|
+
if (deviceDetails.deviceID !== getParam(req, "id")) {
|
|
967
|
+
res.sendStatus(403);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
664
970
|
const inbox = await db.retrieveMail(deviceDetails.deviceID);
|
|
665
971
|
res.send(msgpack.encode(inbox));
|
|
666
972
|
});
|
|
@@ -692,10 +998,7 @@ export const initApp = (
|
|
|
692
998
|
regKey &&
|
|
693
999
|
tokenValidator(uuidStringify(regKey), TokenScopes.Connect)
|
|
694
1000
|
) {
|
|
695
|
-
const token =
|
|
696
|
-
expiresIn: JWT_EXPIRY,
|
|
697
|
-
});
|
|
698
|
-
jwt.verify(token, getJwtSecret());
|
|
1001
|
+
const token = signAuthJwt({ device, scope: "device" }, JWT_EXPIRY);
|
|
699
1002
|
|
|
700
1003
|
res.send(msgpack.encode({ deviceToken: token }));
|
|
701
1004
|
} else {
|
|
@@ -709,6 +1012,10 @@ export const initApp = (
|
|
|
709
1012
|
res.sendStatus(401);
|
|
710
1013
|
return;
|
|
711
1014
|
}
|
|
1015
|
+
if (deviceDetails.deviceID !== getParam(req, "id")) {
|
|
1016
|
+
res.sendStatus(403);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
712
1019
|
const count = await db.getOTKCount(deviceDetails.deviceID);
|
|
713
1020
|
res.send(msgpack.encode({ count }));
|
|
714
1021
|
});
|
|
@@ -747,7 +1054,9 @@ export const initApp = (
|
|
|
747
1054
|
res.status(400).send({ error: "Prekey deviceID mismatch." });
|
|
748
1055
|
return;
|
|
749
1056
|
}
|
|
750
|
-
if (
|
|
1057
|
+
if (
|
|
1058
|
+
!(await verifyPreKeyWsSignature(preKey, device.signKey, "signed"))
|
|
1059
|
+
) {
|
|
751
1060
|
res.status(401).send({ error: "Prekey signature invalid." });
|
|
752
1061
|
return;
|
|
753
1062
|
}
|
|
@@ -796,7 +1105,11 @@ export const initApp = (
|
|
|
796
1105
|
return;
|
|
797
1106
|
}
|
|
798
1107
|
if (
|
|
799
|
-
!(await verifyPreKeyWsSignature(
|
|
1108
|
+
!(await verifyPreKeyWsSignature(
|
|
1109
|
+
submittedOTK,
|
|
1110
|
+
device.signKey,
|
|
1111
|
+
"one-time",
|
|
1112
|
+
))
|
|
800
1113
|
) {
|
|
801
1114
|
res.status(401).send({ error: "OTK signature invalid." });
|
|
802
1115
|
return;
|
|
@@ -809,6 +1122,18 @@ export const initApp = (
|
|
|
809
1122
|
|
|
810
1123
|
api.get("/emoji/:emojiID/details", protect, async (req, res) => {
|
|
811
1124
|
const emoji = await db.retrieveEmoji(getParam(req, "emojiID"));
|
|
1125
|
+
if (!emoji) {
|
|
1126
|
+
res.sendStatus(404);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
const permissions = await db.retrievePermissions(
|
|
1130
|
+
getUser(req).userID,
|
|
1131
|
+
"server",
|
|
1132
|
+
);
|
|
1133
|
+
if (!hasAnyPermission(permissions, emoji.owner)) {
|
|
1134
|
+
res.sendStatus(403);
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
812
1137
|
res.send(msgpack.encode(emoji));
|
|
813
1138
|
});
|
|
814
1139
|
|
|
@@ -818,6 +1143,19 @@ export const initApp = (
|
|
|
818
1143
|
res.sendStatus(400);
|
|
819
1144
|
return;
|
|
820
1145
|
}
|
|
1146
|
+
const emoji = await db.retrieveEmoji(safeId.data);
|
|
1147
|
+
if (!emoji) {
|
|
1148
|
+
res.sendStatus(404);
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
const permissions = await db.retrievePermissions(
|
|
1152
|
+
getUser(req).userID,
|
|
1153
|
+
"server",
|
|
1154
|
+
);
|
|
1155
|
+
if (!hasAnyPermission(permissions, emoji.owner)) {
|
|
1156
|
+
res.sendStatus(403);
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
821
1159
|
const filePath = "./emoji/" + safeId.data;
|
|
822
1160
|
const typeDetails = await fileTypeFromFile(filePath).catch(() => null);
|
|
823
1161
|
if (!typeDetails) {
|
|
@@ -835,92 +1173,106 @@ export const initApp = (
|
|
|
835
1173
|
stream.pipe(res);
|
|
836
1174
|
});
|
|
837
1175
|
|
|
838
|
-
api.post(
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1176
|
+
api.post(
|
|
1177
|
+
"/emoji/:serverID/json",
|
|
1178
|
+
uploadLimiter,
|
|
1179
|
+
protect,
|
|
1180
|
+
async (req, res) => {
|
|
1181
|
+
const parsedPayload = emojiPayload.safeParse(req.body);
|
|
1182
|
+
if (!parsedPayload.success) {
|
|
1183
|
+
res.status(400).json({
|
|
1184
|
+
error: "Invalid emoji payload",
|
|
1185
|
+
issues: parsedPayload.error.issues,
|
|
1186
|
+
});
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
const payload = parsedPayload.data;
|
|
848
1190
|
|
|
849
|
-
|
|
850
|
-
|
|
1191
|
+
const userDetails = getUser(req);
|
|
1192
|
+
const device = req.device;
|
|
851
1193
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1194
|
+
if (!device) {
|
|
1195
|
+
res.sendStatus(401);
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
856
1198
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1199
|
+
if (!payload.file) {
|
|
1200
|
+
res.sendStatus(400);
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
861
1203
|
|
|
862
|
-
|
|
863
|
-
|
|
1204
|
+
let buf: Buffer;
|
|
1205
|
+
try {
|
|
1206
|
+
buf = Buffer.from(XUtils.decodeBase64(payload.file));
|
|
1207
|
+
} catch {
|
|
1208
|
+
res.status(400).send({ error: "Emoji must be valid base64." });
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
const serverEntry = await db.retrieveServer(
|
|
1212
|
+
getParam(req, "serverID"),
|
|
1213
|
+
);
|
|
864
1214
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1215
|
+
const permissionList = await db.retrievePermissionsByResourceID(
|
|
1216
|
+
getParam(req, "serverID"),
|
|
1217
|
+
);
|
|
868
1218
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1219
|
+
if (
|
|
1220
|
+
!userHasPermission(
|
|
1221
|
+
permissionList,
|
|
1222
|
+
userDetails.userID,
|
|
1223
|
+
POWER_LEVELS.EMOJI,
|
|
1224
|
+
)
|
|
1225
|
+
) {
|
|
1226
|
+
res.sendStatus(401);
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
if (!serverEntry) {
|
|
1230
|
+
res.sendStatus(404);
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
if (!payload.name) {
|
|
1234
|
+
res.sendStatus(400);
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
if (Buffer.byteLength(buf) > 256000) {
|
|
1238
|
+
res.sendStatus(413);
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
891
1241
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1242
|
+
const mimeType = await fileTypeFromBuffer(buf);
|
|
1243
|
+
if (!ALLOWED_IMAGE_TYPES.includes(mimeType?.mime || "no/type")) {
|
|
1244
|
+
res.status(400).send({
|
|
1245
|
+
error:
|
|
1246
|
+
"Unsupported file type. Expected jpeg, png, gif, apng, or avif but received " +
|
|
1247
|
+
String(mimeType?.ext),
|
|
1248
|
+
});
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
901
1251
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1252
|
+
const emoji: Emoji = {
|
|
1253
|
+
emojiID: crypto.randomUUID(),
|
|
1254
|
+
name: payload.name,
|
|
1255
|
+
owner: getParam(req, "serverID"),
|
|
1256
|
+
};
|
|
907
1257
|
|
|
908
|
-
|
|
1258
|
+
await db.createEmoji(emoji);
|
|
909
1259
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1260
|
+
try {
|
|
1261
|
+
// write the file to disk
|
|
1262
|
+
await fsp.writeFile("emoji/" + emoji.emojiID, buf);
|
|
1263
|
+
res.send(msgpack.encode(emoji));
|
|
1264
|
+
} catch (_err: unknown) {
|
|
1265
|
+
// debugger: emoji write failed
|
|
1266
|
+
res.sendStatus(500);
|
|
1267
|
+
}
|
|
1268
|
+
},
|
|
1269
|
+
);
|
|
919
1270
|
|
|
920
1271
|
api.post(
|
|
921
1272
|
"/emoji/:serverID",
|
|
1273
|
+
uploadLimiter,
|
|
922
1274
|
protect,
|
|
923
|
-
|
|
1275
|
+
emojiUpload.single("emoji"),
|
|
924
1276
|
async (req, res) => {
|
|
925
1277
|
const parsedPayload = emojiPayload.safeParse(req.body);
|
|
926
1278
|
if (!parsedPayload.success) {
|
|
@@ -1017,6 +1369,7 @@ export const initApp = (
|
|
|
1017
1369
|
api.use(entitlementRouter);
|
|
1018
1370
|
api.use(passkeyRouter);
|
|
1019
1371
|
api.use(passkeyDeviceRouter);
|
|
1372
|
+
api.use(passwordRouter);
|
|
1020
1373
|
|
|
1021
1374
|
api.use("/user", userRouter);
|
|
1022
1375
|
|
|
@@ -1024,6 +1377,8 @@ export const initApp = (
|
|
|
1024
1377
|
|
|
1025
1378
|
api.use("/avatar", avatarRouter);
|
|
1026
1379
|
|
|
1380
|
+
api.use("/server-icon", serverIconRouter);
|
|
1381
|
+
|
|
1027
1382
|
api.use("/invite", inviteRouter);
|
|
1028
1383
|
|
|
1029
1384
|
setupDocs(api);
|