diff --git a/public/i18n/locale-en.json b/public/i18n/locale-en.json index 0cbd056..671dfd4 100644 --- a/public/i18n/locale-en.json +++ b/public/i18n/locale-en.json @@ -8,6 +8,7 @@ "not_authorized": "You do not have permission to perform this action.", "unable_to_connect_user": "Unable to connect your account. Please try again later.", "user_not_found": "The requested user could not be found.", + "user_banned": "Your account has been banned. Contact the admin for more information.", "repo_access_limited": "GitHub blocked access because the repository's organization restricts third-party OAuth apps. Ask an org owner to approve Anonymous GitHub under Settings → Third-party Access → OAuth app policy, or anonymize a personal fork instead.", "repo_not_found": "The repository was not found on GitHub. Check the URL and spelling, make sure you are signed in to the account that can see it, and confirm the repo isn't hidden under an org that restricts third-party app access.", "repo_empty": "The selected branch has no commits on GitHub. Push at least one commit, or pick a different branch, then retry.", diff --git a/public/partials/admin/user.htm b/public/partials/admin/user.htm index 05874e4..09218fa 100644 --- a/public/partials/admin/user.htm +++ b/public/partials/admin/user.htm @@ -22,6 +22,10 @@ Admin +
+ + +
diff --git a/public/script/admin.js b/public/script/admin.js index 05b4ef0..463449b 100644 --- a/public/script/admin.js +++ b/public/script/admin.js @@ -519,6 +519,18 @@ angular getUser($routeParams.username); getUserRepositories($routeParams.username); + $scope.banUser = () => { + if (!confirm(`Ban user ${$routeParams.username}?`)) return; + $http + .post(`/api/admin/users/${$routeParams.username}/ban`) + .then(() => getUser($routeParams.username), (err) => console.error(err)); + }; + $scope.activateUser = () => { + $http + .post(`/api/admin/users/${$routeParams.username}/activate`) + .then(() => getUser($routeParams.username), (err) => console.error(err)); + }; + $scope.tokens = []; $scope.tokenForm = { name: "", plaintext: null }; diff --git a/src/core/model/users/users.types.ts b/src/core/model/users/users.types.ts index 1f5c820..31776e4 100644 --- a/src/core/model/users/users.types.ts +++ b/src/core/model/users/users.types.ts @@ -38,7 +38,7 @@ export interface IUser { page: string | null; }; }; - status?: "active" | "removed"; + status?: "active" | "removed" | "banned"; dateOfEntry?: Date; lastUpdated?: Date; } diff --git a/src/server/routes/admin.ts b/src/server/routes/admin.ts index 6498e1d..75199ae 100644 --- a/src/server/routes/admin.ts +++ b/src/server/routes/admin.ts @@ -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); diff --git a/src/server/routes/connection.ts b/src/server/routes/connection.ts index c66a031..39e5f06 100644 --- a/src/server/routes/connection.ts +++ b/src/server/routes/connection.ts @@ -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, diff --git a/src/server/routes/route-utils.ts b/src/server/routes/route-utils.ts index a31457a..e8a6f7d 100644 --- a/src/server/routes/route-utils.ts +++ b/src/server/routes/route-utils.ts @@ -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); } diff --git a/src/server/routes/token-auth.ts b/src/server/routes/token-auth.ts index 843434b..29a5520 100644 --- a/src/server/routes/token-auth.ts +++ b/src/server/routes/token-auth.ts @@ -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.