fix
This commit is contained in:
@@ -91,7 +91,7 @@ export function setPermissionAllowed(
|
||||
if (!allowed) {
|
||||
for (const descendant of descendantsOf(key, allKeys)) next.delete(descendant);
|
||||
next.add(key);
|
||||
return normalize(next, allKeys);
|
||||
return normalizeDenied(next, allKeys);
|
||||
}
|
||||
|
||||
next.delete(key);
|
||||
@@ -104,11 +104,11 @@ export function setPermissionAllowed(
|
||||
if (!openPath.has(sibling)) next.add(sibling);
|
||||
}
|
||||
}
|
||||
return normalize(next, allKeys);
|
||||
return normalizeDenied(next, allKeys);
|
||||
}
|
||||
|
||||
/** Drop keys outside the catalog and denials already implied by a denied ancestor. */
|
||||
function normalize(denied: Set<string>, allKeys: string[]): string[] {
|
||||
export function normalizeDenied(denied: Set<string>, allKeys: string[]): string[] {
|
||||
const known = new Set(allKeys);
|
||||
const out: string[] = [];
|
||||
for (const key of denied) {
|
||||
@@ -122,7 +122,7 @@ function normalize(denied: Set<string>, allKeys: string[]): string[] {
|
||||
/** Deny every grantable key (the always-on shell keys are not in the catalog). */
|
||||
export function denyAll(catalog: PermissionCatalog | null | undefined): string[] {
|
||||
const all = catalogKeys(catalog);
|
||||
return normalize(new Set(all), all);
|
||||
return normalizeDenied(new Set(all), all);
|
||||
}
|
||||
|
||||
/** Allowed / total counts for the summary line, ignoring keys the plan already denies. */
|
||||
@@ -170,3 +170,110 @@ export function filterCatalog(
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Role presets.
|
||||
*
|
||||
* Raw feature keys ("catalog.products.tab_error") are precise but unreadable to most
|
||||
* owners, so the editor leads with a role and derives the checkbox state from it.
|
||||
* A role grants whole dashboard sections, with a few key-level exceptions; anything
|
||||
* not granted is denied, and parent-prefix denial collapses the stored list.
|
||||
*
|
||||
* Roles describe which AREAS a teammate can open — the model is page-level, not
|
||||
* verb-level, so a role never implies "read-only" within an area it grants.
|
||||
*/
|
||||
export type PermissionRole = {
|
||||
id: string;
|
||||
/** Sections fully granted, or "all" for every section. */
|
||||
sections: "all" | string[];
|
||||
/** Extra keys granted outside the granted sections. */
|
||||
allowKeys?: string[];
|
||||
/** Keys denied even though their section is granted. */
|
||||
denyKeys?: string[];
|
||||
};
|
||||
|
||||
export const PERMISSION_ROLES: PermissionRole[] = [
|
||||
{ id: "full", sections: "all" },
|
||||
{
|
||||
// Runs the whole product operation, but money and account access stay with the owner.
|
||||
id: "manager",
|
||||
sections: "all",
|
||||
denyKeys: [
|
||||
"settings.api_keys",
|
||||
"settings.team",
|
||||
"settings.team_invite",
|
||||
"billing.checkout",
|
||||
"billing.customer_portal",
|
||||
"billing.quick_upgrade"
|
||||
]
|
||||
},
|
||||
{ id: "catalog_editor", sections: ["dashboard", "catalog", "support"] },
|
||||
{ id: "feed_operator", sections: ["dashboard", "catalog", "feeds", "processing", "support"] },
|
||||
{
|
||||
id: "marketing",
|
||||
sections: ["dashboard", "catalog", "marketing", "support"],
|
||||
allowKeys: ["integrations.email"]
|
||||
},
|
||||
{
|
||||
id: "viewer",
|
||||
sections: ["dashboard", "support"],
|
||||
allowKeys: [
|
||||
"catalog.products",
|
||||
"catalog.categories",
|
||||
"catalog.attributes",
|
||||
"catalog.standard_fields",
|
||||
"feeds.list"
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
/** Sentinel returned by detectRole when the selection matches no preset. */
|
||||
export const CUSTOM_ROLE_ID = "custom";
|
||||
|
||||
export function roleById(id: string): PermissionRole | null {
|
||||
return PERMISSION_ROLES.find((role) => role.id === id) ?? null;
|
||||
}
|
||||
|
||||
/** Top-level grantable entries (those with no grantable ancestor in the catalog). */
|
||||
function rootEntries(catalog: PermissionCatalog | null | undefined): PermissionCatalogEntry[] {
|
||||
return (catalog?.sections ?? []).flatMap((section) =>
|
||||
section.entries.filter((entry) => !entry.parent)
|
||||
);
|
||||
}
|
||||
|
||||
/** The denied list a role produces for this company's catalog. */
|
||||
export function rolePresetDenied(
|
||||
catalog: PermissionCatalog | null | undefined,
|
||||
role: PermissionRole | null
|
||||
): string[] {
|
||||
if (!role) return [];
|
||||
const all = catalogKeys(catalog);
|
||||
const allowKeys = new Set(role.allowKeys ?? []);
|
||||
const denyKeys = new Set(role.denyKeys ?? []);
|
||||
const denied = new Set<string>();
|
||||
|
||||
for (const entry of rootEntries(catalog)) {
|
||||
const sectionGranted =
|
||||
role.sections === "all" || role.sections.includes(entry.section);
|
||||
if (denyKeys.has(entry.key) || (!sectionGranted && !allowKeys.has(entry.key))) {
|
||||
denied.add(entry.key);
|
||||
}
|
||||
}
|
||||
// Key-level exceptions below a granted root (e.g. deny settings.api_keys under settings).
|
||||
for (const key of denyKeys) {
|
||||
if (!isDenied(denied, key)) denied.add(key);
|
||||
}
|
||||
return normalizeDenied(denied, all);
|
||||
}
|
||||
|
||||
/** Which role the current selection corresponds to, or CUSTOM_ROLE_ID. */
|
||||
export function detectRole(
|
||||
catalog: PermissionCatalog | null | undefined,
|
||||
denied: string[]
|
||||
): string {
|
||||
for (const role of PERMISSION_ROLES) {
|
||||
if (samePermissions(rolePresetDenied(catalog, role), denied)) return role.id;
|
||||
}
|
||||
return CUSTOM_ROLE_ID;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user