makes max IP limit configurable per-user

This commit is contained in:
nai-degen
2023-09-18 23:16:06 -05:00
parent 40e71435f0
commit c6453638e9
8 changed files with 74 additions and 54 deletions
+2 -2
View File
@@ -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);
});
+3 -36
View File
@@ -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());
}
});
});
</script>
<%- include("partials/admin-ban-xhr-script") %>
<%- include("partials/admin-footer") %>
+25 -9
View File
@@ -41,17 +41,35 @@
</tr>
<tr>
<th scope="row">Disabled At</th>
<td colspan="2"><%- user.disabledAt %></td>
<td><%- user.disabledAt %></td>
<td class="actions">
<% if (user.disabledAt) { %>
<a title="Unban" href="#" class="unban" data-token="<%= user.token %>">🔄️</a>
<% } else { %>
<a title="Ban" href="#" class="ban" data-token="<%= user.token %>">🚫</a>
<% } %>
</td>
</tr>
<tr>
<th scope="row">Disabled Reason</th>
<td colspan="2"><%- user.disabledReason %></td>
<td><%- user.disabledReason %></td>
<% if (user.disabledAt) { %>
<td class="actions">
<a title="Edit" id="edit-disabledReason" href="#" data-field="disabledReason"
data-token="<%= user.token %>">✏️</a>
</td>
<% } %>
</tr>
<tr>
<th scope="row">IP Address Limit</th>
<td><%- (user.maxIps ?? maxIps) || "Unlimited" %></td>
<td class="actions">
<a title="Edit" id="edit-maxIps" href="#" data-field="maxIps" data-token="<%= user.token %>">✏️</a>
</td>
</tr>
<tr>
<th scope="row">IPs</th>
<td colspan="2">
<%- include("partials/shared_user_ip_list", { user, shouldRedact: false }) %>
</td>
<td colspan="2"><%- include("partials/shared_user_ip_list", { user, shouldRedact: false }) %></td>
</tr>
<% if (user.type === "temporary") { %>
<tr>
@@ -69,9 +87,7 @@
<input type="hidden" name="_csrf" value="<%- csrfToken %>" />
<button type="submit" class="btn btn-primary">Refresh Quotas for User</button>
</form>
<% } %>
<%- include("partials/shared_quota-info", { quota, user }) %>
<% } %> <%- include("partials/shared_quota-info", { quota, user }) %>
<p><a href="/admin/manage/list-users">Back to User List</a></p>
@@ -112,4 +128,4 @@
});
</script>
<%- include("partials/admin-footer") %>
<%- include("partials/admin-ban-xhr-script") %> <%- include("partials/admin-footer") %>
@@ -0,0 +1,32 @@
<script>
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());
}
});
});
</script>
+2
View File
@@ -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();
+3 -5
View File
@@ -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<string, User> = new Map();
const usersToFlush = new Set<string>();
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;
+6 -1
View File
@@ -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) => {
+1 -1
View File
@@ -51,7 +51,7 @@
<td colspan="2"><%- user.lastUsedAt || "never" %></td>
</tr>
<tr>
<th scope="row">IPs<%- maxIps ? ` (max ${maxIps})` : "" %> </th>
<th scope="row">IPs<%- ipLimit ? ` (max ${ipLimit})` : "" %></th>
<td colspan="2"><%- include("partials/shared_user_ip_list", { user, shouldRedact: true }) %></td>
</tr>
<% if (user.type === "temporary") { %>