Add user ban/activate feature

Add admin endpoints to ban and activate users, block banned users
from all auth flows (OAuth, token login, bearer auth), and invalidate
existing sessions on next request. Includes frontend translation and
user detail page ban/activate buttons.
This commit is contained in:
tdurieux
2026-05-07 05:41:12 +03:00
parent 48256e743c
commit 8fc7ac5175
8 changed files with 77 additions and 2 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ export interface IUser {
page: string | null;
};
};
status?: "active" | "removed";
status?: "active" | "removed" | "banned";
dateOfEntry?: Date;
lastUpdated?: Date;
}
+36
View File
@@ -806,6 +806,42 @@ router.get(
}
}
);
router.post(
"/users/:username/ban",
async (req: express.Request, res: express.Response) => {
try {
const result = await UserModel.updateOne(
{ username: req.params.username },
{ $set: { status: "banned" } }
);
if (result.matchedCount === 0) {
throw new AnonymousError("user_not_found", { httpStatus: 404 });
}
res.json({ ok: true });
} catch (error) {
handleError(error, res, req);
}
}
);
router.post(
"/users/:username/activate",
async (req: express.Request, res: express.Response) => {
try {
const result = await UserModel.updateOne(
{ username: req.params.username },
{ $set: { status: "active" } }
);
if (result.matchedCount === 0) {
throw new AnonymousError("user_not_found", { httpStatus: 404 });
}
res.json({ ok: true });
} catch (error) {
handleError(error, res, req);
}
}
);
router.get("/conferences", async (req, res) => {
const page = parseInt(req.query.page as string) || 1;
const limit = Math.min(parseInt(req.query.limit as string) || 10, 1000);
+9
View File
@@ -92,6 +92,14 @@ const verify = async (
await user.save();
}
}
if (user!.status === "banned") {
done(
new AnonymousError("user_banned", {
httpStatus: 403,
})
);
return;
}
done(null, {
username: profile.username,
accessToken,
@@ -197,6 +205,7 @@ router.all(
"apiTokens.tokenHash": hashToken(token),
});
if (!model) return res.status(401).json({ error: "invalid_token" });
if (model.status === "banned") return res.status(403).json({ error: "user_banned" });
const synthUser = {
username: model.username,
accessToken: model.accessTokens?.github,
+13 -1
View File
@@ -216,7 +216,9 @@ export function handleError(
status = 401;
}
if (res && !res.headersSent) {
res.status(status).json({ error: errorCode });
const safeCode =
error instanceof AnonymousError ? errorCode : "internal_error";
res.status(status).json({ error: safeCode });
}
return;
}
@@ -244,5 +246,15 @@ export async function getUser(req: express.Request) {
if (!model) {
notConnected();
}
if (model.status === "banned") {
req.logout((error) => {
if (error) {
logger.error("logout failed", serializeError(error));
}
});
throw new AnonymousError("user_banned", {
httpStatus: 403,
});
}
return new User(model);
}
+1
View File
@@ -29,6 +29,7 @@ export async function bearerTokenAuth(
try {
const model = await UserModel.findOne({ "apiTokens.tokenHash": tokenHash });
if (!model) return next();
if (model.status === "banned") return next();
// Mirror the shape produced by passport's verify() in connection.ts
// so existing getUser()/route code works unchanged.