adds optional user_token nicknames
This commit is contained in:
@@ -45,6 +45,7 @@ export function paginate(set: unknown[], page: number, pageSize: number = 20) {
|
||||
export const UserSchema = z
|
||||
.object({
|
||||
ip: z.array(z.string()).optional(),
|
||||
nickname: z.string().max(80).optional(),
|
||||
type: z.enum(["normal", "special"]).optional(),
|
||||
promptCount: z.number().optional(),
|
||||
tokenCount: z.any().optional(), // never used, but remains for compatibility
|
||||
|
||||
+5
-1
@@ -8,7 +8,11 @@ const { generateToken, doubleCsrfProtection } = doubleCsrf({
|
||||
getSecret: () => CSRF_SECRET,
|
||||
cookieName: "csrf",
|
||||
cookieOptions: { sameSite: "strict", path: "/" },
|
||||
getTokenFromRequest: (req) => req.body["_csrf"] || req.query["_csrf"],
|
||||
getTokenFromRequest: (req) => {
|
||||
const val = req.body["_csrf"] || req.query["_csrf"];
|
||||
delete req.body["_csrf"];
|
||||
return val;
|
||||
},
|
||||
});
|
||||
|
||||
const injectCsrfToken: express.RequestHandler = (req, res, next) => {
|
||||
|
||||
+2
-1
@@ -37,7 +37,8 @@ adminRouter.use(
|
||||
} else if (err.name === "ForbiddenError") {
|
||||
data.status = 403;
|
||||
if (err.message === "invalid csrf token") {
|
||||
data.message = "Invalid CSRF token; try refreshing the previous page before submitting again.";
|
||||
data.message =
|
||||
"Invalid CSRF token; try refreshing the previous page before submitting again.";
|
||||
}
|
||||
return res.status(403).render("admin/error", { ...data, flash: null });
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ type QuotaModel = "claude" | "turbo" | "gpt4";
|
||||
export interface User {
|
||||
/** The user's personal access token. */
|
||||
token: string;
|
||||
/** The user's nickname. */
|
||||
nickname?: string;
|
||||
/** The IP addresses the user has connected from. */
|
||||
ip: string[];
|
||||
/** The user's privilege level. */
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
border-collapse: collapse;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
table.striped tr:nth-child(even) {
|
||||
background-color: #eaeaea
|
||||
}
|
||||
table td, table th {
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.25em 0.5em;
|
||||
@@ -35,6 +38,7 @@
|
||||
}
|
||||
td.actions {
|
||||
padding: 0;
|
||||
width: 0;
|
||||
text-align: center;
|
||||
}
|
||||
td.actions a {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<%- include("../_partials/admin-header", { title: "Error" }) %>
|
||||
<div style="color: red; background-color: #eedddd; padding: 1em">
|
||||
<div id="error-content" style="color: red; background-color: #eedddd; padding: 1em">
|
||||
<p><strong>⚠️ Error <%= status %>:</strong> <%= message %></p>
|
||||
<pre><%= stack %></pre>
|
||||
<a href="#" onclick="window.history.back()">Go Back</a> | <a href="/admin">Go Home</a>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
</p>
|
||||
<ul>
|
||||
<li><code>token</code> (required): a unique identifier for the user</li>
|
||||
<li><code>nickname</code> (optional): a nickname for the user, max 80 chars</li>
|
||||
<li><code>ip</code> (optional): IP addresses the user has connected from</li>
|
||||
<li>
|
||||
<code>type</code> (optional): either <code>normal</code> or
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<%- include("../_partials/admin-header", { title: "Users - OAI Reverse Proxy Admin" }) %>
|
||||
<h1>User Token List</h1>
|
||||
|
||||
<!-- <input type="hidden" name="_csrf" value="<%= csrfToken %>" /> -->
|
||||
<% if (users.length === 0) { %>
|
||||
<p>No users found.</p>
|
||||
<% } else { %>
|
||||
<input type="checkbox" id="toggle-nicknames" onchange="toggleNicknames()" />
|
||||
<label for="toggle-nicknames">Show Nicknames</label>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User Token</th>
|
||||
<th>User</th>
|
||||
<th <% if (sort.includes("ip")) { %>class="active"<% } %> ><a href="/admin/manage/list-users?sort=ip">IPs</a></th>
|
||||
<th <% if (sort.includes("promptCount")) { %>class="active"<% } %> ><a href="/admin/manage/list-users?sort=promptCount">Prompts</a></th>
|
||||
<th <% if (sort.includes("sumTokenCounts")) { %>class="active"<% } %> ><a href="/admin/manage/list-users?sort=sumTokenCounts">Tokens</a></th>
|
||||
@@ -22,7 +23,14 @@
|
||||
<% users.forEach(function(user){ %>
|
||||
<tr>
|
||||
<td>
|
||||
<code><a href="/admin/manage/view-user/<%= user.token %>"><%= user.token %></a></code>
|
||||
<a href="/admin/manage/view-user/<%= user.token %>">
|
||||
<code class="usertoken"><%= user.token %></code>
|
||||
<% if (user.nickname) { %>
|
||||
<span class="nickname" style="display: none"><%= user.nickname %></span>
|
||||
<% } else { %>
|
||||
<code class="nickname" style="display: none"><%= "..." + user.token.slice(-5) %></code>
|
||||
<% } %>
|
||||
</a>
|
||||
</td>
|
||||
<td><%= user.ip.length %></td>
|
||||
<td><%= user.promptCount %></td>
|
||||
@@ -41,7 +49,6 @@
|
||||
</tr>
|
||||
<% }); %>
|
||||
</table>
|
||||
|
||||
<ul class="pagination">
|
||||
<% if (page > 1) { %>
|
||||
<li><a href="/admin/manage/list-users?sort=<%= sort %>&page=<%= page - 1 %>">«</a></li>
|
||||
@@ -57,6 +64,23 @@
|
||||
<% } %>
|
||||
|
||||
<script>
|
||||
function toggleNicknames() {
|
||||
const checked = document.getElementById("toggle-nicknames").checked;
|
||||
const visibleSelector = checked ? ".nickname" : ".usertoken";
|
||||
const hiddenSelector = checked ? ".usertoken" : ".nickname";
|
||||
document.querySelectorAll(visibleSelector).forEach(function (el) {
|
||||
el.style.display = "inline";
|
||||
});
|
||||
document.querySelectorAll(hiddenSelector).forEach(function (el) {
|
||||
el.style.display = "none";
|
||||
});
|
||||
localStorage.setItem("showNicknames", checked);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -1,31 +1,43 @@
|
||||
<%- include("../_partials/admin-header", { title: "View User - OAI Reverse Proxy Admin" }) %>
|
||||
<h1>View User</h1>
|
||||
|
||||
<table class="table table-striped">
|
||||
<table class="striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Key</th>
|
||||
<th scope="col">Value</th>
|
||||
<th scope="col" colspan="2">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">Token</th>
|
||||
<td><%- user.token %></td>
|
||||
<td colspan="2"><%- user.token %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Nickname</th>
|
||||
<td><%- user.nickname ?? "none" %></td>
|
||||
<td class="actions">
|
||||
<a
|
||||
title="Edit"
|
||||
id="edit-nickname"
|
||||
href="#"
|
||||
data-field="nickname"
|
||||
data-token="<%= user.token %>"
|
||||
>✏️</a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">Type</th>
|
||||
<td><%- user.type %></td>
|
||||
<td colspan="2"><%- user.type %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Prompt Count</th>
|
||||
<td><%- user.promptCount %></td>
|
||||
<td colspan="2"><%- user.promptCount %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Token Counts</th>
|
||||
<td>
|
||||
<td colspan="2">
|
||||
<ul style="padding-left: 1em; margin: 0">
|
||||
<% Object.entries(user.tokenCounts).forEach(([key, count]) => { %>
|
||||
<li><strong><%- key %></strong>: <%- count %></li>
|
||||
@@ -35,7 +47,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Token Limits</th>
|
||||
<td>
|
||||
<td colspan="2">
|
||||
<ul style="padding-left: 1em; margin: 0">
|
||||
<% Object.entries(user.tokenLimits).forEach(([key, count]) => { %>
|
||||
<li><strong><%- key %></strong>: <%- count %></li>
|
||||
@@ -45,23 +57,23 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Created At</th>
|
||||
<td><%- user.createdAt %></td>
|
||||
<td colspan="2"><%- user.createdAt %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Last Used At</th>
|
||||
<td><%- user.lastUsedAt || "never" %></td>
|
||||
<td colspan="2"><%- user.lastUsedAt || "never" %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Disabled At</th>
|
||||
<td><%- user.disabledAt %></td>
|
||||
<td colspan="2"><%- user.disabledAt %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Disabled Reason</th>
|
||||
<td><%- user.disabledReason %></td>
|
||||
<td colspan="2"><%- user.disabledReason %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">IPs</th>
|
||||
<td>
|
||||
<td colspan="2">
|
||||
<a href="#" id="ip-list-toggle">Show all (<%- user.ip.length %>)</a>
|
||||
<ol id="ip-list" style="display: none; padding-left: 1em; margin: 0">
|
||||
<% user.ip.forEach((ip) => { %>
|
||||
@@ -81,12 +93,48 @@
|
||||
</form>
|
||||
<% } %>
|
||||
|
||||
<p><a href="/admin/manage/list-users">Back to User List</a></p>
|
||||
|
||||
<script>
|
||||
document.getElementById("ip-list-toggle").addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
document.getElementById("ip-list").style.display = "block";
|
||||
document.getElementById("ip-list-toggle").style.display = "none";
|
||||
});
|
||||
|
||||
document.querySelectorAll("td.actions a[data-field]").forEach(function (a) {
|
||||
a.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
const token = a.dataset.token;
|
||||
const field = a.dataset.field;
|
||||
const value = prompt(`Enter new value for '${field}'':`);
|
||||
if (value !== null) {
|
||||
fetch(`/admin/manage/edit-user/${token}`, {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
body: JSON.stringify({
|
||||
[field]: value,
|
||||
_csrf: document
|
||||
.querySelector("meta[name=csrf-token]")
|
||||
.getAttribute("content"),
|
||||
}),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
.then((res) => Promise.all([res.ok, res.text()]))
|
||||
.then(([ok, text]) => {
|
||||
if (!ok) {
|
||||
document.body.innerHTML = text;
|
||||
return;
|
||||
}
|
||||
const url = new URL(window.location.href);
|
||||
const params = new URLSearchParams();
|
||||
params.set("flash", `success: User's ${field} updated.`);
|
||||
url.search = params.toString();
|
||||
window.location.assign(url);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<%- include("../_partials/admin-footer") %>
|
||||
|
||||
Reference in New Issue
Block a user