diff --git a/src/admin/web/manage.ts b/src/admin/web/manage.ts index 587e63a..366d23d 100644 --- a/src/admin/web/manage.ts +++ b/src/admin/web/manage.ts @@ -165,8 +165,8 @@ router.post("/reactivate-user/:token", (req, res) => { userStore.upsertUser({ token: user.token, - disabledAt: 0, - disabledReason: "", + disabledAt: null, + disabledReason: null, }); return res.sendStatus(204); }); diff --git a/src/admin/web/views/admin_list-users.ejs b/src/admin/web/views/admin_list-users.ejs index 4acacb5..d259d2e 100644 --- a/src/admin/web/views/admin_list-users.ejs +++ b/src/admin/web/views/admin_list-users.ejs @@ -80,41 +80,8 @@ const state = localStorage.getItem("showNicknames") === "true"; document.getElementById("toggle-nicknames").checked = state; toggleNicknames(); - - document.querySelectorAll("td.actions a.ban").forEach(function (a) { - a.addEventListener("click", function (e) { - e.preventDefault(); - var token = a.getAttribute("data-token"); - if (confirm("Are you sure you want to ban this user?")) { - let reason = prompt("Reason for ban:"); - fetch( - "/admin/manage/disable-user/" + token, - { - method: "POST", - credentials: "same-origin", - body: JSON.stringify({ reason, _csrf: document.querySelector("meta[name=csrf-token]").getAttribute("content") }), - headers: { "Content-Type": "application/json" } - }).then(() => window.location.reload()); - } - }); - }); - - document.querySelectorAll("td.actions a.unban").forEach(function (a) { - a.addEventListener("click", function (e) { - e.preventDefault(); - var token = a.getAttribute("data-token"); - if (confirm("Are you sure you want to unban this user?")) { - fetch( - "/admin/manage/reactivate-user/" + token, - { - method: "POST", - credentials: "same-origin", - body: JSON.stringify({ _csrf: document.querySelector("meta[name=csrf-token]").getAttribute("content") }), - headers: { "Content-Type": "application/json" } - } - ).then(() => window.location.reload()); - } - }); - }); + +<%- include("partials/admin-ban-xhr-script") %> + <%- include("partials/admin-footer") %> diff --git a/src/admin/web/views/admin_view-user.ejs b/src/admin/web/views/admin_view-user.ejs index 6200da2..0412dd4 100644 --- a/src/admin/web/views/admin_view-user.ejs +++ b/src/admin/web/views/admin_view-user.ejs @@ -41,17 +41,35 @@ Disabled At - <%- user.disabledAt %> + <%- user.disabledAt %> + + <% if (user.disabledAt) { %> + 🔄️ + <% } else { %> + 🚫 + <% } %> + Disabled Reason - <%- user.disabledReason %> + <%- user.disabledReason %> + <% if (user.disabledAt) { %> + + ✏️ + + <% } %> + + + IP Address Limit + <%- (user.maxIps ?? maxIps) || "Unlimited" %> + + ✏️ + IPs - - <%- include("partials/shared_user_ip_list", { user, shouldRedact: false }) %> - + <%- include("partials/shared_user_ip_list", { user, shouldRedact: false }) %> <% if (user.type === "temporary") { %> @@ -69,9 +87,7 @@ -<% } %> -<%- include("partials/shared_quota-info", { quota, user }) %> - +<% } %> <%- include("partials/shared_quota-info", { quota, user }) %>

Back to User List

@@ -112,4 +128,4 @@ }); -<%- include("partials/admin-footer") %> +<%- include("partials/admin-ban-xhr-script") %> <%- include("partials/admin-footer") %> diff --git a/src/admin/web/views/partials/admin-ban-xhr-script.ejs b/src/admin/web/views/partials/admin-ban-xhr-script.ejs new file mode 100644 index 0000000..ef76450 --- /dev/null +++ b/src/admin/web/views/partials/admin-ban-xhr-script.ejs @@ -0,0 +1,32 @@ + diff --git a/src/shared/users/schema.ts b/src/shared/users/schema.ts index 27ee3e2..8339bc7 100644 --- a/src/shared/users/schema.ts +++ b/src/shared/users/schema.ts @@ -50,6 +50,8 @@ export const UserSchema = z disabledReason: z.string().optional(), /** Time at which the user will expire and be disabled (for temp users). */ expiresAt: z.number().optional(), + /** The user's maximum number of IP addresses; supercedes global max. */ + maxIps: z.coerce.number().int().min(0).optional(), }) .strict(); diff --git a/src/shared/users/user-store.ts b/src/shared/users/user-store.ts index e34d123..20f2750 100644 --- a/src/shared/users/user-store.ts +++ b/src/shared/users/user-store.ts @@ -17,8 +17,6 @@ import { User, UserUpdate } from "./schema"; const log = logger.child({ module: "users" }); -const MAX_IPS_PER_USER = config.maxIpsPerUser; - const users: Map = new Map(); const usersToFlush = new Set(); let quotaRefreshJob: schedule.Job | null = null; @@ -173,10 +171,10 @@ export function authenticate(token: string, ip: string) { const user = users.get(token); if (!user || user.disabledAt) return; if (!user.ip.includes(ip)) user.ip.push(ip); - - // If too many IPs are associated with the user, disable the account. + + const configIpLimit = user.maxIps ?? config.maxIpsPerUser; const ipLimit = - user.type === "special" || !MAX_IPS_PER_USER ? Infinity : MAX_IPS_PER_USER; + user.type === "special" || !configIpLimit ? Infinity : configIpLimit; if (user.ip.length > ipLimit) { disableUser(token, "IP address limit exceeded."); return; diff --git a/src/user/web/self-service.ts b/src/user/web/self-service.ts index f94f260..ca564c6 100644 --- a/src/user/web/self-service.ts +++ b/src/user/web/self-service.ts @@ -20,7 +20,12 @@ router.get("/", (_req, res) => { }); router.get("/lookup", (_req, res) => { - res.render("user_lookup", { user: res.locals.currentSelfServiceUser }); + const ipLimit = + (res.locals.currentSelfServiceUser?.maxIps ?? config.maxIpsPerUser) || 0; + res.render("user_lookup", { + user: res.locals.currentSelfServiceUser, + ipLimit, + }); }); router.post("/lookup", (req, res) => { diff --git a/src/user/web/views/user_lookup.ejs b/src/user/web/views/user_lookup.ejs index 7570709..4b75c87 100644 --- a/src/user/web/views/user_lookup.ejs +++ b/src/user/web/views/user_lookup.ejs @@ -51,7 +51,7 @@ <%- user.lastUsedAt || "never" %> - IPs<%- maxIps ? ` (max ${maxIps})` : "" %> + IPs<%- ipLimit ? ` (max ${ipLimit})` : "" %> <%- include("partials/shared_user_ip_list", { user, shouldRedact: true }) %> <% if (user.type === "temporary") { %>