Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/utils/db/logs.db.server.ts
T

211 lines
5.7 KiB
TypeScript

import { Tenant, Log, ApiKey } from "@hivegrid/db/prisma-browser";
import { FiltersDto } from "~/application/dtos/data/FiltersDto";
import { PaginationDto } from "~/application/dtos/data/PaginationDto";
import { DefaultLogActions } from "~/application/dtos/shared/DefaultLogActions";
import { db } from "../db.server";
import RowFiltersHelper from "../helpers/RowFiltersHelper";
import { getUserInfo } from "../session.server";
import { UserWithoutPassword } from "./users.db.server";
import { getRlsExecutionContext, runWithRlsContext } from "@hivegrid/db/rls-context.server";
export type LogWithDetails = Log & {
user: UserWithoutPassword | null;
apiKey: ApiKey | null;
tenant?: Tenant | null;
};
export type AdminUserLastActivity = Pick<Log, "id" | "userId" | "action" | "createdAt">;
const include = {
user: true,
tenant: true,
apiKey: true,
};
export async function getAllLogs(
pagination: { page: number; pageSize: number },
filters: FiltersDto
): Promise<{ items: LogWithDetails[]; pagination: PaginationDto }> {
const where = RowFiltersHelper.getFiltersCondition(filters);
const items = await db.log.findMany({
take: pagination.pageSize,
skip: pagination.pageSize * (pagination.page - 1),
include,
where,
});
const totalItems = await db.log.count({
where,
});
return {
items,
pagination: {
page: pagination.page,
pageSize: pagination.pageSize,
totalItems,
totalPages: Math.ceil(totalItems / pagination.pageSize),
},
};
}
export async function getLogs(
tenantId: string,
pagination: { page: number; pageSize: number },
filters: FiltersDto
): Promise<{ items: LogWithDetails[]; pagination: PaginationDto }> {
const where = RowFiltersHelper.getFiltersCondition(filters);
const items = await db.log.findMany({
take: pagination.pageSize,
skip: pagination.pageSize * (pagination.page - 1),
where: { tenantId, ...where },
include,
});
const totalItems = await db.log.count({
where: { tenantId, ...where },
});
return {
items,
pagination: {
page: pagination.page,
pageSize: pagination.pageSize,
totalItems,
totalPages: Math.ceil(totalItems / pagination.pageSize),
},
};
}
export async function createLog(request: Request, tenantId: string | null, action: string, details: string) {
const userInfo = await getUserInfo(request);
await withLogRlsContext(userInfo.userId, tenantId, () =>
db.log.create({
data: {
tenantId,
userId: userInfo.userId,
url: new URL(request.url).pathname,
action,
details,
},
}),
);
}
export async function createManualLog(userId: string, tenantId: string | null, pathname: string, action: string, details: string) {
await withLogRlsContext(userId, tenantId, () =>
db.log.create({
data: {
tenantId,
userId,
url: pathname,
action,
details,
},
}),
);
}
export async function createLogLogin(request: Request, user: UserWithoutPassword) {
// eslint-disable-next-line no-console
// console.log({ clientIpAddress: getClientIPAddress(request.headers) });
await withLogRlsContext(user.id, null, () =>
db.log.create({
data: {
userId: user.id,
url: "",
action: DefaultLogActions.Login,
details: "",
},
}),
);
}
export async function createLogLogout(request: Request, userId: string) {
await withLogRlsContext(userId, null, () =>
db.log.create({
data: {
userId,
url: "",
action: DefaultLogActions.Logout,
details: "",
},
}),
);
}
export async function createAdminLog(request: Request, action: string, details: string) {
const userInfo = await getUserInfo(request);
await withLogRlsContext(userInfo.userId, null, () =>
db.log.create({
data: {
userId: userInfo.userId,
url: new URL(request.url).pathname,
action,
details,
},
}),
);
}
export async function getLastUserLog(userId: string): Promise<Log | null> {
return await withLogRlsContext(userId, null, () =>
db.log.findFirst({
where: {
userId,
},
orderBy: {
createdAt: "desc",
},
}),
);
}
/**
* Returns the most recent log entry per user for the given userIds in a
* constant number of queries (1 groupBy + 1 findMany), avoiding the N+1
* pattern of calling `getLastUserLog` per user.
*/
export async function getLastUserLogsByUserIds(userIds: string[]): Promise<Map<string, AdminUserLastActivity>> {
if (userIds.length === 0) {
return new Map();
}
const groups = await db.log.groupBy({
by: ["userId"],
where: { userId: { in: userIds } },
_max: { createdAt: true },
});
const filters = groups
.map((group) => (group._max.createdAt ? { userId: group.userId, createdAt: group._max.createdAt } : null))
.filter((entry): entry is { userId: string; createdAt: Date } => entry !== null);
if (filters.length === 0) {
return new Map();
}
const logs = await db.log.findMany({
where: { OR: filters },
select: { id: true, userId: true, action: true, createdAt: true },
});
const byUserId = new Map<string, AdminUserLastActivity>();
for (const log of logs) {
const existing = byUserId.get(log.userId ?? "");
if (!log.userId) continue;
if (!existing || existing.createdAt < log.createdAt) {
byUserId.set(log.userId, log);
}
}
return byUserId;
}
function withLogRlsContext<T>(userId: string | undefined, tenantId: string | null, operation: () => T): T {
const current = getRlsExecutionContext();
if (current?.userId || current?.tenantId) {
return operation();
}
return runWithRlsContext(
{
...(userId ? { userId } : {}),
...(tenantId ? { tenantId } : {}),
},
operation,
);
}