@carlonicora/nextjs-jsonapi 1.52.0 → 1.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AuthComponent-BkK4Sf3q.d.mts → AuthComponent-CK9aRRW2.d.mts} +1 -1
- package/dist/{AuthComponent-DCfP4o32.d.ts → AuthComponent-IqFWLNIU.d.ts} +1 -1
- package/dist/{BlockNoteEditor-KQPSJCYG.js → BlockNoteEditor-AROKR3J6.js} +14 -14
- package/dist/{BlockNoteEditor-KQPSJCYG.js.map → BlockNoteEditor-AROKR3J6.js.map} +1 -1
- package/dist/{BlockNoteEditor-WUVRCTQI.mjs → BlockNoteEditor-CNMSBGCL.mjs} +4 -4
- package/dist/ModulePathsInterface-49EWvbWy.d.mts +31 -0
- package/dist/ModulePathsInterface-wVS5Raa4.d.ts +31 -0
- package/dist/{auth.interface-C4kEZscm.d.ts → auth.interface-C1WjZ0fM.d.ts} +1 -1
- package/dist/{auth.interface-24ID4yhT.d.mts → auth.interface-fBFqIrw4.d.mts} +1 -1
- package/dist/billing/index.js +346 -346
- package/dist/billing/index.mjs +3 -3
- package/dist/{chunk-BUCV5VFT.mjs → chunk-FE26PIZK.mjs} +53 -2
- package/dist/chunk-FE26PIZK.mjs.map +1 -0
- package/dist/{chunk-BTLJZIDS.mjs → chunk-G5473JP3.mjs} +869 -40
- package/dist/chunk-G5473JP3.mjs.map +1 -0
- package/dist/{chunk-XNISXVQL.mjs → chunk-J2PYGXVD.mjs} +70 -1
- package/dist/chunk-J2PYGXVD.mjs.map +1 -0
- package/dist/{chunk-YKPIFJOB.js → chunk-PQIXFKHT.js} +1457 -628
- package/dist/chunk-PQIXFKHT.js.map +1 -0
- package/dist/{chunk-QIA5FOQB.js → chunk-QOLVON35.js} +71 -2
- package/dist/chunk-QOLVON35.js.map +1 -0
- package/dist/{chunk-V63TFESU.js → chunk-UJBUJALX.js} +53 -2
- package/dist/chunk-UJBUJALX.js.map +1 -0
- package/dist/client/index.d.mts +25 -7
- package/dist/client/index.d.ts +25 -7
- package/dist/client/index.js +10 -4
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +9 -3
- package/dist/components/index.d.mts +52 -10
- package/dist/components/index.d.ts +52 -10
- package/dist/components/index.js +16 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +15 -3
- package/dist/{config-CPN6QZfo.d.ts → config-DZWAFB7H.d.ts} +1 -1
- package/dist/{config-DaxjKdIo.d.mts → config-ndRJIQsP.d.mts} +1 -1
- package/dist/{content.interface-DvPs_JbX.d.mts → content.interface-B5ySfiOE.d.mts} +1 -1
- package/dist/{content.interface-Czin-YRh.d.ts → content.interface-mmz0uMwm.d.ts} +1 -1
- package/dist/contexts/index.d.mts +2 -2
- package/dist/contexts/index.d.ts +2 -2
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +15 -10
- package/dist/core/index.d.ts +15 -10
- package/dist/core/index.js +6 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +5 -1
- package/dist/index.d.mts +47 -10
- package/dist/index.d.ts +47 -10
- package/dist/index.js +17 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +16 -2
- package/dist/{notification.interface-DEW8hR8g.d.ts → notification.interface-COKHDQeE.d.ts} +1 -1
- package/dist/{notification.interface-DKR5WGKH.d.mts → notification.interface-DG7cq9oG.d.mts} +1 -1
- package/dist/{s3.service-BHjcTA0t.d.mts → s3.service-BoRPFx82.d.mts} +4 -4
- package/dist/{s3.service-C_K1VHyx.d.ts → s3.service-ppn9iGJU.d.ts} +4 -4
- package/dist/server/index.d.mts +4 -4
- package/dist/server/index.d.ts +4 -4
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/dist/useRbacState-DhuYYr0S.d.mts +77 -0
- package/dist/useRbacState-NnzNL2ED.d.ts +77 -0
- package/dist/{useSocket-BW6haECW.d.mts → useSocket-CtfuR5wD.d.mts} +1 -1
- package/dist/{useSocket-C9FmYuRM.d.ts → useSocket-bsV-K4qR.d.ts} +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +4 -0
- package/src/components/containers/RoundPageContainer.tsx +1 -1
- package/src/components/containers/RoundPageContainerTitle.tsx +1 -1
- package/src/components/index.ts +6 -0
- package/src/core/index.ts +1 -0
- package/src/core/registry/ModuleRegistry.ts +3 -0
- package/src/features/rbac/components/RbacContainer.tsx +82 -0
- package/src/features/rbac/components/RbacFeatureSection.tsx +66 -0
- package/src/features/rbac/components/RbacModuleTable.tsx +121 -0
- package/src/features/rbac/components/RbacPermissionCell.tsx +97 -0
- package/src/features/rbac/components/RbacPermissionPicker.tsx +179 -0
- package/src/features/rbac/components/RbacToolbar.tsx +40 -0
- package/src/features/rbac/data/ModulePaths.ts +25 -0
- package/src/features/rbac/data/ModulePathsInterface.ts +6 -0
- package/src/features/rbac/data/PermissionMapping.ts +43 -0
- package/src/features/rbac/data/PermissionMappingInterface.ts +12 -0
- package/src/features/rbac/data/RbacService.ts +47 -0
- package/src/features/rbac/data/RbacTypes.ts +15 -0
- package/src/features/rbac/data/index.ts +6 -0
- package/src/features/rbac/hooks/useRbacState.test.ts +178 -0
- package/src/features/rbac/hooks/useRbacState.ts +319 -0
- package/src/features/rbac/index.ts +19 -0
- package/src/features/rbac/rbac.module.ts +19 -0
- package/src/features/rbac/utils/RbacMigrationGenerator.test.ts +124 -0
- package/src/features/rbac/utils/RbacMigrationGenerator.ts +184 -0
- package/src/index.ts +4 -0
- package/dist/chunk-BTLJZIDS.mjs.map +0 -1
- package/dist/chunk-BUCV5VFT.mjs.map +0 -1
- package/dist/chunk-QIA5FOQB.js.map +0 -1
- package/dist/chunk-V63TFESU.js.map +0 -1
- package/dist/chunk-XNISXVQL.mjs.map +0 -1
- package/dist/chunk-YKPIFJOB.js.map +0 -1
- package/dist/useDataListRetriever-BqJSFBck.d.mts +0 -33
- package/dist/useDataListRetriever-BqJSFBck.d.ts +0 -33
- /package/dist/{BlockNoteEditor-WUVRCTQI.mjs.map → BlockNoteEditor-CNMSBGCL.mjs.map} +0 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../../lib/utils";
|
|
4
|
+
import { CheckIcon, MinusIcon, XIcon } from "lucide-react";
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
6
|
+
import { useCallback, useState } from "react";
|
|
7
|
+
import { Button, Checkbox, Input, Popover, PopoverContent, PopoverTrigger, Separator } from "../../../shadcnui";
|
|
8
|
+
import { PermissionValue } from "../data/RbacTypes";
|
|
9
|
+
import RbacPermissionCell from "./RbacPermissionCell";
|
|
10
|
+
|
|
11
|
+
interface RbacPermissionPickerProps {
|
|
12
|
+
value: PermissionValue | undefined | null;
|
|
13
|
+
originalValue?: PermissionValue | undefined | null;
|
|
14
|
+
isRoleColumn?: boolean;
|
|
15
|
+
knownSegments: string[];
|
|
16
|
+
onSetValue: (value: PermissionValue) => void;
|
|
17
|
+
onClear?: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function RbacPermissionPicker({
|
|
21
|
+
value,
|
|
22
|
+
originalValue,
|
|
23
|
+
isRoleColumn = false,
|
|
24
|
+
knownSegments,
|
|
25
|
+
onSetValue,
|
|
26
|
+
onClear,
|
|
27
|
+
}: RbacPermissionPickerProps) {
|
|
28
|
+
const t = useTranslations();
|
|
29
|
+
const [open, setOpen] = useState(false);
|
|
30
|
+
const [customSegment, setCustomSegment] = useState("");
|
|
31
|
+
|
|
32
|
+
// Parse current segments from value if it's a string
|
|
33
|
+
const currentSegments: string[] = typeof value === "string" ? value.split("|").filter(Boolean) : [];
|
|
34
|
+
|
|
35
|
+
const toggleSegment = useCallback(
|
|
36
|
+
(segment: string) => {
|
|
37
|
+
const newSegments = currentSegments.includes(segment)
|
|
38
|
+
? currentSegments.filter((s) => s !== segment)
|
|
39
|
+
: [...currentSegments, segment];
|
|
40
|
+
|
|
41
|
+
if (newSegments.length === 0) {
|
|
42
|
+
onSetValue(false);
|
|
43
|
+
} else {
|
|
44
|
+
onSetValue(newSegments.join("|"));
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
[currentSegments, onSetValue],
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const addCustomSegment = useCallback(() => {
|
|
51
|
+
if (!customSegment.trim()) return;
|
|
52
|
+
const segment = customSegment.trim();
|
|
53
|
+
if (!currentSegments.includes(segment)) {
|
|
54
|
+
const newSegments = [...currentSegments, segment];
|
|
55
|
+
onSetValue(newSegments.join("|"));
|
|
56
|
+
}
|
|
57
|
+
setCustomSegment("");
|
|
58
|
+
}, [customSegment, currentSegments, onSetValue]);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
62
|
+
<PopoverTrigger>
|
|
63
|
+
<RbacPermissionCell value={value} originalValue={originalValue} isRoleColumn={isRoleColumn} />
|
|
64
|
+
</PopoverTrigger>
|
|
65
|
+
<PopoverContent className="w-72 p-3" align="center">
|
|
66
|
+
<div className="space-y-3">
|
|
67
|
+
{/* Quick toggles */}
|
|
68
|
+
<div>
|
|
69
|
+
<p className="text-xs font-medium text-muted-foreground mb-2">{t("rbac.quick_value")}</p>
|
|
70
|
+
<div className="flex gap-2">
|
|
71
|
+
<Button
|
|
72
|
+
size="sm"
|
|
73
|
+
variant={value === true ? "default" : "outline"}
|
|
74
|
+
className={cn("flex-1 gap-1", value === true && "bg-emerald-600 hover:bg-emerald-700")}
|
|
75
|
+
onClick={() => {
|
|
76
|
+
onSetValue(true);
|
|
77
|
+
setOpen(false);
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
<CheckIcon className="h-3 w-3" />
|
|
81
|
+
<span>true</span>
|
|
82
|
+
</Button>
|
|
83
|
+
<Button
|
|
84
|
+
size="sm"
|
|
85
|
+
variant={value === false ? "destructive" : "outline"}
|
|
86
|
+
className="flex-1 gap-1"
|
|
87
|
+
onClick={() => {
|
|
88
|
+
onSetValue(false);
|
|
89
|
+
setOpen(false);
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
<XIcon className="h-3 w-3" />
|
|
93
|
+
<span>false</span>
|
|
94
|
+
</Button>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{/* Clear / Inherit (role columns only) */}
|
|
99
|
+
{isRoleColumn && onClear && (
|
|
100
|
+
<>
|
|
101
|
+
<Separator />
|
|
102
|
+
<Button
|
|
103
|
+
size="sm"
|
|
104
|
+
variant="ghost"
|
|
105
|
+
className="w-full gap-1 text-muted-foreground"
|
|
106
|
+
onClick={() => {
|
|
107
|
+
onClear();
|
|
108
|
+
setOpen(false);
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
<MinusIcon className="h-3 w-3" />
|
|
112
|
+
{t("rbac.inherit_from_defaults")}
|
|
113
|
+
</Button>
|
|
114
|
+
</>
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
{/* Relationship path builder */}
|
|
118
|
+
{knownSegments.length > 0 && (
|
|
119
|
+
<>
|
|
120
|
+
<Separator />
|
|
121
|
+
<div>
|
|
122
|
+
<p className="text-xs font-medium text-muted-foreground mb-2">{t("rbac.relationships")}</p>
|
|
123
|
+
<div className="max-h-40 space-y-1 overflow-y-auto">
|
|
124
|
+
{knownSegments.map((segment) => (
|
|
125
|
+
<label
|
|
126
|
+
key={segment}
|
|
127
|
+
className="flex items-center gap-2 rounded px-1 py-0.5 text-sm hover:bg-muted cursor-pointer"
|
|
128
|
+
>
|
|
129
|
+
<Checkbox
|
|
130
|
+
checked={currentSegments.includes(segment)}
|
|
131
|
+
onCheckedChange={() => toggleSegment(segment)}
|
|
132
|
+
/>
|
|
133
|
+
<span className="font-mono text-xs">{segment}</span>
|
|
134
|
+
</label>
|
|
135
|
+
))}
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
{/* Custom segment input */}
|
|
139
|
+
<div className="mt-2 flex gap-1">
|
|
140
|
+
<Input
|
|
141
|
+
value={customSegment}
|
|
142
|
+
onChange={(e) => setCustomSegment(e.target.value)}
|
|
143
|
+
placeholder={t("rbac.custom_segment_placeholder")}
|
|
144
|
+
className="h-7 text-xs font-mono"
|
|
145
|
+
onKeyDown={(e) => {
|
|
146
|
+
if (e.key === "Enter") {
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
addCustomSegment();
|
|
149
|
+
}
|
|
150
|
+
}}
|
|
151
|
+
/>
|
|
152
|
+
<Button
|
|
153
|
+
size="sm"
|
|
154
|
+
variant="outline"
|
|
155
|
+
className="h-7 px-2 text-xs"
|
|
156
|
+
onClick={addCustomSegment}
|
|
157
|
+
disabled={!customSegment.trim()}
|
|
158
|
+
>
|
|
159
|
+
+
|
|
160
|
+
</Button>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{/* Preview */}
|
|
164
|
+
{currentSegments.length > 0 && (
|
|
165
|
+
<div className="mt-2 rounded bg-muted p-2">
|
|
166
|
+
<p className="text-xs text-muted-foreground mb-1">{t("rbac.preview")}</p>
|
|
167
|
+
<p className="text-xs font-mono break-all">{currentSegments.join("|")}</p>
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
</>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
</PopoverContent>
|
|
175
|
+
</Popover>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export { RbacPermissionPicker };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { DownloadIcon, RotateCcwIcon } from "lucide-react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { Badge, Button } from "../../../shadcnui";
|
|
6
|
+
|
|
7
|
+
interface RbacToolbarProps {
|
|
8
|
+
isDirty: boolean;
|
|
9
|
+
onGenerate: () => void;
|
|
10
|
+
onReset: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function RbacToolbar({ isDirty, onGenerate, onReset }: RbacToolbarProps) {
|
|
14
|
+
const t = useTranslations();
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="flex items-center justify-between rounded-lg border bg-card px-4 py-3">
|
|
18
|
+
<div className="flex items-center gap-3">
|
|
19
|
+
<h2 className="text-lg font-semibold">{t("rbac.title")}</h2>
|
|
20
|
+
{isDirty && (
|
|
21
|
+
<Badge variant="outline" className="border-amber-400 text-amber-600">
|
|
22
|
+
{t("rbac.unsaved_changes")}
|
|
23
|
+
</Badge>
|
|
24
|
+
)}
|
|
25
|
+
</div>
|
|
26
|
+
<div className="flex items-center gap-2">
|
|
27
|
+
<Button variant="outline" size="sm" onClick={onReset} disabled={!isDirty} className="gap-1">
|
|
28
|
+
<RotateCcwIcon className="h-3.5 w-3.5" />
|
|
29
|
+
{t("rbac.reset")}
|
|
30
|
+
</Button>
|
|
31
|
+
<Button size="sm" onClick={onGenerate} disabled={!isDirty} className="gap-1">
|
|
32
|
+
<DownloadIcon className="h-3.5 w-3.5" />
|
|
33
|
+
{t("rbac.generate_migration")}
|
|
34
|
+
</Button>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export { RbacToolbar };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AbstractApiData, JsonApiHydratedDataInterface } from "../../../core";
|
|
2
|
+
import { ModulePathsInterface } from "./ModulePathsInterface";
|
|
3
|
+
|
|
4
|
+
export class ModulePaths extends AbstractApiData implements ModulePathsInterface {
|
|
5
|
+
private _moduleId?: string;
|
|
6
|
+
private _paths?: string[];
|
|
7
|
+
|
|
8
|
+
get moduleId(): string {
|
|
9
|
+
if (!this._moduleId) throw new Error("JsonApi error: module paths moduleId is missing");
|
|
10
|
+
return this._moduleId;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
get paths(): string[] {
|
|
14
|
+
return this._paths ?? [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
rehydrate(data: JsonApiHydratedDataInterface): this {
|
|
18
|
+
super.rehydrate(data);
|
|
19
|
+
|
|
20
|
+
this._moduleId = data.jsonApi.attributes.moduleId;
|
|
21
|
+
this._paths = data.jsonApi.attributes.paths;
|
|
22
|
+
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { AbstractApiData, JsonApiHydratedDataInterface } from "../../../core";
|
|
2
|
+
import { PermissionMappingInterface } from "./PermissionMappingInterface";
|
|
3
|
+
|
|
4
|
+
export class PermissionMapping extends AbstractApiData implements PermissionMappingInterface {
|
|
5
|
+
private _roleId?: string;
|
|
6
|
+
private _moduleId?: string;
|
|
7
|
+
private _permissions?: {
|
|
8
|
+
create?: boolean | string;
|
|
9
|
+
read?: boolean | string;
|
|
10
|
+
update?: boolean | string;
|
|
11
|
+
delete?: boolean | string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
get roleId(): string {
|
|
15
|
+
if (!this._roleId) throw new Error("JsonApi error: permission mapping roleId is missing");
|
|
16
|
+
return this._roleId;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get moduleId(): string {
|
|
20
|
+
if (!this._moduleId) throw new Error("JsonApi error: permission mapping moduleId is missing");
|
|
21
|
+
return this._moduleId;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get permissions(): {
|
|
25
|
+
create?: boolean | string;
|
|
26
|
+
read?: boolean | string;
|
|
27
|
+
update?: boolean | string;
|
|
28
|
+
delete?: boolean | string;
|
|
29
|
+
} {
|
|
30
|
+
if (!this._permissions) throw new Error("JsonApi error: permission mapping permissions is missing");
|
|
31
|
+
return this._permissions;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
rehydrate(data: JsonApiHydratedDataInterface): this {
|
|
35
|
+
super.rehydrate(data);
|
|
36
|
+
|
|
37
|
+
this._roleId = data.jsonApi.attributes.roleId;
|
|
38
|
+
this._moduleId = data.jsonApi.attributes.moduleId;
|
|
39
|
+
this._permissions = data.jsonApi.meta?.permissions;
|
|
40
|
+
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ApiDataInterface } from "../../../core";
|
|
2
|
+
|
|
3
|
+
export interface PermissionMappingInterface extends ApiDataInterface {
|
|
4
|
+
get roleId(): string;
|
|
5
|
+
get moduleId(): string;
|
|
6
|
+
get permissions(): {
|
|
7
|
+
create?: boolean | string;
|
|
8
|
+
read?: boolean | string;
|
|
9
|
+
update?: boolean | string;
|
|
10
|
+
delete?: boolean | string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { AbstractService, EndpointCreator, HttpMethod, Modules } from "../../../core";
|
|
2
|
+
import { FeatureInterface } from "../../feature";
|
|
3
|
+
import { RoleInterface } from "../../role";
|
|
4
|
+
import { PermissionMappingInterface } from "./PermissionMappingInterface";
|
|
5
|
+
import { ModulePathsInterface } from "./ModulePathsInterface";
|
|
6
|
+
|
|
7
|
+
export class RbacService extends AbstractService {
|
|
8
|
+
static async getFeatures(): Promise<FeatureInterface[]> {
|
|
9
|
+
const endpoint = new EndpointCreator({ endpoint: Modules.Feature }).addAdditionalParam("fetchAll", "true");
|
|
10
|
+
|
|
11
|
+
return this.callApi<FeatureInterface[]>({
|
|
12
|
+
type: Modules.Feature,
|
|
13
|
+
method: HttpMethod.GET,
|
|
14
|
+
endpoint: endpoint.generate(),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static async getRoles(): Promise<RoleInterface[]> {
|
|
19
|
+
const endpoint = new EndpointCreator({ endpoint: Modules.Role }).addAdditionalParam("fetchAll", "true");
|
|
20
|
+
|
|
21
|
+
return this.callApi<RoleInterface[]>({
|
|
22
|
+
type: Modules.Role,
|
|
23
|
+
method: HttpMethod.GET,
|
|
24
|
+
endpoint: endpoint.generate(),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static async getPermissionMappings(): Promise<PermissionMappingInterface[]> {
|
|
29
|
+
const endpoint = new EndpointCreator({ endpoint: Modules.PermissionMapping });
|
|
30
|
+
|
|
31
|
+
return this.callApi<PermissionMappingInterface[]>({
|
|
32
|
+
type: Modules.PermissionMapping,
|
|
33
|
+
method: HttpMethod.GET,
|
|
34
|
+
endpoint: endpoint.generate(),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static async getModuleRelationshipPaths(): Promise<ModulePathsInterface[]> {
|
|
39
|
+
const endpoint = new EndpointCreator({ endpoint: Modules.ModulePaths });
|
|
40
|
+
|
|
41
|
+
return this.callApi<ModulePathsInterface[]>({
|
|
42
|
+
type: Modules.ModulePaths,
|
|
43
|
+
method: HttpMethod.GET,
|
|
44
|
+
endpoint: endpoint.generate(),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const COMPANY_ADMINISTRATOR_ROLE_ID = "2e1eee00-6cba-4506-9059-ccd24e4ea5b0";
|
|
2
|
+
|
|
3
|
+
export type PermissionValue = boolean | string;
|
|
4
|
+
|
|
5
|
+
export type ActionType = "read" | "create" | "update" | "delete";
|
|
6
|
+
|
|
7
|
+
export const ACTION_TYPES: ActionType[] = ["read", "create", "update", "delete"];
|
|
8
|
+
|
|
9
|
+
/** The permissions object shape used by both Module and PermissionMapping entities */
|
|
10
|
+
export type PermissionsMap = {
|
|
11
|
+
create?: PermissionValue;
|
|
12
|
+
read?: PermissionValue;
|
|
13
|
+
update?: PermissionValue;
|
|
14
|
+
delete?: PermissionValue;
|
|
15
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { renderHook, act } from "@testing-library/react";
|
|
3
|
+
import { useRbacState } from "./useRbacState";
|
|
4
|
+
import { FeatureInterface } from "../../feature";
|
|
5
|
+
import { RoleInterface } from "../../role";
|
|
6
|
+
import { PermissionMappingInterface } from "../data/PermissionMappingInterface";
|
|
7
|
+
import { ModulePathsInterface } from "../data/ModulePathsInterface";
|
|
8
|
+
import { ModuleInterface } from "../../module";
|
|
9
|
+
|
|
10
|
+
const mockModule: ModuleInterface = {
|
|
11
|
+
id: "mod-1",
|
|
12
|
+
type: "modules",
|
|
13
|
+
included: [],
|
|
14
|
+
createdAt: new Date(),
|
|
15
|
+
updatedAt: new Date(),
|
|
16
|
+
name: "pipelines",
|
|
17
|
+
permissions: { create: true, read: true, update: true, delete: false },
|
|
18
|
+
} as ModuleInterface;
|
|
19
|
+
|
|
20
|
+
const mockFeatures: FeatureInterface[] = [
|
|
21
|
+
{
|
|
22
|
+
id: "feat-1",
|
|
23
|
+
type: "features",
|
|
24
|
+
included: [],
|
|
25
|
+
createdAt: new Date(),
|
|
26
|
+
updatedAt: new Date(),
|
|
27
|
+
name: "CRM",
|
|
28
|
+
isCore: false,
|
|
29
|
+
modules: [mockModule],
|
|
30
|
+
} as FeatureInterface,
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const mockRoles: RoleInterface[] = [
|
|
34
|
+
{
|
|
35
|
+
id: "role-1",
|
|
36
|
+
type: "roles",
|
|
37
|
+
included: [],
|
|
38
|
+
createdAt: new Date(),
|
|
39
|
+
updatedAt: new Date(),
|
|
40
|
+
name: "Manager",
|
|
41
|
+
description: "",
|
|
42
|
+
isSelectable: true,
|
|
43
|
+
requiredFeature: undefined,
|
|
44
|
+
} as unknown as RoleInterface,
|
|
45
|
+
{
|
|
46
|
+
id: "role-2",
|
|
47
|
+
type: "roles",
|
|
48
|
+
included: [],
|
|
49
|
+
createdAt: new Date(),
|
|
50
|
+
updatedAt: new Date(),
|
|
51
|
+
name: "Viewer",
|
|
52
|
+
description: "",
|
|
53
|
+
isSelectable: true,
|
|
54
|
+
requiredFeature: undefined,
|
|
55
|
+
} as unknown as RoleInterface,
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const mockPermissionMappings: PermissionMappingInterface[] = [];
|
|
59
|
+
|
|
60
|
+
const mockModulePaths: ModulePathsInterface[] = [
|
|
61
|
+
{
|
|
62
|
+
id: "mod-1",
|
|
63
|
+
type: "module-paths",
|
|
64
|
+
included: [],
|
|
65
|
+
createdAt: new Date(),
|
|
66
|
+
updatedAt: new Date(),
|
|
67
|
+
moduleId: "mod-1",
|
|
68
|
+
paths: ["owner", "company.user"],
|
|
69
|
+
} as unknown as ModulePathsInterface,
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
function initHook() {
|
|
73
|
+
const { result } = renderHook(() => useRbacState());
|
|
74
|
+
act(() => {
|
|
75
|
+
result.current.init(mockFeatures, mockRoles, mockPermissionMappings, mockModulePaths);
|
|
76
|
+
});
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe("useRbacState", () => {
|
|
81
|
+
describe("Scenario: Initialize state from fetched data", () => {
|
|
82
|
+
it("should initialize with features and roles", () => {
|
|
83
|
+
const result = initHook();
|
|
84
|
+
|
|
85
|
+
expect(result.current.original).not.toBeNull();
|
|
86
|
+
expect(result.current.original!.features).toHaveLength(1);
|
|
87
|
+
expect(result.current.original!.features[0].name).toBe("CRM");
|
|
88
|
+
expect(result.current.isDirty).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("Scenario: Set module default permission", () => {
|
|
93
|
+
it("should update default permission and mark as dirty", () => {
|
|
94
|
+
const result = initHook();
|
|
95
|
+
|
|
96
|
+
act(() => {
|
|
97
|
+
result.current.setModuleDefaultPermission("mod-1", "delete", true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(result.current.getModuleDefaultPermission("mod-1", "delete")).toBe(true);
|
|
101
|
+
expect(result.current.isDirty).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("Scenario: Set role-specific permission override", () => {
|
|
106
|
+
it("should set role permission for a specific module and action", () => {
|
|
107
|
+
const result = initHook();
|
|
108
|
+
|
|
109
|
+
act(() => {
|
|
110
|
+
result.current.setRolePermission("role-1", "mod-1", "create", false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(result.current.getRolePermission("role-1", "mod-1", "create")).toBe(false);
|
|
114
|
+
expect(result.current.isDirty).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should support string path values for role permissions", () => {
|
|
118
|
+
const result = initHook();
|
|
119
|
+
|
|
120
|
+
act(() => {
|
|
121
|
+
result.current.setRolePermission("role-2", "mod-1", "update", "owner");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(result.current.getRolePermission("role-2", "mod-1", "update")).toBe("owner");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("Scenario: Clear role permission (inherit from default)", () => {
|
|
129
|
+
it("should clear a role permission so it inherits from default", () => {
|
|
130
|
+
const result = initHook();
|
|
131
|
+
|
|
132
|
+
act(() => {
|
|
133
|
+
result.current.setRolePermission("role-1", "mod-1", "create", false);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
act(() => {
|
|
137
|
+
result.current.clearRolePermission("role-1", "mod-1", "create");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(result.current.getRolePermission("role-1", "mod-1", "create")).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("Scenario: Reset to initial state", () => {
|
|
145
|
+
it("should reset all changes and clear dirty flag", () => {
|
|
146
|
+
const result = initHook();
|
|
147
|
+
|
|
148
|
+
act(() => {
|
|
149
|
+
result.current.setModuleDefaultPermission("mod-1", "delete", true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(result.current.isDirty).toBe(true);
|
|
153
|
+
|
|
154
|
+
act(() => {
|
|
155
|
+
result.current.reset();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(result.current.isDirty).toBe(false);
|
|
159
|
+
expect(result.current.getModuleDefaultPermission("mod-1", "delete")).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("Scenario: Get effective configuration for migration", () => {
|
|
164
|
+
it("should return complete configuration with all changes applied", () => {
|
|
165
|
+
const result = initHook();
|
|
166
|
+
|
|
167
|
+
act(() => {
|
|
168
|
+
result.current.setRolePermission("role-1", "mod-1", "delete", true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const config = result.current.getEffectiveConfiguration();
|
|
172
|
+
expect(config).not.toBeNull();
|
|
173
|
+
expect(config!.features).toHaveLength(1);
|
|
174
|
+
expect(config!.roles).toHaveLength(2);
|
|
175
|
+
expect(config!.rolePermissionsMap).toBeInstanceOf(Map);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
});
|