245 lines
7.1 KiB
TypeScript
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);
|