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") { %>