huge update.
fix issues with failing tests (I'm just gonna fucking assume directx isn't installed on the github actions machines). Added MUCH better RBAC support and control Fixed an issue when you have over a million clients RBAC would be slow asf Add settings page with some basic settings and configs you can change.
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
||||
authenticateUser,
|
||||
extractTokenFromRequest,
|
||||
generateToken,
|
||||
getSessionTtlSeconds,
|
||||
getUserFromRequest,
|
||||
revokeToken,
|
||||
} from "../../auth";
|
||||
@@ -62,6 +63,7 @@ export async function handleAuthRoutes(
|
||||
|
||||
if (user) {
|
||||
const token = await generateToken(user);
|
||||
const sessionTtlSeconds = getSessionTtlSeconds();
|
||||
|
||||
logger.info(
|
||||
`[auth] User ${user.username} logged in. must_change_password =`,
|
||||
@@ -93,7 +95,7 @@ export async function handleAuthRoutes(
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Set-Cookie": `overlord_token=${token}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=604800`,
|
||||
"Set-Cookie": `overlord_token=${token}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=${sessionTtlSeconds}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -7,12 +7,20 @@ import {
|
||||
clientExists,
|
||||
deleteClientRow,
|
||||
getClientIp,
|
||||
isIpBanned,
|
||||
listBannedIps,
|
||||
listClients,
|
||||
setOnlineState,
|
||||
unbanIp,
|
||||
} from "../../db";
|
||||
import { metrics } from "../../metrics";
|
||||
import { encodeMessage } from "../../protocol";
|
||||
import { requirePermission } from "../../rbac";
|
||||
import {
|
||||
canUserAccessClient,
|
||||
getUserClientAccessScope,
|
||||
listUserClientRuleIdsByAccess,
|
||||
} from "../../users";
|
||||
|
||||
type RequestIpProvider = {
|
||||
requestIP: (req: Request) => { address?: string } | null | undefined;
|
||||
@@ -43,7 +51,8 @@ export async function handleClientRoutes(
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/clients") {
|
||||
if (!(await authenticateRequest(req))) {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
const page = Math.max(1, Number(url.searchParams.get("page") || 1));
|
||||
@@ -52,10 +61,86 @@ export async function handleClientRoutes(
|
||||
const sort = url.searchParams.get("sort") || "last_seen_desc";
|
||||
const statusFilter = url.searchParams.get("status") || "all";
|
||||
const osFilter = url.searchParams.get("os") || "all";
|
||||
const result = listClients({ page, pageSize, search, sort, statusFilter, osFilter });
|
||||
if (user.role === "admin") {
|
||||
const result = listClients({ page, pageSize, search, sort, statusFilter, osFilter });
|
||||
return Response.json(result, { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
|
||||
const scope = getUserClientAccessScope(user.userId);
|
||||
if (scope === "none") {
|
||||
return Response.json(
|
||||
{ page, pageSize, total: 0, online: 0, items: [] },
|
||||
{ headers: deps.CORS_HEADERS },
|
||||
);
|
||||
}
|
||||
|
||||
const allowedClientIds =
|
||||
scope === "allowlist"
|
||||
? listUserClientRuleIdsByAccess(user.userId, "allow")
|
||||
: undefined;
|
||||
const deniedClientIds =
|
||||
scope === "denylist"
|
||||
? listUserClientRuleIdsByAccess(user.userId, "deny")
|
||||
: undefined;
|
||||
|
||||
const result = listClients({
|
||||
page,
|
||||
pageSize,
|
||||
search,
|
||||
sort,
|
||||
statusFilter,
|
||||
osFilter,
|
||||
allowedClientIds,
|
||||
deniedClientIds,
|
||||
});
|
||||
return Response.json(result, { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/clients/banned-ips") {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) return new Response("Unauthorized", { status: 401 });
|
||||
|
||||
try {
|
||||
requirePermission(user, "clients:control");
|
||||
} catch (error) {
|
||||
if (error instanceof Response) return error;
|
||||
return new Response("Forbidden", { status: 403 });
|
||||
}
|
||||
|
||||
if (req.method === "GET") {
|
||||
return Response.json({ items: listBannedIps() }, { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
|
||||
if (req.method === "DELETE") {
|
||||
const ipToUnban = (url.searchParams.get("ip") || "").trim();
|
||||
if (!ipToUnban) {
|
||||
return Response.json({ error: "Missing ip query parameter" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!/^[0-9a-fA-F:.]{3,64}$/.test(ipToUnban)) {
|
||||
return Response.json({ error: "Invalid IP format" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!isIpBanned(ipToUnban)) {
|
||||
return Response.json({ error: "IP is not banned" }, { status: 404 });
|
||||
}
|
||||
|
||||
unbanIp(ipToUnban);
|
||||
|
||||
const ip = server.requestIP(req)?.address || "unknown";
|
||||
logAudit({
|
||||
timestamp: Date.now(),
|
||||
username: user.username,
|
||||
ip,
|
||||
action: AuditAction.COMMAND,
|
||||
details: `Unbanned IP ${ipToUnban}`,
|
||||
success: true,
|
||||
});
|
||||
|
||||
return Response.json({ ok: true }, { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
}
|
||||
|
||||
const banMatch = url.pathname.match(/^\/api\/clients\/(.+)\/ban$/);
|
||||
if (req.method === "POST" && banMatch) {
|
||||
const user = await authenticateRequest(req);
|
||||
@@ -68,6 +153,9 @@ export async function handleClientRoutes(
|
||||
}
|
||||
|
||||
const targetId = banMatch[1];
|
||||
if (!canUserAccessClient(user.userId, user.role, targetId)) {
|
||||
return new Response("Forbidden: Client access denied", { status: 403 });
|
||||
}
|
||||
const target = clientManager.getClient(targetId);
|
||||
const targetIp = target?.ip || getClientIp(targetId);
|
||||
if (!targetIp) {
|
||||
@@ -99,10 +187,14 @@ export async function handleClientRoutes(
|
||||
|
||||
const thumbnailMatch = url.pathname.match(/^\/api\/clients\/(.+)\/thumbnail$/);
|
||||
if (req.method === "POST" && thumbnailMatch) {
|
||||
if (!(await authenticateRequest(req))) {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
const clientId = thumbnailMatch[1];
|
||||
if (!canUserAccessClient(user.userId, user.role, clientId)) {
|
||||
return new Response("Forbidden: Client access denied", { status: 403 });
|
||||
}
|
||||
const { generateThumbnail, markThumbnailRequested } = await import("../../thumbnails");
|
||||
markThumbnailRequested(clientId);
|
||||
const target = clientManager.getClient(clientId);
|
||||
@@ -135,6 +227,9 @@ export async function handleClientRoutes(
|
||||
}
|
||||
|
||||
const targetId = clientDeleteMatch[1];
|
||||
if (!canUserAccessClient(user.userId, user.role, targetId)) {
|
||||
return new Response("Forbidden: Client access denied", { status: 403 });
|
||||
}
|
||||
const target = clientManager.getClient(targetId);
|
||||
const existsInDb = clientExists(targetId);
|
||||
if (!target && !existsInDb) {
|
||||
@@ -181,6 +276,9 @@ export async function handleClientRoutes(
|
||||
}
|
||||
|
||||
const targetId = cmdMatch[1];
|
||||
if (!canUserAccessClient(user.userId, user.role, targetId)) {
|
||||
return new Response("Forbidden: Client access denied", { status: 403 });
|
||||
}
|
||||
const target = clientManager.getClient(targetId);
|
||||
const ip = server.requestIP(req)?.address || "unknown";
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { authenticateRequest } from "../../auth";
|
||||
import * as clientManager from "../../clientManager";
|
||||
import { getAuditLogs } from "../../auditLog";
|
||||
import { AuditAction, getAuditLogs, logAudit } from "../../auditLog";
|
||||
import { getConfig, updateSecurityConfig } from "../../config";
|
||||
import { listClients } from "../../db";
|
||||
import { logger } from "../../logger";
|
||||
import { metrics } from "../../metrics";
|
||||
@@ -9,6 +10,7 @@ import { requirePermission } from "../../rbac";
|
||||
type MiscRouteDeps = {
|
||||
CORS_HEADERS: Record<string, string>;
|
||||
SERVER_VERSION: string;
|
||||
requestIP?: (req: Request) => { address?: string } | null | undefined;
|
||||
getConsoleSessionCount: () => number;
|
||||
getRdSessionCount: () => number;
|
||||
getFileBrowserSessionCount: () => number;
|
||||
@@ -93,6 +95,53 @@ export async function handleMiscRoutes(
|
||||
});
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/settings/security") {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
if (user.role !== "admin") {
|
||||
return new Response("Forbidden: Admin access required", { status: 403 });
|
||||
}
|
||||
|
||||
if (req.method === "GET") {
|
||||
return Response.json({ security: getConfig().security }, { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
|
||||
if (req.method === "PUT") {
|
||||
let body: any = {};
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
return Response.json({ error: "Invalid JSON" }, { status: 400 });
|
||||
}
|
||||
|
||||
const updated = await updateSecurityConfig({
|
||||
sessionTtlHours: Number(body?.sessionTtlHours),
|
||||
loginMaxAttempts: Number(body?.loginMaxAttempts),
|
||||
loginWindowMinutes: Number(body?.loginWindowMinutes),
|
||||
loginLockoutMinutes: Number(body?.loginLockoutMinutes),
|
||||
passwordMinLength: Number(body?.passwordMinLength),
|
||||
passwordRequireUppercase: Boolean(body?.passwordRequireUppercase),
|
||||
passwordRequireLowercase: Boolean(body?.passwordRequireLowercase),
|
||||
passwordRequireNumber: Boolean(body?.passwordRequireNumber),
|
||||
passwordRequireSymbol: Boolean(body?.passwordRequireSymbol),
|
||||
});
|
||||
|
||||
logAudit({
|
||||
timestamp: Date.now(),
|
||||
username: user.username,
|
||||
ip: deps.requestIP?.(req)?.address || "unknown",
|
||||
action: AuditAction.COMMAND,
|
||||
details: "Updated security settings",
|
||||
success: true,
|
||||
});
|
||||
|
||||
return Response.json({ ok: true, security: updated }, { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
}
|
||||
|
||||
if (req.method === "GET" && url.pathname === "/api/audit-logs") {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { authenticateRequest } from "../../auth";
|
||||
import { requirePermission } from "../../rbac";
|
||||
import { getUserById } from "../../users";
|
||||
import { canUserAccessClient, getUserById, type UserRole } from "../../users";
|
||||
|
||||
type PageRouteDeps = {
|
||||
PUBLIC_ROOT: string;
|
||||
@@ -37,6 +37,11 @@ export async function handlePageRoutes(
|
||||
url: URL,
|
||||
deps: PageRouteDeps,
|
||||
): Promise<Response | null> {
|
||||
const canAccessClientPage = (userId: number, role: UserRole, clientId: string): boolean => {
|
||||
if (!clientId) return false;
|
||||
return canUserAccessClient(userId, role, clientId);
|
||||
};
|
||||
|
||||
if (req.method === "GET" && (url.pathname === "/" || url.pathname === "/index.html")) {
|
||||
const authed = await authenticateRequest(req);
|
||||
|
||||
@@ -71,6 +76,12 @@ export async function handlePageRoutes(
|
||||
if (user.role === "viewer") {
|
||||
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
|
||||
}
|
||||
|
||||
const clientId = (url.searchParams.get("clientId") || "").trim();
|
||||
if (!canAccessClientPage(user.userId, user.role, clientId)) {
|
||||
return new Response("Forbidden: Client access denied", { status: 403 });
|
||||
}
|
||||
|
||||
const file = Bun.file(`${deps.PUBLIC_ROOT}/remotedesktop.html`);
|
||||
if (await file.exists()) {
|
||||
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("remotedesktop.html")) });
|
||||
@@ -89,6 +100,12 @@ export async function handlePageRoutes(
|
||||
if (user.role === "viewer") {
|
||||
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
|
||||
}
|
||||
|
||||
const clientId = (url.searchParams.get("clientId") || "").trim();
|
||||
if (!canAccessClientPage(user.userId, user.role, clientId)) {
|
||||
return new Response("Forbidden: Client access denied", { status: 403 });
|
||||
}
|
||||
|
||||
const file = Bun.file(`${deps.PUBLIC_ROOT}/hvnc.html`);
|
||||
if (await file.exists()) {
|
||||
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("hvnc.html")) });
|
||||
@@ -168,6 +185,42 @@ export async function handlePageRoutes(
|
||||
}
|
||||
}
|
||||
|
||||
if (req.method === "GET" && url.pathname === "/user-client-access") {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) {
|
||||
return serveLoginOrUnauthorized(deps);
|
||||
}
|
||||
|
||||
const maybeChange = await serveChangePasswordIfRequired(deps, user.userId);
|
||||
if (maybeChange) return maybeChange;
|
||||
|
||||
if (user.role !== "admin") {
|
||||
return new Response("Forbidden: Admin access required", { status: 403 });
|
||||
}
|
||||
|
||||
const file = Bun.file(`${deps.PUBLIC_ROOT}/user-client-access.html`);
|
||||
if (await file.exists()) {
|
||||
return new Response(file, {
|
||||
headers: deps.secureHeaders(deps.mimeType("user-client-access.html")),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (req.method === "GET" && url.pathname === "/settings") {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) {
|
||||
return serveLoginOrUnauthorized(deps);
|
||||
}
|
||||
|
||||
const maybeChange = await serveChangePasswordIfRequired(deps, user.userId);
|
||||
if (maybeChange) return maybeChange;
|
||||
|
||||
const file = Bun.file(`${deps.PUBLIC_ROOT}/settings.html`);
|
||||
if (await file.exists()) {
|
||||
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("settings.html")) });
|
||||
}
|
||||
}
|
||||
|
||||
if (req.method === "GET" && url.pathname === "/build") {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) {
|
||||
@@ -210,6 +263,9 @@ export async function handlePageRoutes(
|
||||
if (user.role === "viewer") {
|
||||
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
|
||||
}
|
||||
if (!canAccessClientPage(user.userId, user.role, consolePageMatch[1])) {
|
||||
return new Response("Forbidden: Client access denied", { status: 403 });
|
||||
}
|
||||
const file = Bun.file(`${deps.PUBLIC_ROOT}/console.html`);
|
||||
if (await file.exists()) {
|
||||
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("console.html")) });
|
||||
@@ -226,6 +282,9 @@ export async function handlePageRoutes(
|
||||
if (user.role === "viewer") {
|
||||
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
|
||||
}
|
||||
if (!canAccessClientPage(user.userId, user.role, filesPageMatch[1])) {
|
||||
return new Response("Forbidden: Client access denied", { status: 403 });
|
||||
}
|
||||
const file = Bun.file(`${deps.PUBLIC_ROOT}/filebrowser.html`);
|
||||
if (await file.exists()) {
|
||||
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("filebrowser.html")) });
|
||||
@@ -242,6 +301,9 @@ export async function handlePageRoutes(
|
||||
if (user.role === "viewer") {
|
||||
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
|
||||
}
|
||||
if (!canAccessClientPage(user.userId, user.role, processesPageMatch[1])) {
|
||||
return new Response("Forbidden: Client access denied", { status: 403 });
|
||||
}
|
||||
const file = Bun.file(`${deps.PUBLIC_ROOT}/processes.html`);
|
||||
if (await file.exists()) {
|
||||
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("processes.html")) });
|
||||
@@ -258,6 +320,9 @@ export async function handlePageRoutes(
|
||||
if (user.role === "viewer") {
|
||||
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
|
||||
}
|
||||
if (!canAccessClientPage(user.userId, user.role, keyloggerPageMatch[1])) {
|
||||
return new Response("Forbidden: Client access denied", { status: 403 });
|
||||
}
|
||||
const file = Bun.file(`${deps.PUBLIC_ROOT}/keylogger.html`);
|
||||
if (await file.exists()) {
|
||||
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("keylogger.html")) });
|
||||
@@ -274,6 +339,9 @@ export async function handlePageRoutes(
|
||||
if (user.role === "viewer") {
|
||||
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
|
||||
}
|
||||
if (!canAccessClientPage(user.userId, user.role, proxyPageMatch[1])) {
|
||||
return new Response("Forbidden: Client access denied", { status: 403 });
|
||||
}
|
||||
const file = Bun.file(`${deps.PUBLIC_ROOT}/proxy.html`);
|
||||
if (await file.exists()) {
|
||||
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("proxy.html")) });
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { generateToken, authenticateRequest } from "../../auth";
|
||||
import { generateToken, authenticateRequest, getSessionTtlSeconds } from "../../auth";
|
||||
import { AuditAction, logAudit } from "../../auditLog";
|
||||
import { logger } from "../../logger";
|
||||
import { requirePermission } from "../../rbac";
|
||||
import {
|
||||
type ClientAccessRuleKind,
|
||||
type ClientAccessScope,
|
||||
createUser,
|
||||
deleteUser,
|
||||
getUserById,
|
||||
getUserClientAccessScope,
|
||||
listUsers,
|
||||
listUserClientAccessRules,
|
||||
removeUserClientAccessRule,
|
||||
setUserClientAccessRule,
|
||||
setUserClientAccessScope,
|
||||
updateUserPassword,
|
||||
updateUserRole,
|
||||
} from "../../users";
|
||||
@@ -114,6 +121,7 @@ export async function handleUsersRoutes(
|
||||
|
||||
if (user.userId === userId && targetUser) {
|
||||
const newToken = await generateToken(targetUser);
|
||||
const sessionTtlSeconds = getSessionTtlSeconds();
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
@@ -122,7 +130,7 @@ export async function handleUsersRoutes(
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Set-Cookie": `overlord_token=${newToken}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=604800`,
|
||||
"Set-Cookie": `overlord_token=${newToken}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=${sessionTtlSeconds}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -166,6 +174,118 @@ export async function handleUsersRoutes(
|
||||
return Response.json({ error: result.error }, { status: 400 });
|
||||
}
|
||||
|
||||
if (req.method === "GET" && url.pathname.match(/^\/api\/users\/\d+\/client-access$/)) {
|
||||
const authedUser = requirePermission(user, "users:manage");
|
||||
const userId = parseInt(url.pathname.split("/")[3]);
|
||||
const targetUser = getUserById(userId);
|
||||
if (!targetUser) {
|
||||
return Response.json({ error: "User not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const scope = getUserClientAccessScope(userId);
|
||||
const rules = listUserClientAccessRules(userId);
|
||||
|
||||
const ip = server.requestIP(req)?.address || "unknown";
|
||||
logAudit({
|
||||
timestamp: Date.now(),
|
||||
username: authedUser.username,
|
||||
ip,
|
||||
action: AuditAction.COMMAND,
|
||||
details: `Viewed client access policy for user: ${targetUser.username}`,
|
||||
success: true,
|
||||
});
|
||||
|
||||
return Response.json({ scope, rules });
|
||||
}
|
||||
|
||||
if (req.method === "PUT" && url.pathname.match(/^\/api\/users\/\d+\/client-access$/)) {
|
||||
const authedUser = requirePermission(user, "users:manage");
|
||||
const userId = parseInt(url.pathname.split("/")[3]);
|
||||
const targetUser = getUserById(userId);
|
||||
if (!targetUser) {
|
||||
return Response.json({ error: "User not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
const scope = body?.scope as ClientAccessScope;
|
||||
const result = setUserClientAccessScope(userId, scope);
|
||||
if (!result.success) {
|
||||
return Response.json({ error: result.error }, { status: 400 });
|
||||
}
|
||||
|
||||
const ip = server.requestIP(req)?.address || "unknown";
|
||||
logAudit({
|
||||
timestamp: Date.now(),
|
||||
username: authedUser.username,
|
||||
ip,
|
||||
action: AuditAction.COMMAND,
|
||||
details: `Updated client access scope for ${targetUser.username} to ${scope}`,
|
||||
success: true,
|
||||
});
|
||||
|
||||
return Response.json({ success: true, scope });
|
||||
}
|
||||
|
||||
if (req.method === "POST" && url.pathname.match(/^\/api\/users\/\d+\/client-access\/rules$/)) {
|
||||
const authedUser = requirePermission(user, "users:manage");
|
||||
const userId = parseInt(url.pathname.split("/")[3]);
|
||||
const targetUser = getUserById(userId);
|
||||
if (!targetUser) {
|
||||
return Response.json({ error: "User not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
const clientId = String(body?.clientId || "").trim();
|
||||
const access = body?.access as ClientAccessRuleKind;
|
||||
const result = setUserClientAccessRule(userId, clientId, access);
|
||||
if (!result.success) {
|
||||
return Response.json({ error: result.error }, { status: 400 });
|
||||
}
|
||||
|
||||
const ip = server.requestIP(req)?.address || "unknown";
|
||||
logAudit({
|
||||
timestamp: Date.now(),
|
||||
username: authedUser.username,
|
||||
ip,
|
||||
action: AuditAction.COMMAND,
|
||||
details: `Set client access rule for ${targetUser.username}: ${access} ${clientId}`,
|
||||
success: true,
|
||||
});
|
||||
|
||||
return Response.json({ success: true });
|
||||
}
|
||||
|
||||
if (req.method === "DELETE" && url.pathname.match(/^\/api\/users\/\d+\/client-access\/rules$/)) {
|
||||
const authedUser = requirePermission(user, "users:manage");
|
||||
const userId = parseInt(url.pathname.split("/")[3]);
|
||||
const targetUser = getUserById(userId);
|
||||
if (!targetUser) {
|
||||
return Response.json({ error: "User not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const clientId = String(url.searchParams.get("clientId") || "").trim();
|
||||
if (!clientId) {
|
||||
return Response.json({ error: "clientId is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const result = removeUserClientAccessRule(userId, clientId);
|
||||
if (!result.success) {
|
||||
return Response.json({ error: result.error }, { status: 400 });
|
||||
}
|
||||
|
||||
const ip = server.requestIP(req)?.address || "unknown";
|
||||
logAudit({
|
||||
timestamp: Date.now(),
|
||||
username: authedUser.username,
|
||||
ip,
|
||||
action: AuditAction.COMMAND,
|
||||
details: `Removed client access rule for ${targetUser.username}: ${clientId}`,
|
||||
success: true,
|
||||
});
|
||||
|
||||
return Response.json({ success: true });
|
||||
}
|
||||
|
||||
if (req.method === "DELETE" && url.pathname.match(/^\/api\/users\/\d+$/)) {
|
||||
const authedUser = requirePermission(user, "users:manage");
|
||||
const userId = parseInt(url.pathname.split("/")[3]);
|
||||
|
||||
Reference in New Issue
Block a user