155 lines
4.7 KiB
TypeScript
155 lines
4.7 KiB
TypeScript
import type { AdminPermissionName } from "@hivegrid/db/admin-permissions";
|
|
import { adminHasPermission } from "@hivegrid/db/admin-rbac.server";
|
|
import { writeAdminSalesAudit } from "@hivegrid/db/admin-sales-core.server";
|
|
|
|
import { runAsAdminIdentity } from "~/services/admin/admin-identity.server";
|
|
import {
|
|
extractWebActorIp,
|
|
extractWebActorUserAgent,
|
|
extractWebRequestId,
|
|
} from "~/utils/audit/withAuditContext";
|
|
import { db } from "~/utils/db.server";
|
|
import { NOT_FOUND_ENVELOPE } from "~/utils/errors.server";
|
|
|
|
export async function runAsAdminBookingOperator<T>(
|
|
request: Request,
|
|
permission:
|
|
| Extract<
|
|
AdminPermissionName,
|
|
| "admin_sales.booking.manage"
|
|
| "admin_sales.communication.read_all"
|
|
>
|
|
| readonly Extract<
|
|
AdminPermissionName,
|
|
| "admin_sales.booking.manage"
|
|
| "admin_sales.communication.read_all"
|
|
>[],
|
|
operation: (context: {
|
|
readonly adminUserId: string;
|
|
readonly email: string;
|
|
readonly canReadAllCommunication: boolean;
|
|
readonly actor: (reason: string) => {
|
|
readonly adminUserId: string;
|
|
readonly email: string;
|
|
readonly reason: string;
|
|
readonly requestId: string | null;
|
|
readonly ip: string | null;
|
|
readonly userAgent: string | null;
|
|
};
|
|
}) => Promise<T>
|
|
): Promise<T> {
|
|
return runAsAdminIdentity(request, permission, async (session) => {
|
|
const admin = await db.adminUser.findFirst({
|
|
where: { id: session.adminUserId, status: "active" },
|
|
select: { id: true, email: true },
|
|
});
|
|
if (!admin) {
|
|
throw Response.json(NOT_FOUND_ENVELOPE, { status: 404 });
|
|
}
|
|
const canReadAllCommunication = await adminHasPermission(
|
|
admin.id,
|
|
"admin_sales.communication.read_all",
|
|
db
|
|
);
|
|
const actorBase = {
|
|
adminUserId: admin.id,
|
|
email: admin.email,
|
|
requestId: extractWebRequestId(request),
|
|
ip: extractWebActorIp(request),
|
|
userAgent: extractWebActorUserAgent(request),
|
|
};
|
|
return operation({
|
|
adminUserId: admin.id,
|
|
email: admin.email,
|
|
canReadAllCommunication,
|
|
actor: (reason) => ({ ...actorBase, reason }),
|
|
});
|
|
});
|
|
}
|
|
|
|
export async function readAdminBookingWorkspace(context: {
|
|
readonly adminUserId: string;
|
|
readonly canReadAllCommunication: boolean;
|
|
readonly actor: (reason: string) => {
|
|
readonly adminUserId: string;
|
|
readonly email: string;
|
|
readonly reason: string;
|
|
readonly requestId: string | null;
|
|
readonly ip: string | null;
|
|
readonly userAgent: string | null;
|
|
};
|
|
}) {
|
|
const bookingWhere = context.canReadAllCommunication
|
|
? {}
|
|
: {
|
|
OR: [
|
|
{ adminUserId: context.adminUserId },
|
|
{
|
|
meetingType: {
|
|
hostAdminUserIds: { has: context.adminUserId },
|
|
},
|
|
},
|
|
],
|
|
};
|
|
const [meetingTypes, pages, bookings, connectedCalendars] =
|
|
await Promise.all([
|
|
db.adminMeetingType.findMany({
|
|
where: { archivedAt: null },
|
|
orderBy: [{ active: "desc" }, { name: "asc" }, { id: "asc" }],
|
|
}),
|
|
db.adminBookingPage.findMany({
|
|
where: { archivedAt: null },
|
|
orderBy: [
|
|
{ published: "desc" },
|
|
{ title: "asc" },
|
|
{ id: "asc" },
|
|
],
|
|
include: { meetingType: { select: { name: true } } },
|
|
}),
|
|
db.adminBooking.findMany({
|
|
where: bookingWhere,
|
|
orderBy: [{ startsAt: "asc" }, { id: "asc" }],
|
|
take: 200,
|
|
include: {
|
|
contact: {
|
|
select: { firstName: true, lastName: true },
|
|
},
|
|
meetingType: { select: { name: true } },
|
|
},
|
|
}),
|
|
db.adminCalendarConnection.count({
|
|
where: {
|
|
adminUserId: context.adminUserId,
|
|
archivedAt: null,
|
|
status: "active",
|
|
},
|
|
}),
|
|
]);
|
|
const foreignBookings = bookings.filter(
|
|
(booking) => booking.adminUserId !== context.adminUserId
|
|
);
|
|
if (foreignBookings.length > 0) {
|
|
await writeAdminSalesAudit(db, {
|
|
actor: context.actor("Fremde Team-Buchungen geöffnet"),
|
|
action: "admin.sales.booking.foreignBookings.read",
|
|
targetType: "AdminBooking",
|
|
targetId: foreignBookings[0]!.id,
|
|
after: {
|
|
foreignTargets: foreignBookings.map((booking) => ({
|
|
targetId: booking.id,
|
|
dataOwnerAdminUserId: booking.adminUserId,
|
|
})),
|
|
accessKind: "read",
|
|
},
|
|
});
|
|
}
|
|
return { meetingTypes, pages, bookings, connectedCalendars };
|
|
}
|
|
|
|
export function adminBookingOrigin(request: Request): string {
|
|
const configured = process.env.ADMIN_BOOKING_PUBLIC_ORIGIN?.trim();
|
|
if (configured) return configured.replace(/\/$/u, "");
|
|
const url = new URL(request.url);
|
|
return `${url.protocol}//meet.localhost:3000`;
|
|
}
|