Add docs navigation and tighten API request security
This commit is contained in:
30
lib/api.ts
30
lib/api.ts
@@ -14,6 +14,8 @@ import {
|
||||
import { mediaUrl, mediaUrls } from "./catalog";
|
||||
import { ServiceError, checkServiceError } from "./service-error";
|
||||
import { requestOrigin, verifyOrigin } from "./request-origin";
|
||||
import { browserAccount } from "./request-security";
|
||||
import { jsonBody } from "./json-body";
|
||||
import { accountApi } from "./account-api";
|
||||
import { multiplayerApi } from "./multiplayer-api";
|
||||
import { bugReportApi } from "./bug-report-api";
|
||||
@@ -45,10 +47,11 @@ const uuid = (value: string) => idSchema.parse(value);
|
||||
function cookieSecret(request: NextRequest) {
|
||||
return request.cookies.get("mg_guest")?.value ?? "";
|
||||
}
|
||||
async function contributor(request: NextRequest) {
|
||||
async function contributor(request: NextRequest, browserOnly = false) {
|
||||
const token = request.headers.get("authorization")?.replace(/^Bearer /, "");
|
||||
let owner: string | undefined;
|
||||
if (token) {
|
||||
if (browserOnly) owner = (await browserAccount(request, user)).id;
|
||||
else if (token) {
|
||||
const row = required(
|
||||
await admin()
|
||||
.from("contributor_tokens")
|
||||
@@ -67,7 +70,7 @@ async function contributor(request: NextRequest) {
|
||||
return owner;
|
||||
}
|
||||
async function moderator(request: NextRequest, adminOnly = false) {
|
||||
const owner = await contributor(request);
|
||||
const owner = await contributor(request, true);
|
||||
const profile = required(
|
||||
await admin().from("profiles").select("role").eq("id", owner).single(),
|
||||
);
|
||||
@@ -112,23 +115,6 @@ async function session(request: NextRequest, id: string) {
|
||||
if (account && record.owner_id === account.id) return record;
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
async function jsonBody(request: NextRequest) {
|
||||
const reader = request.body?.getReader();
|
||||
if (!reader) throw new Error("JSON request required");
|
||||
const chunks: Uint8Array[] = [];
|
||||
let size = 0;
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
size += value.byteLength;
|
||||
if (size > 2 * 1024 * 1024) {
|
||||
await reader.cancel();
|
||||
throw new Error("Request too large");
|
||||
}
|
||||
chunks.push(value);
|
||||
}
|
||||
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
||||
}
|
||||
export async function handle(request: NextRequest, path: string[]) {
|
||||
try {
|
||||
verifyOrigin(request);
|
||||
@@ -802,7 +788,7 @@ export async function handle(request: NextRequest, path: string[]) {
|
||||
}
|
||||
}
|
||||
if (path[0] === "tokens") {
|
||||
const owner = await contributor(request);
|
||||
const owner = await contributor(request, true);
|
||||
if (method === "GET")
|
||||
return NextResponse.json({
|
||||
tokens: required(
|
||||
@@ -871,7 +857,7 @@ export async function handle(request: NextRequest, path: string[]) {
|
||||
});
|
||||
}
|
||||
if (method === "POST" && path[1] === "approve") {
|
||||
const owner = await contributor(request);
|
||||
const owner = await contributor(request, true);
|
||||
const code = z
|
||||
.string()
|
||||
.regex(/^[A-F0-9]{8}$/)
|
||||
|
||||
21
lib/json-body.ts
Normal file
21
lib/json-body.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { ServiceError } from "./service-error";
|
||||
|
||||
export const jsonBodyLimit = 2 * 1024 * 1024;
|
||||
|
||||
export async function jsonBody(request: Pick<Request, "body">) {
|
||||
const reader = request.body?.getReader();
|
||||
if (!reader) throw new ServiceError("JSON request required", 400);
|
||||
const chunks: Uint8Array[] = [];
|
||||
let size = 0;
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
size += value.byteLength;
|
||||
if (size > jsonBodyLimit) {
|
||||
await reader.cancel();
|
||||
throw new ServiceError("Request too large", 413);
|
||||
}
|
||||
chunks.push(value);
|
||||
}
|
||||
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { multiplayerAppearance } from "./multiplayer-appearance";
|
||||
import { mediaUrls } from "./catalog";
|
||||
import { requestOrigin } from "./request-origin";
|
||||
import { ServiceError } from "./service-error";
|
||||
import { limitRoomCreation } from "./request-security";
|
||||
import { scheduleDiscordCleanup } from "./discord-cleanup-scheduler";
|
||||
import {
|
||||
discordPartyReady,
|
||||
@@ -65,7 +66,7 @@ export async function multiplayerApi(
|
||||
if (!account)
|
||||
throw new ServiceError("Log in to create a multiplayer room", 401);
|
||||
const body = createRoomSchema.parse(await jsonBody(request));
|
||||
await limit("multiplayer:create:" + accessHash, 20, 3600);
|
||||
await limitRoomCreation(account.id, accessHash, limit);
|
||||
const profile = await accountProfile(account);
|
||||
const id = required(
|
||||
await client.rpc("create_map_room", {
|
||||
|
||||
23
lib/request-security.ts
Normal file
23
lib/request-security.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ServiceError } from "./service-error";
|
||||
|
||||
/** Device upload tokens cannot authorize staff actions or issue other credentials. */
|
||||
export async function browserAccount<T extends { id: string }>(
|
||||
request: Pick<Request, "headers">,
|
||||
getUser: () => Promise<T | null>,
|
||||
): Promise<T> {
|
||||
if (request.headers.has("authorization"))
|
||||
throw new ServiceError("Use your signed-in account for this action", 403);
|
||||
const account = await getUser();
|
||||
if (!account) throw new ServiceError("Sign in to continue", 401);
|
||||
return account;
|
||||
}
|
||||
|
||||
export async function limitRoomCreation(
|
||||
accountId: string,
|
||||
accessHash: string,
|
||||
limit: (key: string, count: number, seconds: number) => Promise<void>,
|
||||
) {
|
||||
// A browser cookie is replaceable. The verified account is the durable limit.
|
||||
await limit("multiplayer:create:account:" + accountId, 20, 3600);
|
||||
await limit("multiplayer:create:" + accessHash, 20, 3600);
|
||||
}
|
||||
Reference in New Issue
Block a user