Restrict features

This commit is contained in:
Owen
2025-10-24 16:29:37 -07:00
parent 10a5af67aa
commit 4b40e7b8d6
16 changed files with 622 additions and 85 deletions

View File

@@ -29,13 +29,13 @@ export const orgs = pgTable("orgs", {
createdAt: text("createdAt"),
settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever
.notNull()
.default(15),
.default(7),
settingsLogRetentionDaysAccess: integer("settingsLogRetentionDaysAccess")
.notNull()
.default(15),
.default(0),
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction")
.notNull()
.default(15),
.default(0)
});
export const orgDomains = pgTable("orgDomains", {

View File

@@ -22,13 +22,13 @@ export const orgs = sqliteTable("orgs", {
createdAt: text("createdAt"),
settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever
.notNull()
.default(15),
.default(7),
settingsLogRetentionDaysAccess: integer("settingsLogRetentionDaysAccess")
.notNull()
.default(15),
.default(0),
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction")
.notNull()
.default(15)
.default(0)
});
export const userDomains = sqliteTable("userDomains", {

View File

@@ -5,6 +5,7 @@ import { runSetupFunctions } from "./setup";
import { createApiServer } from "./apiServer";
import { createNextServer } from "./nextServer";
import { createInternalServer } from "./internalServer";
import { createIntegrationApiServer } from "./integrationApiServer";
import {
ApiKey,
ApiKeyOrg,
@@ -13,13 +14,13 @@ import {
User,
UserOrg
} from "@server/db";
import { createIntegrationApiServer } from "./integrationApiServer";
import config from "@server/lib/config";
import { setHostMeta } from "@server/lib/hostMeta";
import { initTelemetryClient } from "./lib/telemetry.js";
import { TraefikConfigManager } from "./lib/traefik/TraefikConfigManager.js";
import { initTelemetryClient } from "@server/lib/telemetry";
import { TraefikConfigManager } from "@server/lib/traefik/TraefikConfigManager";
import { initCleanup } from "#dynamic/cleanup";
import license from "#dynamic/license/license";
import { initLogCleanupInterval } from "@server/lib/cleanupLogs";
async function startServers() {
await setHostMeta();
@@ -33,6 +34,8 @@ async function startServers() {
initTelemetryClient();
initLogCleanupInterval();
// Start all servers
const apiServer = createApiServer();
const internalServer = createInternalServer();

62
server/lib/cleanupLogs.ts Normal file
View File

@@ -0,0 +1,62 @@
import { db, orgs } from "@server/db";
import { cleanUpOldLogs as cleanUpOldAccessLogs } from "@server/private/lib/logAccessAudit";
import { cleanUpOldLogs as cleanUpOldActionLogs } from "@server/private/middlewares/logActionAudit";
import { cleanUpOldLogs as cleanUpOldRequestLogs } from "@server/routers/badger/logRequestAudit";
import { gt, or } from "drizzle-orm";
export function initLogCleanupInterval() {
return setInterval(
async () => {
const orgsToClean = await db
.select({
orgId: orgs.orgId,
settingsLogRetentionDaysAction:
orgs.settingsLogRetentionDaysAction,
settingsLogRetentionDaysAccess:
orgs.settingsLogRetentionDaysAccess,
settingsLogRetentionDaysRequest:
orgs.settingsLogRetentionDaysRequest
})
.from(orgs)
.where(
or(
gt(orgs.settingsLogRetentionDaysAction, 0),
gt(orgs.settingsLogRetentionDaysAccess, 0),
gt(orgs.settingsLogRetentionDaysRequest, 0)
)
);
for (const org of orgsToClean) {
const {
orgId,
settingsLogRetentionDaysAction,
settingsLogRetentionDaysAccess,
settingsLogRetentionDaysRequest
} = org;
if (settingsLogRetentionDaysAction > 0) {
await cleanUpOldActionLogs(
orgId,
settingsLogRetentionDaysRequest
);
}
if (settingsLogRetentionDaysAccess > 0) {
await cleanUpOldAccessLogs(
orgId,
settingsLogRetentionDaysRequest
);
}
if (settingsLogRetentionDaysRequest > 0) {
await cleanUpOldRequestLogs(
orgId,
settingsLogRetentionDaysRequest
);
}
}
},
// 3 * 60 * 60 * 1000
60 * 1000 // for testing
); // every 3 hours
}

View File

@@ -1,7 +1,8 @@
import { accessAuditLog, db, orgs } from "@server/db";
import { getCountryCodeForIp } from "@server/lib/geoip";
import logger from "@server/logger";
import { eq } from "drizzle-orm";
import { and, eq, lt } from "drizzle-orm";
import cache from "@server/lib/cache";
async function getAccessDays(orgId: string): Promise<number> {
// check cache first
@@ -23,11 +24,38 @@ async function getAccessDays(orgId: string): Promise<number> {
}
// store the result in cache
cache.set(`org_${orgId}_accessDays`, org.settingsLogRetentionDaysAction);
cache.set(
`org_${orgId}_accessDays`,
org.settingsLogRetentionDaysAction,
300
);
return org.settingsLogRetentionDaysAction;
}
export async function cleanUpOldLogs(orgId: string, retentionDays: number) {
const now = Math.floor(Date.now() / 1000);
const cutoffTimestamp = now - retentionDays * 24 * 60 * 60;
try {
const deleteResult = await db
.delete(accessAuditLog)
.where(
and(
lt(accessAuditLog.timestamp, cutoffTimestamp),
eq(accessAuditLog.orgId, orgId)
)
);
logger.info(
`Cleaned up ${deleteResult.changes} access audit logs older than ${retentionDays} days`
);
} catch (error) {
logger.error("Error cleaning up old action audit logs:", error);
}
}
export async function logAccessAudit(data: {
action: boolean;
type: string;
@@ -40,6 +68,12 @@ export async function logAccessAudit(data: {
requestIp?: string;
}) {
try {
const retentionDays = await getAccessDays(data.orgId);
if (retentionDays === 0) {
// do not log
return;
}
let actorType: string | undefined;
let actor: string | undefined;
let actorId: string | undefined;

View File

@@ -17,10 +17,9 @@ import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import { Request, Response, NextFunction } from "express";
import createHttpError from "http-errors";
import NodeCache from "node-cache";
import { eq } from "drizzle-orm";
import { and, eq, lt } from "drizzle-orm";
import cache from "@server/lib/cache";
const cache = new NodeCache({ stdTTL: 300 }); // cache for 5 minutes
async function getActionDays(orgId: string): Promise<number> {
// check cache first
const cached = cache.get<number>(`org_${orgId}_actionDays`);
@@ -41,11 +40,34 @@ async function getActionDays(orgId: string): Promise<number> {
}
// store the result in cache
cache.set(`org_${orgId}_actionDays`, org.settingsLogRetentionDaysAction);
cache.set(`org_${orgId}_actionDays`, org.settingsLogRetentionDaysAction, 300);
return org.settingsLogRetentionDaysAction;
}
export async function cleanUpOldLogs(orgId: string, retentionDays: number) {
const now = Math.floor(Date.now() / 1000);
const cutoffTimestamp = now - retentionDays * 24 * 60 * 60;
try {
const deleteResult = await db
.delete(actionAuditLog)
.where(
and(
lt(actionAuditLog.timestamp, cutoffTimestamp),
eq(actionAuditLog.orgId, orgId)
)
);
logger.info(
`Cleaned up ${deleteResult.changes} action audit logs older than ${retentionDays} days`
);
} catch (error) {
logger.error("Error cleaning up old action audit logs:", error);
}
}
export function logActionAudit(action: ActionsEnum) {
return async function (
req: Request,

View File

@@ -1,7 +1,7 @@
import { db, orgs, requestAuditLog } from "@server/db";
import logger from "@server/logger";
import { eq } from "drizzle-orm";
import NodeCache from "node-cache";
import { and, eq, lt } from "drizzle-orm";
import cache from "@server/lib/cache";
/**
@@ -24,7 +24,6 @@ Reasons:
*/
const cache = new NodeCache({ stdTTL: 300 }); // cache for 5 minutes
async function getRetentionDays(orgId: string): Promise<number> {
// check cache first
const cached = cache.get<number>(`org_${orgId}_retentionDays`);
@@ -34,7 +33,8 @@ async function getRetentionDays(orgId: string): Promise<number> {
const [org] = await db
.select({
settingsLogRetentionDaysRequest: orgs.settingsLogRetentionDaysRequest
settingsLogRetentionDaysRequest:
orgs.settingsLogRetentionDaysRequest
})
.from(orgs)
.where(eq(orgs.orgId, orgId))
@@ -45,11 +45,38 @@ async function getRetentionDays(orgId: string): Promise<number> {
}
// store the result in cache
cache.set(`org_${orgId}_retentionDays`, org.settingsLogRetentionDaysRequest);
cache.set(
`org_${orgId}_retentionDays`,
org.settingsLogRetentionDaysRequest,
300
);
return org.settingsLogRetentionDaysRequest;
}
export async function cleanUpOldLogs(orgId: string, retentionDays: number) {
const now = Math.floor(Date.now() / 1000);
const cutoffTimestamp = now - retentionDays * 24 * 60 * 60;
try {
const deleteResult = await db
.delete(requestAuditLog)
.where(
and(
lt(requestAuditLog.timestamp, cutoffTimestamp),
eq(requestAuditLog.orgId, orgId)
)
);
logger.info(
`Cleaned up ${deleteResult.changes} request audit logs older than ${retentionDays} days`
);
} catch (error) {
logger.error("Error cleaning up old request audit logs:", error);
}
}
export async function logRequestAudit(
data: {
action: boolean;
@@ -76,7 +103,6 @@ export async function logRequestAudit(
}
) {
try {
if (data.orgId) {
const retentionDays = await getRetentionDays(data.orgId);
if (retentionDays === 0) {

View File

@@ -49,13 +49,13 @@ export async function getOrg(
const { orgId } = parsedParams.data;
const org = await db
const [org] = await db
.select()
.from(orgs)
.where(eq(orgs.orgId, orgId))
.limit(1);
if (org.length === 0) {
if (!org) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
@@ -66,7 +66,7 @@ export async function getOrg(
return response<GetOrgResponse>(res, {
data: {
org: org[0]
org
},
success: true,
error: false,

View File

@@ -18,7 +18,10 @@ const updateOrgParamsSchema = z
const updateOrgBodySchema = z
.object({
name: z.string().min(1).max(255).optional()
name: z.string().min(1).max(255).optional(),
settingsLogRetentionDaysRequest: z.number().min(-1).optional(),
settingsLogRetentionDaysAccess: z.number().min(-1).optional(),
settingsLogRetentionDaysAction: z.number().min(-1).optional()
})
.strict()
.refine((data) => Object.keys(data).length > 0, {
@@ -74,7 +77,7 @@ export async function updateOrg(
const updatedOrg = await db
.update(orgs)
.set({
name: parsedBody.data.name
...parsedBody.data
})
.where(eq(orgs.orgId, orgId))
.returning();