mirror of
https://github.com/fosrl/pangolin.git
synced 2025-12-16 13:06:27 +00:00
171 lines
4.8 KiB
TypeScript
171 lines
4.8 KiB
TypeScript
/*
|
|
* This file is part of a proprietary work.
|
|
*
|
|
* Copyright (c) 2025 Fossorial, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This file is licensed under the Fossorial Commercial License.
|
|
* You may not use this file except in compliance with the License.
|
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
*
|
|
* This file is not licensed under the AGPLv3.
|
|
*/
|
|
|
|
import { accessAuditLog, db, orgs } from "@server/db";
|
|
import { getCountryCodeForIp } from "@server/lib/geoip";
|
|
import logger from "@server/logger";
|
|
import { and, eq, lt } from "drizzle-orm";
|
|
import cache from "@server/lib/cache";
|
|
|
|
async function getAccessDays(orgId: string): Promise<number> {
|
|
// check cache first
|
|
const cached = cache.get<number>(`org_${orgId}_accessDays`);
|
|
if (cached !== undefined) {
|
|
return cached;
|
|
}
|
|
|
|
const [org] = await db
|
|
.select({
|
|
settingsLogRetentionDaysAction: orgs.settingsLogRetentionDaysAction
|
|
})
|
|
.from(orgs)
|
|
.where(eq(orgs.orgId, orgId))
|
|
.limit(1);
|
|
|
|
if (!org) {
|
|
return 0;
|
|
}
|
|
|
|
// store the result in cache
|
|
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 {
|
|
await db
|
|
.delete(accessAuditLog)
|
|
.where(
|
|
and(
|
|
lt(accessAuditLog.timestamp, cutoffTimestamp),
|
|
eq(accessAuditLog.orgId, orgId)
|
|
)
|
|
);
|
|
|
|
logger.debug(
|
|
`Cleaned up 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;
|
|
orgId: string;
|
|
resourceId?: number;
|
|
user?: { username: string; userId: string };
|
|
apiKey?: { name: string | null; apiKeyId: string };
|
|
metadata?: any;
|
|
userAgent?: string;
|
|
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;
|
|
|
|
const user = data.user;
|
|
if (user) {
|
|
actorType = "user";
|
|
actor = user.username;
|
|
actorId = user.userId;
|
|
}
|
|
const apiKey = data.apiKey;
|
|
if (apiKey) {
|
|
actorType = "apiKey";
|
|
actor = apiKey.name || apiKey.apiKeyId;
|
|
actorId = apiKey.apiKeyId;
|
|
}
|
|
|
|
// if (!actorType || !actor || !actorId) {
|
|
// logger.warn("logRequestAudit: Incomplete actor information");
|
|
// return;
|
|
// }
|
|
|
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
|
|
let metadata = null;
|
|
if (metadata) {
|
|
metadata = JSON.stringify(metadata);
|
|
}
|
|
|
|
const clientIp = data.requestIp
|
|
? (() => {
|
|
if (
|
|
data.requestIp.startsWith("[") &&
|
|
data.requestIp.includes("]")
|
|
) {
|
|
// if brackets are found, extract the IPv6 address from between the brackets
|
|
const ipv6Match = data.requestIp.match(/\[(.*?)\]/);
|
|
if (ipv6Match) {
|
|
return ipv6Match[1];
|
|
}
|
|
}
|
|
return data.requestIp;
|
|
})()
|
|
: undefined;
|
|
|
|
const countryCode = data.requestIp
|
|
? await getCountryCodeFromIp(data.requestIp)
|
|
: undefined;
|
|
|
|
await db.insert(accessAuditLog).values({
|
|
timestamp: timestamp,
|
|
orgId: data.orgId,
|
|
actorType,
|
|
actor,
|
|
actorId,
|
|
action: data.action,
|
|
type: data.type,
|
|
metadata,
|
|
resourceId: data.resourceId,
|
|
userAgent: data.userAgent,
|
|
ip: clientIp,
|
|
location: countryCode
|
|
});
|
|
} catch (error) {
|
|
logger.error(error);
|
|
}
|
|
}
|
|
|
|
async function getCountryCodeFromIp(ip: string): Promise<string | undefined> {
|
|
const geoIpCacheKey = `geoip_access:${ip}`;
|
|
|
|
let cachedCountryCode: string | undefined = cache.get(geoIpCacheKey);
|
|
|
|
if (!cachedCountryCode) {
|
|
cachedCountryCode = await getCountryCodeForIp(ip); // do it locally
|
|
// Cache for longer since IP geolocation doesn't change frequently
|
|
cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes
|
|
}
|
|
|
|
return cachedCountryCode;
|
|
}
|