tons of server side fixes
This commit is contained in:
@@ -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 AutoScript = {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
hashTokenForSession,
|
||||
persistRevokedTokenHash,
|
||||
getSessionById,
|
||||
deleteInactiveSessions,
|
||||
} from "../../db";
|
||||
import { logger } from "../../logger";
|
||||
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$/)) {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { authenticateRequest } from "../../auth";
|
||||
import { AuditAction, logAudit } from "../../auditLog";
|
||||
import * as buildManager from "../../build/buildManager";
|
||||
import * as clientManager from "../../clientManager";
|
||||
import { deleteBuild, getAllBuilds, getBuild } from "../../db";
|
||||
import { deleteBuild, getAllBuilds, getBuild, countBuildsForUser, getOldestBuildForUser } from "../../db";
|
||||
import { encodeMessage } from "../../protocol";
|
||||
import { metrics } from "../../metrics";
|
||||
import { requirePermission } from "../../rbac";
|
||||
@@ -180,6 +180,30 @@ export async function handleBuildRoutes(
|
||||
const buildId = uuidv4();
|
||||
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({
|
||||
timestamp: Date.now(),
|
||||
username: user.username,
|
||||
@@ -324,7 +348,10 @@ export async function handleBuildRoutes(
|
||||
if (req.method === "GET" && url.pathname === "/api/build/list") {
|
||||
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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,18 @@ import { logAudit, AuditAction } from "../../auditLog";
|
||||
import * as clientManager from "../../clientManager";
|
||||
import { setOnlineState } from "../../db";
|
||||
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): {
|
||||
allowedClientIds?: string[];
|
||||
@@ -118,6 +130,8 @@ export async function handleEnrollmentRoutes(
|
||||
|
||||
setClientEnrollmentStatus(clientId, "approved", user.username);
|
||||
|
||||
grantClientAccess(user.userId, user.role, clientId);
|
||||
|
||||
_postApproveHook?.(clientId);
|
||||
|
||||
logAudit({
|
||||
@@ -261,6 +275,7 @@ export async function handleEnrollmentRoutes(
|
||||
if (ok) {
|
||||
updated++;
|
||||
if (action === "approve") {
|
||||
grantClientAccess(user.userId, user.role, id);
|
||||
_postApproveHook?.(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { authenticateRequest } from "../../auth";
|
||||
import { AuditAction, getAuditLogs, logAudit } from "../../auditLog";
|
||||
import { getConfig, updateSecurityConfig, updateTlsConfig, updateAppearanceConfig, getExportableConfig, importFullConfig } from "../../config";
|
||||
import { getClientMetricsSummary } from "../../db";
|
||||
import { getClientMetricsSummary, getClientMetricsSummaryForUser } from "../../db";
|
||||
import { metrics } from "../../metrics";
|
||||
import { requirePermission } from "../../rbac";
|
||||
import { getUserTelegramChatId, setUserTelegramChatId, getUserClientAccessScope, listUserClientRuleIdsByAccess } from "../../users";
|
||||
@@ -40,7 +40,9 @@ export async function handleMiscRoutes(
|
||||
|
||||
const snapshot = metrics.getSnapshot();
|
||||
|
||||
const clientSummary = getClientMetricsSummary();
|
||||
const clientSummary = user.role === "admin"
|
||||
? getClientMetricsSummary()
|
||||
: getClientMetricsSummaryForUser(user.userId);
|
||||
snapshot.clients.total = clientSummary.total;
|
||||
snapshot.clients.online = clientSummary.online;
|
||||
snapshot.clients.offline = clientSummary.total - clientSummary.online;
|
||||
|
||||
Reference in New Issue
Block a user