docs: audit housekeeping economy workflows
This commit is contained in:
1 parent
09850c807c
commit
3a03a3db15
2 files changed
+476
No files matched your search
@@ -0,0 +1,55 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { ownedLegacyPages } from "./discover-legacy-pages";
|
||||||
|
import { economyMigrationEntries } from "./economy";
|
||||||
|
import { validateMigrationEntries } from "./validate-matrix";
|
||||||
|
|
||||||
|
const ECONOMY_PREFIXES = [
|
||||||
|
"/admin/catalog",
|
||||||
|
"/admin/items",
|
||||||
|
"/admin/shop",
|
||||||
|
"/admin/marketplace",
|
||||||
|
"/admin/transactions",
|
||||||
|
"/admin/vouchers",
|
||||||
|
"/admin/subscriptions",
|
||||||
|
"/admin/rare-values",
|
||||||
|
"/admin/badges",
|
||||||
|
"/admin/achievements",
|
||||||
|
"/admin/sounds",
|
||||||
|
"/admin/calendar",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
describe("economyMigrationEntries", () => {
|
||||||
|
it("covers every economy page without legacy-domain targets", () => {
|
||||||
|
const expected = ownedLegacyPages(ECONOMY_PREFIXES);
|
||||||
|
expect(validateMigrationEntries(expected, economyMigrationEntries)).toEqual(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
economyMigrationEntries.every(
|
||||||
|
(row) =>
|
||||||
|
row.targetPath === null ||
|
||||||
|
row.targetPath.startsWith("/admin/economy/"),
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps every audited workflow planned without parity claims", () => {
|
||||||
|
expect(
|
||||||
|
economyMigrationEntries.every(
|
||||||
|
(row) => row.status === "PLANNED" && row.parityEvidence.length === 0,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires privileged audit for economy mutations", () => {
|
||||||
|
const mutationRows = economyMigrationEntries.filter(
|
||||||
|
(row) => row.dependencies.mutations.length > 0,
|
||||||
|
);
|
||||||
|
expect(mutationRows.length).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
mutationRows.every(
|
||||||
|
(row) => row.auditRequirement === "PRIVILEGED_MUTATION",
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,421 @@
|
|||||||
|
import { PERMS } from "@/lib/permission-slugs";
|
||||||
|
import type { MigrationEntry } from "./types";
|
||||||
|
|
||||||
|
type PlannedEconomyEntry = Omit<
|
||||||
|
MigrationEntry,
|
||||||
|
"targetDomain" | "requiredTests" | "parityEvidence" | "status" | "notes"
|
||||||
|
> & {
|
||||||
|
requiredTests?: MigrationEntry["requiredTests"];
|
||||||
|
notes?: MigrationEntry["notes"];
|
||||||
|
};
|
||||||
|
|
||||||
|
function plannedEconomyEntry(entry: PlannedEconomyEntry): MigrationEntry {
|
||||||
|
return {
|
||||||
|
...entry,
|
||||||
|
targetDomain: "economy",
|
||||||
|
requiredTests: entry.requiredTests ?? ["integration", "e2e", "visual"],
|
||||||
|
parityEvidence: [],
|
||||||
|
status: "PLANNED",
|
||||||
|
notes: entry.notes ?? [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const economyMigrationEntries: readonly MigrationEntry[] = [
|
||||||
|
// Batch 1: catalog pages, Builder Club, maintenance, and base items.
|
||||||
|
plannedEconomyEntry({
|
||||||
|
surface: "admin",
|
||||||
|
legacyPath: "/admin/catalog",
|
||||||
|
sourceFile: "src/app/admin/catalog/page.tsx",
|
||||||
|
targetPath: "/admin/economy/catalog",
|
||||||
|
decision: "MERGE",
|
||||||
|
capabilities: {
|
||||||
|
read: [PERMS.CATALOG_VIEW],
|
||||||
|
mutate: [PERMS.CATALOG_EDIT],
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
queries: [
|
||||||
|
"CatalogItems",
|
||||||
|
"CatalogItemsBc",
|
||||||
|
"CatalogPages",
|
||||||
|
"CatalogPagesBc",
|
||||||
|
"GET /api/admin/catalog/items",
|
||||||
|
"GET /api/admin/catalog/tree",
|
||||||
|
"getCatalogItemCounts",
|
||||||
|
],
|
||||||
|
mutations: [
|
||||||
|
"createBcPage",
|
||||||
|
"createCatalogPage",
|
||||||
|
"DELETE /api/admin/catalog/tree",
|
||||||
|
"deleteBcTreePage",
|
||||||
|
"deleteCatalogPage",
|
||||||
|
"deleteTreePage",
|
||||||
|
"fixEverythingAction",
|
||||||
|
"PATCH /api/admin/catalog/tree",
|
||||||
|
"reorderBcTreePage",
|
||||||
|
"reorderTreePage",
|
||||||
|
"toggleBcPage",
|
||||||
|
"toggleCatalogPage",
|
||||||
|
"updateBcPage",
|
||||||
|
"updateCatalogPage",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
auditRequirement: "PRIVILEGED_MUTATION",
|
||||||
|
localization: "PARTIAL",
|
||||||
|
accessibility: "PARTIAL",
|
||||||
|
notes: [
|
||||||
|
"Merge the overlapping table, tree, and visual-manager editors into one catalog workflow",
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
plannedEconomyEntry({
|
||||||
|
surface: "admin",
|
||||||
|
legacyPath: "/admin/catalog/:id",
|
||||||
|
sourceFile: "src/app/admin/catalog/[id]/page.tsx",
|
||||||
|
targetPath: "/admin/economy/catalog/:id",
|
||||||
|
decision: "MERGE",
|
||||||
|
capabilities: {
|
||||||
|
read: [PERMS.CATALOG_VIEW],
|
||||||
|
mutate: [PERMS.CATALOG_EDIT],
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
queries: [
|
||||||
|
"CatalogPages",
|
||||||
|
"GET /api/admin/catalog/suggest",
|
||||||
|
"GET /api/admin/furni/search",
|
||||||
|
"getAncestors",
|
||||||
|
"loadCatalogItemsData",
|
||||||
|
],
|
||||||
|
mutations: [
|
||||||
|
"bulkCreateCatalogItems",
|
||||||
|
"createCatalogItem",
|
||||||
|
"deleteCatalogItems",
|
||||||
|
"deleteCatalogPage",
|
||||||
|
"moveCatalogItems",
|
||||||
|
"reorderCatalogItems",
|
||||||
|
"translateCatalogItems",
|
||||||
|
"updateCatalogItem",
|
||||||
|
"updateCatalogPage",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
auditRequirement: "PRIVILEGED_MUTATION",
|
||||||
|
localization: "PARTIAL",
|
||||||
|
accessibility: "PARTIAL",
|
||||||
|
notes: [
|
||||||
|
"Merge the detail tabs with the collection-level visual editor while preserving the page id",
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
plannedEconomyEntry({
|
||||||
|
surface: "admin",
|
||||||
|
legacyPath: "/admin/catalog/builder-club",
|
||||||
|
sourceFile: "src/app/admin/catalog/builder-club/page.tsx",
|
||||||
|
targetPath: "/admin/economy/catalog",
|
||||||
|
decision: "MERGE",
|
||||||
|
capabilities: { read: [PERMS.CATALOG_VIEW], mutate: [] },
|
||||||
|
dependencies: { queries: [], mutations: [] },
|
||||||
|
auditRequirement: "NONE",
|
||||||
|
localization: "COMPLETE",
|
||||||
|
accessibility: "COMPLETE",
|
||||||
|
requiredTests: ["e2e"],
|
||||||
|
notes: ["Redirect-only entry into the Builder Club catalog view"],
|
||||||
|
}),
|
||||||
|
plannedEconomyEntry({
|
||||||
|
surface: "admin",
|
||||||
|
legacyPath: "/admin/catalog/builder-club/:id",
|
||||||
|
sourceFile: "src/app/admin/catalog/builder-club/[id]/page.tsx",
|
||||||
|
targetPath: "/admin/economy/catalog/builder-club/:id",
|
||||||
|
decision: "REBUILD",
|
||||||
|
capabilities: {
|
||||||
|
read: [PERMS.CATALOG_VIEW],
|
||||||
|
mutate: [PERMS.CATALOG_EDIT],
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
queries: ["CatalogItemsBc", "CatalogPagesBc"],
|
||||||
|
mutations: [
|
||||||
|
"createBcItem",
|
||||||
|
"deleteBcItem",
|
||||||
|
"updateBcItem",
|
||||||
|
"updateBcPage",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
auditRequirement: "PRIVILEGED_MUTATION",
|
||||||
|
localization: "PARTIAL",
|
||||||
|
accessibility: "PARTIAL",
|
||||||
|
}),
|
||||||
|
plannedEconomyEntry({
|
||||||
|
surface: "admin",
|
||||||
|
legacyPath: "/admin/catalog/maintenance",
|
||||||
|
sourceFile: "src/app/admin/catalog/maintenance/page.tsx",
|
||||||
|
targetPath: "/admin/economy/catalog/maintenance",
|
||||||
|
decision: "REBUILD",
|
||||||
|
capabilities: {
|
||||||
|
read: [PERMS.CATALOG_VIEW],
|
||||||
|
mutate: [PERMS.CATALOG_EDIT],
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
queries: ["getFurniHealthAction", "previewAlignIdsAction"],
|
||||||
|
mutations: [
|
||||||
|
"applyAlignIdsAction",
|
||||||
|
"fixCatalogOffersAction",
|
||||||
|
"fixEverythingAction",
|
||||||
|
"fixSpriteIdsAction",
|
||||||
|
"reconcileIdsAction",
|
||||||
|
"removeDuplicateItemsBaseAction",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
auditRequirement: "PRIVILEGED_MUTATION",
|
||||||
|
localization: "PARTIAL",
|
||||||
|
accessibility: "PARTIAL",
|
||||||
|
}),
|
||||||
|
plannedEconomyEntry({
|
||||||
|
surface: "admin",
|
||||||
|
legacyPath: "/admin/items",
|
||||||
|
sourceFile: "src/app/admin/items/page.tsx",
|
||||||
|
targetPath: "/admin/economy/items",
|
||||||
|
decision: "REBUILD",
|
||||||
|
capabilities: { read: [PERMS.CATALOG_VIEW], mutate: [] },
|
||||||
|
dependencies: { queries: ["fetchAdminList", "ItemsBase"], mutations: [] },
|
||||||
|
auditRequirement: "NONE",
|
||||||
|
localization: "COMPLETE",
|
||||||
|
accessibility: "PARTIAL",
|
||||||
|
}),
|
||||||
|
plannedEconomyEntry({
|
||||||
|
surface: "admin",
|
||||||
|
legacyPath: "/admin/items/:id",
|
||||||
|
sourceFile: "src/app/admin/items/[id]/page.tsx",
|
||||||
|
targetPath: "/admin/economy/items/:id",
|
||||||
|
decision: "REBUILD",
|
||||||
|
capabilities: {
|
||||||
|
read: [PERMS.CATALOG_VIEW],
|
||||||
|
mutate: [PERMS.CATALOG_EDIT],
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
queries: ["ItemsBase"],
|
||||||
|
mutations: ["updateItemsBase"],
|
||||||
|
},
|
||||||
|
auditRequirement: "PRIVILEGED_MUTATION",
|
||||||
|
localization: "COMPLETE",
|
||||||
|
accessibility: "PARTIAL",
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Batch 2: commerce products, listings, vouchers, and subscriptions.
|
||||||
|
plannedEconomyEntry({
|
||||||
|
surface: "admin",
|
||||||
|
legacyPath: "/admin/shop",
|
||||||
|
sourceFile: "src/app/admin/shop/page.tsx",
|
||||||
|
targetPath: "/admin/economy/commerce/shop",
|
||||||
|
decision: "REBUILD",
|
||||||
|
capabilities: { read: [PERMS.SHOP_VIEW], mutate: [] },
|
||||||
|
dependencies: {
|
||||||
|
queries: ["WebsitePaypalTransactions", "WebsiteShopArticles"],
|
||||||
|
mutations: [],
|
||||||
|
},
|
||||||
|
auditRequirement: "NONE",
|
||||||
|
localization: "PARTIAL",
|
||||||
|
accessibility: "PARTIAL",
|
||||||
|
}),
|
||||||
|
plannedEconomyEntry({
|
||||||
|
surface: "admin",
|
||||||
|
legacyPath: "/admin/shop/new",
|
||||||
|
sourceFile: "src/app/admin/shop/new/page.tsx",
|
||||||
|
targetPath: "/admin/economy/commerce/shop/new",
|
||||||
|
decision: "REBUILD",
|
||||||
|
capabilities: {
|
||||||
|
read: [PERMS.SHOP_EDIT],
|
||||||
|
mutate: [PERMS.SHOP_EDIT],
|
||||||
|
},
|
||||||
|
dependencies: { queries: [], mutations: ["createShopArticle"] },
|
||||||
|
auditRequirement: "PRIVILEGED_MUTATION",
|
||||||
|
localization: "COMPLETE",
|
||||||
|
accessibility: "PARTIAL",
|
||||||
|
}),
|
||||||
|
plannedEconomyEntry({
|
||||||
|
surface: "admin",
|
||||||
|
legacyPath: "/admin/shop/:id",
|
||||||
|
sourceFile: "src/app/admin/shop/[id]/page.tsx",
|
||||||
|
targetPath: "/admin/economy/commerce/shop/:id",
|
||||||
|
decision: "REBUILD",
|
||||||
|
capabilities: {
|
||||||
|
read: [PERMS.SHOP_VIEW],
|
||||||
|
mutate: [PERMS.SHOP_EDIT],
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
queries: ["WebsiteShopArticles"],
|
||||||
|
mutations: ["deleteShopArticle", "updateShopArticle"],
|
||||||
|
},
|
||||||
|
auditRequirement: "PRIVILEGED_MUTATION",
|
||||||
|
localization: "COMPLETE",
|
||||||
|
accessibility: "PARTIAL",
|
||||||
|
}),
|
||||||
|
plannedEconomyEntry({
|
||||||
|
surface: "admin",
|
||||||
|
legacyPath: "/admin/marketplace",
|
||||||
|
sourceFile: "src/app/admin/marketplace/page.tsx",
|
||||||
|
targetPath: "/admin/economy/commerce/marketplace",
|
||||||
|
decision: "REBUILD",
|
||||||
|
capabilities: {
|
||||||
|
read: [PERMS.SHOP_VIEW],
|
||||||
|
mutate: [PERMS.SHOP_EDIT],
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
queries: ["MarketplaceItems", "User"],
|
||||||
|
mutations: ["cancelMarketplaceListing"],
|
||||||
|
},
|
||||||
|
auditRequirement: "PRIVILEGED_MUTATION",
|
||||||
|
localization: "COMPLETE",
|
||||||
|
accessibility: "PARTIAL",
|
||||||
|
}),
|
||||||
|
plannedEconomyEntry({
|
||||||
|
surface: "admin",
|
||||||
|
legacyPath: "/admin/vouchers",
|
||||||
|
sourceFile: "src/app/admin/vouchers/page.tsx",
|
||||||
|
targetPath: "/admin/economy/commerce/vouchers",
|
||||||
|
decision: "REBUILD",
|
||||||
|
capabilities: {
|
||||||
|
read: [PERMS.SHOP_VIEW],
|
||||||
|
mutate: [PERMS.SHOP_EDIT],
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
queries: ["WebsiteShopVouchers"],
|
||||||
|
mutations: ["createVoucher", "deleteVoucher"],
|
||||||
|
},
|
||||||
|
auditRequirement: "PRIVILEGED_MUTATION",
|
||||||
|
localization: "COMPLETE",
|
||||||
|
accessibility: "PARTIAL",
|
||||||
|
}),
|
||||||
|
plannedEconomyEntry({
|
||||||
|
surface: "admin",
|
||||||
|
legacyPath: "/admin/subscriptions",
|
||||||
|
sourceFile: "src/app/admin/subscriptions/page.tsx",
|
||||||
|
targetPath: "/admin/economy/commerce/subscriptions",
|
||||||
|
decision: "REBUILD",
|
||||||
|
capabilities: { read: [PERMS.SHOP_VIEW], mutate: [] },
|
||||||
|
dependencies: { queries: ["users_subscriptions"], mutations: [] },
|
||||||
|
auditRequirement: "NONE",
|
||||||
|
localization: "PARTIAL",
|
||||||
|
accessibility: "PARTIAL",
|
||||||
|
notes: ["Current route reads users_subscriptions through raw SQL"],
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Batch 3: transaction history and rare-value configuration.
|
||||||
|
plannedEconomyEntry({
|
||||||
|
surface: "admin",
|
||||||
|
legacyPath: "/admin/transactions",
|
||||||
|
sourceFile: "src/app/admin/transactions/page.tsx",
|
||||||
|
targetPath: "/admin/economy/history/transactions",
|
||||||
|
decision: "REBUILD",
|
||||||
|
capabilities: { read: [PERMS.SHOP_VIEW], mutate: [] },
|
||||||
|
dependencies: {
|
||||||
|
queries: ["User", "WebsitePaypalTransactions"],
|
||||||
|
mutations: [],
|
||||||
|
},
|
||||||
|
auditRequirement: "NONE",
|
||||||
|
localization: "COMPLETE",
|
||||||
|
accessibility: "PARTIAL",
|
||||||
|
}),
|
||||||
|
plannedEconomyEntry({
|
||||||
|
surface: "admin",
|
||||||
|
legacyPath: "/admin/rare-values",
|
||||||
|
sourceFile: "src/app/admin/rare-values/page.tsx",
|
||||||
|
targetPath: "/admin/economy/value/rare-values",
|
||||||
|
decision: "REBUILD",
|
||||||
|
capabilities: {
|
||||||
|
read: [PERMS.SHOP_VIEW],
|
||||||
|
mutate: [PERMS.SHOP_EDIT],
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
queries: ["WebsiteRareValueCategories", "WebsiteRareValues"],
|
||||||
|
mutations: [
|
||||||
|
"createCategory",
|
||||||
|
"createValue",
|
||||||
|
"deleteCategory",
|
||||||
|
"deleteValue",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
auditRequirement: "PRIVILEGED_MUTATION",
|
||||||
|
localization: "COMPLETE",
|
||||||
|
accessibility: "PARTIAL",
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Batch 4: badges, achievements, soundtracks, and calendar rewards.
|
||||||
|
plannedEconomyEntry({
|
||||||
|
surface: "admin",
|
||||||
|
legacyPath: "/admin/badges",
|
||||||
|
sourceFile: "src/app/admin/badges/page.tsx",
|
||||||
|
targetPath: "/admin/economy/rewards/badges",
|
||||||
|
decision: "REBUILD",
|
||||||
|
capabilities: {
|
||||||
|
read: [PERMS.CATALOG_VIEW],
|
||||||
|
mutate: [PERMS.CATALOG_EDIT],
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
queries: ["WebsiteBadges"],
|
||||||
|
mutations: ["giveBadge", "uploadBadge"],
|
||||||
|
},
|
||||||
|
auditRequirement: "PRIVILEGED_MUTATION",
|
||||||
|
localization: "COMPLETE",
|
||||||
|
accessibility: "PARTIAL",
|
||||||
|
}),
|
||||||
|
plannedEconomyEntry({
|
||||||
|
surface: "admin",
|
||||||
|
legacyPath: "/admin/achievements",
|
||||||
|
sourceFile: "src/app/admin/achievements/page.tsx",
|
||||||
|
targetPath: "/admin/economy/rewards/achievements",
|
||||||
|
decision: "REBUILD",
|
||||||
|
capabilities: { read: [PERMS.CATALOG_VIEW], mutate: [] },
|
||||||
|
dependencies: { queries: ["Achievements"], mutations: [] },
|
||||||
|
auditRequirement: "NONE",
|
||||||
|
localization: "COMPLETE",
|
||||||
|
accessibility: "PARTIAL",
|
||||||
|
}),
|
||||||
|
plannedEconomyEntry({
|
||||||
|
surface: "admin",
|
||||||
|
legacyPath: "/admin/sounds",
|
||||||
|
sourceFile: "src/app/admin/sounds/page.tsx",
|
||||||
|
targetPath: "/admin/economy/rewards/sounds",
|
||||||
|
decision: "REBUILD",
|
||||||
|
capabilities: {
|
||||||
|
read: [PERMS.CATALOG_VIEW],
|
||||||
|
mutate: [PERMS.CATALOG_EDIT],
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
queries: ["CatalogItems", "siteSettings.get", "Soundtracks"],
|
||||||
|
mutations: [
|
||||||
|
"deleteSoundtrack",
|
||||||
|
"POST /api/admin/sounds/upload",
|
||||||
|
"updateSoundtrack",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
auditRequirement: "PRIVILEGED_MUTATION",
|
||||||
|
localization: "PARTIAL",
|
||||||
|
accessibility: "PARTIAL",
|
||||||
|
}),
|
||||||
|
plannedEconomyEntry({
|
||||||
|
surface: "admin",
|
||||||
|
legacyPath: "/admin/calendar",
|
||||||
|
sourceFile: "src/app/admin/calendar/page.tsx",
|
||||||
|
targetPath: "/admin/economy/rewards/calendar",
|
||||||
|
decision: "REBUILD",
|
||||||
|
capabilities: { read: [PERMS.SHOP_VIEW], mutate: [] },
|
||||||
|
dependencies: {
|
||||||
|
queries: ["CalendarCampaigns", "CalendarRewards"],
|
||||||
|
mutations: [],
|
||||||
|
},
|
||||||
|
auditRequirement: "NONE",
|
||||||
|
localization: "PARTIAL",
|
||||||
|
accessibility: "PARTIAL",
|
||||||
|
}),
|
||||||
|
plannedEconomyEntry({
|
||||||
|
surface: "admin",
|
||||||
|
legacyPath: "/admin/calendar/:id",
|
||||||
|
sourceFile: "src/app/admin/calendar/[id]/page.tsx",
|
||||||
|
targetPath: "/admin/economy/rewards/calendar/:id",
|
||||||
|
decision: "REBUILD",
|
||||||
|
capabilities: { read: [PERMS.SHOP_VIEW], mutate: [] },
|
||||||
|
dependencies: {
|
||||||
|
queries: ["CalendarCampaigns", "CalendarRewards"],
|
||||||
|
mutations: [],
|
||||||
|
},
|
||||||
|
auditRequirement: "NONE",
|
||||||
|
localization: "COMPLETE",
|
||||||
|
accessibility: "PARTIAL",
|
||||||
|
}),
|
||||||
|
];
|
||||||
Reference in new issue
Block a user