feat(housekeeping): complete people moderation parity
This commit is contained in:
1 parent
3d385d1869
commit
29fe22297b
41 files changed
+3465
-580
No files matched your search
@@ -0,0 +1,244 @@
|
||||
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);
|
||||
Reference in new issue
Block a user