Files
EpicNext-Cms/src/features/housekeeping/domains/people/inbox.ts
T

245 lines
7.1 KiB
TypeScript

import "server-only";
import { PERMS } from "@/lib/permission-slugs";
import { authorizeHousekeeping } from "../../foundation/authorization";
import { satisfiesCapability } from "../../foundation/capability-context";
import {
anyCapability,
type CapabilityRequirement,
fail,
type HousekeepingCapabilityContext,
type HousekeepingInboxSource,
type HousekeepingWorkItem,
ok,
} from "../../foundation/contracts";
import { peopleModerationQuery } from "./queries/moderation";
import { peopleSupportQuery } from "./queries/support";
export const PEOPLE_INBOX_SOURCE_IDS = [
"people.tickets",
"people.help-tickets",
"people.cfh",
"people.active-bans",
] as const;
export interface PeopleInboxAdapters {
tickets(
context: HousekeepingCapabilityContext,
signal: AbortSignal,
): Promise<readonly HousekeepingWorkItem[]>;
helpTickets(
context: HousekeepingCapabilityContext,
signal: AbortSignal,
): Promise<readonly HousekeepingWorkItem[]>;
cfh(
context: HousekeepingCapabilityContext,
signal: AbortSignal,
): Promise<readonly HousekeepingWorkItem[]>;
activeBans(
context: HousekeepingCapabilityContext,
signal: AbortSignal,
): Promise<readonly HousekeepingWorkItem[]>;
}
function safeHref(href: string): boolean {
return (
href.startsWith("/ase/") &&
!href.includes("\\") &&
!Array.from(href).some((character) => {
const code = character.codePointAt(0) ?? 0;
return code < 32 || code === 127;
})
);
}
function createSource(
id: (typeof PEOPLE_INBOX_SOURCE_IDS)[number],
capability: CapabilityRequirement,
load: PeopleInboxAdapters[keyof PeopleInboxAdapters],
): HousekeepingInboxSource {
return {
id,
owner: "people",
capability,
async getItems(context, signal) {
const authorization = authorizeHousekeeping(context, capability);
if (!authorization.ok) return authorization;
try {
const items = await load(context, signal);
return ok(
{
availability: "available" as const,
items: items
.filter(
(item) =>
safeHref(item.href) &&
satisfiesCapability(context, item.capability),
)
.slice(0, 25),
},
authorization.correlationId,
);
} catch {
return fail(
"DEPENDENCY_UNAVAILABLE",
"errors.housekeeping.dependencyUnavailable",
authorization.correlationId,
);
}
},
};
}
export function createPeopleInboxSources(
adapters: PeopleInboxAdapters,
): readonly HousekeepingInboxSource[] {
return [
createSource(
"people.tickets",
anyCapability(PERMS.TICKETS_VIEW, PERMS.MOD_TICKETS_VIEW),
adapters.tickets,
),
createSource(
"people.help-tickets",
anyCapability(PERMS.TICKETS_VIEW, PERMS.MOD_TICKETS_VIEW),
adapters.helpTickets,
),
createSource(
"people.cfh",
anyCapability(PERMS.MODERATION_VIEW, PERMS.MOD_CFH_VIEW),
adapters.cfh,
),
createSource(
"people.active-bans",
anyCapability(PERMS.BANS_VIEW, PERMS.MOD_BANS_VIEW),
adapters.activeBans,
),
];
}
function time(occurredAt: string | null) {
if (occurredAt === null) return null;
const timestamp = Date.parse(occurredAt);
if (!Number.isFinite(timestamp)) return null;
return {
occurredAt,
ageMs: Math.max(0, Date.now() - timestamp),
freshness:
Date.now() - timestamp > 86_400_000
? ("stale" as const)
: ("fresh" as const),
};
}
const productionAdapters: PeopleInboxAdapters = {
async tickets(context) {
const result = await peopleSupportQuery.run(context, {
routeId: "people.support.tickets",
list: { pageSize: 25, offset: 0, sort: "updatedAt", order: "desc" },
});
if (!result.ok || result.data.kind !== "tickets") throw new Error("tickets");
const capability = anyCapability(PERMS.TICKETS_VIEW, PERMS.MOD_TICKETS_VIEW);
return result.data.page.items.flatMap((ticket) => {
const date = time(ticket.updatedAt);
return date === null
? []
: [{
sourceId: "people.tickets",
itemId: String(ticket.id),
deduplicationKey: `${ticket.source}-ticket:${ticket.id}`,
domain: "people" as const,
capability,
severity: ticket.priority === "urgent" ? "critical" as const : "info" as const,
priority: ticket.priority === "urgent" ? "critical" as const : "normal" as const,
...date,
state: ticket.status,
titleKey: "pages.housekeeping.items.ticket",
context: { subject: ticket.subject },
href: ticket.href,
actions: [],
}];
});
},
async helpTickets(context) {
const result = await peopleSupportQuery.run(context, {
routeId: "people.support.help-tickets",
list: { pageSize: 25, offset: 0, sort: "updatedAt", order: "desc" },
});
if (!result.ok || result.data.kind !== "help-tickets") throw new Error("help");
const capability = anyCapability(PERMS.TICKETS_VIEW, PERMS.MOD_TICKETS_VIEW);
return result.data.page.items.flatMap((ticket) => {
const date = time(ticket.updatedAt);
return date === null ? [] : [{
sourceId: "people.help-tickets",
itemId: ticket.id,
deduplicationKey: `help-ticket:${ticket.id}`,
domain: "people" as const,
capability,
severity: "info" as const,
priority: "normal" as const,
...date,
state: ticket.open ? "open" : "closed",
titleKey: "pages.housekeeping.items.helpTicket",
context: { title: ticket.title },
href: ticket.href,
actions: [],
}];
});
},
async cfh(context) {
const result = await peopleModerationQuery.run(context, {
routeId: "people.moderation.cfh",
list: { pageSize: 25, offset: 0, sort: "createdAt", order: "desc" },
});
if (!result.ok || result.data.kind !== "cfh") throw new Error("cfh");
const capability = anyCapability(PERMS.MODERATION_VIEW, PERMS.MOD_CFH_VIEW);
return result.data.page.items.flatMap((ticket) => {
const date = time(ticket.createdAt);
return date === null ? [] : [{
sourceId: "people.cfh",
itemId: String(ticket.id),
deduplicationKey: `cfh:${ticket.id}`,
domain: "people" as const,
capability,
severity: "warning" as const,
priority: "high" as const,
...date,
state: String(ticket.state),
titleKey: "pages.housekeeping.items.cfh",
context: { issue: ticket.issue },
href: ticket.href,
actions: [],
}];
});
},
async activeBans(context) {
const result = await peopleModerationQuery.run(context, {
routeId: "people.moderation.bans",
list: { pageSize: 25, offset: 0, sort: "createdAt", order: "desc" },
});
if (!result.ok || result.data.kind !== "bans") throw new Error("bans");
const capability = anyCapability(PERMS.BANS_VIEW, PERMS.MOD_BANS_VIEW);
return result.data.page.items.flatMap((ban) => {
const date = time(ban.createdAt);
return date === null ? [] : [{
sourceId: "people.active-bans",
itemId: String(ban.id),
deduplicationKey: `ban:${ban.id}`,
domain: "people" as const,
capability,
severity: "warning" as const,
priority: "normal" as const,
...date,
state: "active",
titleKey: "pages.housekeeping.items.activeBan",
context: { userId: ban.userId, reason: ban.reason },
href: `/ase/people/users/${ban.userId}` as const,
actions: [],
}];
});
},
};
export const PEOPLE_INBOX_SOURCES =
createPeopleInboxSources(productionAdapters);