211 lines
5.7 KiB
TypeScript
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,
|
|
);
|
|
}
|