diff --git a/server/src/external/mongo/indexes.ts b/server/src/external/mongo/indexes.ts index c3717106a..49d715f17 100644 --- a/server/src/external/mongo/indexes.ts +++ b/server/src/external/mongo/indexes.ts @@ -54,6 +54,7 @@ const staticIndexes: Partial> = { ], users: [ index({ id: 1 }, UNIQUE), + index({ id: 1, authLevel: 1 }, UNIQUE), index({ username: 1 }, UNIQUE), index({ usernameLowercase: 1 }, UNIQUE), ], diff --git a/server/src/server/middleware/auth.test.ts b/server/src/server/middleware/auth.test.ts index 30313e60f..910d93b85 100644 --- a/server/src/server/middleware/auth.test.ts +++ b/server/src/server/middleware/auth.test.ts @@ -2,6 +2,7 @@ import expMiddlewareMock from "express-request-mock"; import db from "external/mongo/db"; import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi"; import t from "tap"; +import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; import { AllPermissions, SetRequestPermissions } from "./auth"; @@ -119,3 +120,27 @@ t.test("#SetRequestPermissions", (t) => { t.end(); }); + +t.test("#RejectIfBanned", (t) => { + t.test("Should stop banned users from doing anything.", async (t) => { + await db.users.update({ id: 1 }, { $set: { authLevel: 0 } }); + + await db["api-tokens"].insert({ + userID: 1, + identifier: "Mock API Token", + permissions: { + customise_profile: true, + }, + token: "mock_token", + fromAPIClient: null, + }); + + const res = await mockApi.get("/api/v1/status").set("Authorization", "Bearer mock_token"); + + t.equal(res.statusCode, 403, "Should return 403 on benign endpoints."); + + t.end(); + }); + + t.end(); +}); diff --git a/server/src/server/middleware/auth.ts b/server/src/server/middleware/auth.ts index 7f82cbb0c..d3bc0f4aa 100644 --- a/server/src/server/middleware/auth.ts +++ b/server/src/server/middleware/auth.ts @@ -2,8 +2,9 @@ import { RequestHandler } from "express"; import db from "external/mongo/db"; import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi"; import { SplitAuthorizationHeader } from "utils/misc"; -import { APITokenDocument, APIPermissions } from "tachi-common"; +import { APITokenDocument, APIPermissions, UserAuthLevels } from "tachi-common"; import CreateLogCtx from "lib/logger/logger"; +import { TachiConfig } from "lib/setup/config"; const logger = CreateLogCtx(__filename); @@ -186,3 +187,21 @@ const CreateRequireNotGuest = export const RequireNotGuest: RequestHandler = CreateRequireNotGuest("description"); export const FervidexStyleRequireNotGuest: RequestHandler = CreateRequireNotGuest("error"); + +export const RejectIfBanned: RequestHandler = async (req, res, next) => { + if (req[SYMBOL_TachiAPIAuth].userID) { + const isBanned = await db.users.findOne({ + id: req[SYMBOL_TachiAPIAuth].userID!, + authLevel: UserAuthLevels.BANNED, + }); + + if (isBanned) { + return res.status(403).json({ + success: false, + description: `You are banned from ${TachiConfig.NAME}`, + }); + } + } + + next(); +}; diff --git a/server/src/server/middleware/request-logger.ts b/server/src/server/middleware/request-logger.ts index 6800a7883..657bf4ea5 100644 --- a/server/src/server/middleware/request-logger.ts +++ b/server/src/server/middleware/request-logger.ts @@ -1,6 +1,7 @@ import CreateLogCtx from "lib/logger/logger"; import { RequestHandler, Response } from "express-serve-static-core"; import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi"; +import { TachiConfig } from "lib/setup/config"; const logger = CreateLogCtx(__filename); @@ -60,6 +61,10 @@ export const RequestLoggerMiddleware: RequestHandler = (req, res, next) => { return; } + if (contents.body?.description === `You are banned from ${TachiConfig.NAME}.`) { + return; + } + if (res.statusCode < 400 || res.statusCode === 404) { logger.verbose( `(${req.method} ${req.originalUrl}) Returned ${res.statusCode}.`, diff --git a/server/src/server/router/router.ts b/server/src/server/router/router.ts index 10088693c..c9c6d04d4 100644 --- a/server/src/server/router/router.ts +++ b/server/src/server/router/router.ts @@ -1,6 +1,6 @@ import { Router } from "express"; import { UpdateLastSeen } from "server/middleware/update-last-seen"; -import { SetRequestPermissions } from "../middleware/auth"; +import { RejectIfBanned, SetRequestPermissions } from "../middleware/auth"; import { NormalRateLimitMiddleware } from "../middleware/rate-limiter"; import apiRouterV1 from "./api/v1/router"; import irRouter from "./ir/router"; @@ -13,6 +13,7 @@ router.use("/ir", NormalRateLimitMiddleware, irRouter); // but also may require custom authentication. router.use(SetRequestPermissions); router.use(UpdateLastSeen); +router.use(RejectIfBanned); router.use("/api/v1", apiRouterV1); diff --git a/server/src/server/server.ts b/server/src/server/server.ts index 4f62cd32d..f951a45a2 100644 --- a/server/src/server/server.ts +++ b/server/src/server/server.ts @@ -8,6 +8,7 @@ import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import { Environment, ServerConfig, TachiConfig } from "lib/setup/config"; import { integer } from "tachi-common"; +import { RejectIfBanned } from "./middleware/auth"; import { RequestLoggerMiddleware } from "./middleware/request-logger"; import mainRouter from "./router/router"; @@ -96,6 +97,8 @@ process.on("unhandledRejection", (reason, promise) => { app.use(express.json({ limit: "4mb" })); app.use((req, res, next) => { + // Always mount an empty req body. We operate under the assumption that req.body is + // always defined. if (req.method !== "GET" && !req.body) { req.body = {}; }