@studiocubics/cms 0.0.1
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/.turbo/turbo-build.log +7 -0
- package/CHANGELOG.md +12 -0
- package/README.md +15 -0
- package/eslint.config.js +21 -0
- package/package.json +79 -0
- package/rollup.config.js +48 -0
- package/src/clerk/_index.ts +6 -0
- package/src/clerk/actions/_index.ts +2 -0
- package/src/clerk/actions/invitations.ts +78 -0
- package/src/clerk/actions/systemUsers.ts +94 -0
- package/src/clerk/auth.ts +34 -0
- package/src/clerk/clerk.d.ts +105 -0
- package/src/clerk/hasPermission.ts +96 -0
- package/src/clerk/rbacConfig.ts +68 -0
- package/src/clerk/schemas/_index.ts +1 -0
- package/src/clerk/schemas/invitation.ts +17 -0
- package/src/clerk/schemas/systemUser.ts +16 -0
- package/src/clerk/toClientSafeUser.ts +77 -0
- package/src/constants/_index.ts +2 -0
- package/src/constants/defaults.tsx +62 -0
- package/src/constants/pageLimits.ts +2 -0
- package/src/declaration.d.ts +5 -0
- package/src/index.ts +5 -0
- package/src/providers/CMSRootProviders.tsx +13 -0
- package/src/providers/_index.ts +1 -0
- package/src/routes.d.ts +96 -0
- package/src/ui/Inputs/ThemedMonacoEditor/ThemedMonacoEditor.module.css +4 -0
- package/src/ui/Inputs/ThemedMonacoEditor/ThemedMonacoEditor.tsx +16 -0
- package/src/ui/Inputs/_index.ts +1 -0
- package/src/ui/Layout/CMSSecurityLayout.tsx +27 -0
- package/src/ui/Layout/CMSSidebar/CMSSidebar.tsx +39 -0
- package/src/ui/Layout/CMSSidebar/CMSSidebarBody.tsx +43 -0
- package/src/ui/Layout/CMSSidebar/CMSSidebarFooter/CMSSidebarFooter.module.css +7 -0
- package/src/ui/Layout/CMSSidebar/CMSSidebarFooter/CMSSidebarFooter.tsx +59 -0
- package/src/ui/Layout/CMSSidebar/CMSSidebarHeader/CMSSidebarHeader.module.css +44 -0
- package/src/ui/Layout/CMSSidebar/CMSSidebarHeader/CMSSidebarHeader.tsx +30 -0
- package/src/ui/Layout/CMSSidebar/_index.ts +4 -0
- package/src/ui/Layout/_index.ts +2 -0
- package/src/ui/System/Auth/SignIn/SignIn.module.css +50 -0
- package/src/ui/System/Auth/SignIn/SignIn.tsx +79 -0
- package/src/ui/System/Auth/SignIn/_index.ts +2 -0
- package/src/ui/System/Auth/SignIn/useSignInForm.tsx +42 -0
- package/src/ui/System/Auth/SignUp/SignUp.module.css +48 -0
- package/src/ui/System/Auth/SignUp/SignUp.tsx +138 -0
- package/src/ui/System/Auth/SignUp/_index.ts +2 -0
- package/src/ui/System/Auth/SignUp/useSignUpForm.tsx +54 -0
- package/src/ui/System/Auth/_index.ts +2 -0
- package/src/ui/System/Invitations/InvitationList.tsx +9 -0
- package/src/ui/System/Invitations/InvitationListActions.tsx +167 -0
- package/src/ui/System/Invitations/InvitationListCard.tsx +79 -0
- package/src/ui/System/Invitations/InvitationListPage.tsx +32 -0
- package/src/ui/System/Invitations/InvitationListPagination.tsx +19 -0
- package/src/ui/System/Invitations/_index.ts +5 -0
- package/src/ui/System/Permissions/RoleListCard.tsx +33 -0
- package/src/ui/System/Permissions/RolePermissionsPage.tsx +18 -0
- package/src/ui/System/Permissions/RolePermissionsTable.tsx +36 -0
- package/src/ui/System/Permissions/_index.ts +3 -0
- package/src/ui/System/SystemUser/CurrentSystemUserButton/CurrentSystemUserButton.module.css +5 -0
- package/src/ui/System/SystemUser/CurrentSystemUserButton/CurrentSystemUserButton.tsx +102 -0
- package/src/ui/System/SystemUser/CurrentSystemUserPage.tsx +12 -0
- package/src/ui/System/SystemUser/SystemUserActions.tsx +45 -0
- package/src/ui/System/SystemUser/SystemUserDetails/SystemUserDetails.module.css +6 -0
- package/src/ui/System/SystemUser/SystemUserDetails/SystemUserDetails.tsx +71 -0
- package/src/ui/System/SystemUser/SystemUserDetailsForm/SystemUserDetailsForm.module.css +7 -0
- package/src/ui/System/SystemUser/SystemUserDetailsForm/SystemUserDetailsForm.tsx +114 -0
- package/src/ui/System/SystemUser/SystemUserList.tsx +18 -0
- package/src/ui/System/SystemUser/SystemUserListActions.tsx +17 -0
- package/src/ui/System/SystemUser/SystemUserListCard.tsx +85 -0
- package/src/ui/System/SystemUser/SystemUserListPage.tsx +33 -0
- package/src/ui/System/SystemUser/SystemUserListPagination.tsx +19 -0
- package/src/ui/System/SystemUser/SystemUserPage.tsx +30 -0
- package/src/ui/System/SystemUser/SystemUserPageContent.tsx +54 -0
- package/src/ui/System/SystemUser/SystemUserRole/SystemUserRole.module.css +17 -0
- package/src/ui/System/SystemUser/SystemUserRole/SystemUserRole.tsx +64 -0
- package/src/ui/System/SystemUser/SystemUserRoleForm/SystemUserRoleForm.tsx +51 -0
- package/src/ui/System/SystemUser/SystemUserTimestamps.tsx +56 -0
- package/src/ui/System/SystemUser/_index.ts +14 -0
- package/src/ui/System/WelcomePage/WelcomePage.module.css +18 -0
- package/src/ui/System/WelcomePage/WelcomePage.tsx +43 -0
- package/src/ui/System/_index.ts +6 -0
- package/src/ui/System/types.ts +7 -0
- package/src/ui/_index.ts +3 -0
- package/src/utils/_index.ts +1 -0
- package/src/utils/proxyFunctions.ts +37 -0
- package/tsconfig.json +32 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# @studiocubics/cms
|
|
2
|
+
|
|
3
|
+
To install dependencies:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bun install
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
To run:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun run index.ts
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This project was created using `bun init` in bun v1.3.7. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import js from "@eslint/js";
|
|
2
|
+
import globals from "globals";
|
|
3
|
+
import reactHooks from "eslint-plugin-react-hooks";
|
|
4
|
+
import tseslint from "typescript-eslint";
|
|
5
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
6
|
+
|
|
7
|
+
export default defineConfig([
|
|
8
|
+
globalIgnores(["dist"]),
|
|
9
|
+
{
|
|
10
|
+
files: ["**/*.{ts,tsx}"],
|
|
11
|
+
extends: [
|
|
12
|
+
js.configs.recommended,
|
|
13
|
+
tseslint.configs.recommended,
|
|
14
|
+
reactHooks.configs.flat.recommended,
|
|
15
|
+
],
|
|
16
|
+
languageOptions: {
|
|
17
|
+
ecmaVersion: 2020,
|
|
18
|
+
globals: globals.browser,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
]);
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@studiocubics/cms",
|
|
3
|
+
"description": "A collection of pages and components to build a cms for a website.",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"private": false,
|
|
8
|
+
"version": "0.0.1",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"@studiocubics",
|
|
11
|
+
"cubics",
|
|
12
|
+
"studio",
|
|
13
|
+
"react",
|
|
14
|
+
"hooks"
|
|
15
|
+
],
|
|
16
|
+
"author": {
|
|
17
|
+
"name": "Studio Cubics",
|
|
18
|
+
"email": "studiocubics7@gmail.com",
|
|
19
|
+
"url": "https://studio-cubics.vercel.app"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"development": "./src/index.ts",
|
|
27
|
+
"production": "./dist/index.js",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./styles.css": "./dist/index.css"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@monaco-editor/react": "4.8.0-rc.3",
|
|
34
|
+
"lucide-react": "^0.554.0",
|
|
35
|
+
"next-themes": "^0.4.6",
|
|
36
|
+
"zod": "^4.3.6",
|
|
37
|
+
"@studiocubics/hooks": "^0.0.1",
|
|
38
|
+
"@studiocubics/ui": "^0.0.1",
|
|
39
|
+
"@studiocubics/utils": "^0.0.1",
|
|
40
|
+
"@studiocubics/next": "^0.0.1"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@eslint/js": "^9.39.1",
|
|
44
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
45
|
+
"@rollup/plugin-typescript": "^12.3.0",
|
|
46
|
+
"@types/node": "^24.10.1",
|
|
47
|
+
"@types/react": "^19.2.5",
|
|
48
|
+
"@types/react-dom": "^19.2.3",
|
|
49
|
+
"eslint": "^9.39.1",
|
|
50
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
51
|
+
"globals": "^16.5.0",
|
|
52
|
+
"postcss": "^8.5.6",
|
|
53
|
+
"postcss-modules": "^6.0.1",
|
|
54
|
+
"rollup": "^4.53.3",
|
|
55
|
+
"rollup-plugin-postcss": "^4.0.2",
|
|
56
|
+
"rollup-preserve-directives": "^1.1.3",
|
|
57
|
+
"typescript-eslint": "^8.46.4",
|
|
58
|
+
"babel-plugin-react-compiler": "1.0.0",
|
|
59
|
+
"next": "16.1.6",
|
|
60
|
+
"react": "19.2.4",
|
|
61
|
+
"react-dom": "19.2.4",
|
|
62
|
+
"typescript": "^5.9.3",
|
|
63
|
+
"@studiocubics/types": "^0.0.1"
|
|
64
|
+
},
|
|
65
|
+
"peerDependencies": {
|
|
66
|
+
"babel-plugin-react-compiler": ">= 1",
|
|
67
|
+
"@clerk/nextjs": ">= 6",
|
|
68
|
+
"next": ">= 16",
|
|
69
|
+
"react": ">= 19",
|
|
70
|
+
"react-dom": ">= 19",
|
|
71
|
+
"typescript": ">= 5"
|
|
72
|
+
},
|
|
73
|
+
"scripts": {
|
|
74
|
+
"build": "rollup -c",
|
|
75
|
+
"clean:build": "rimraf dist node_modules && rollup -c",
|
|
76
|
+
"lint": "eslint .",
|
|
77
|
+
"clean": "rimraf dist node_modules"
|
|
78
|
+
}
|
|
79
|
+
}
|
package/rollup.config.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { defineConfig } from "rollup";
|
|
2
|
+
import typescript from "@rollup/plugin-typescript";
|
|
3
|
+
import postcss from "rollup-plugin-postcss";
|
|
4
|
+
import terser from "@rollup/plugin-terser";
|
|
5
|
+
import preserveDirectives from "rollup-preserve-directives";
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
input: "src/index.ts",
|
|
9
|
+
output: {
|
|
10
|
+
preserveModules: true,
|
|
11
|
+
preserveModulesRoot: "src",
|
|
12
|
+
dir: "dist",
|
|
13
|
+
format: "esm",
|
|
14
|
+
sourcemap: true,
|
|
15
|
+
},
|
|
16
|
+
plugins: [
|
|
17
|
+
preserveDirectives(),
|
|
18
|
+
postcss({
|
|
19
|
+
include: ["globals.css", "**/*.module.css"],
|
|
20
|
+
modules: true,
|
|
21
|
+
extract: true,
|
|
22
|
+
// minimize: true,
|
|
23
|
+
}),
|
|
24
|
+
typescript({ tsconfig: "./tsconfig.json" }),
|
|
25
|
+
// terser({ compress: { directives: false } }),
|
|
26
|
+
],
|
|
27
|
+
external: [
|
|
28
|
+
"lucide-react",
|
|
29
|
+
"next/cache",
|
|
30
|
+
"next/font/google",
|
|
31
|
+
"next/headers",
|
|
32
|
+
"next/link",
|
|
33
|
+
"next/navigation",
|
|
34
|
+
"next/server",
|
|
35
|
+
"next-themes",
|
|
36
|
+
"react/jsx-runtime",
|
|
37
|
+
"react",
|
|
38
|
+
"react-dom",
|
|
39
|
+
"@clerk/nextjs",
|
|
40
|
+
"@clerk/nextjs/server",
|
|
41
|
+
"@monaco-editor/react",
|
|
42
|
+
"@studiocubics/ui",
|
|
43
|
+
"@studiocubics/utils",
|
|
44
|
+
"@studiocubics/hooks",
|
|
45
|
+
"@studiocubics/next",
|
|
46
|
+
"zod",
|
|
47
|
+
],
|
|
48
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
|
|
3
|
+
import { clerkClient, type Invitation } from "@clerk/nextjs/server";
|
|
4
|
+
import { auth } from "../auth";
|
|
5
|
+
import { revalidatePath } from "next/cache";
|
|
6
|
+
import { headers } from "next/headers";
|
|
7
|
+
import { getInvitationPublicMetadata } from "../rbacConfig";
|
|
8
|
+
import { createInvitationSchema } from "../schemas/invitation";
|
|
9
|
+
import z from "zod";
|
|
10
|
+
import type { InvitationCreateState } from "../../ui/System/Invitations/InvitationListActions";
|
|
11
|
+
import { apiRes } from "@studiocubics/utils";
|
|
12
|
+
import type { PaginatedResponse } from "../clerk";
|
|
13
|
+
import { cache } from "react";
|
|
14
|
+
|
|
15
|
+
export const invitationListReadAction = cache(
|
|
16
|
+
async (
|
|
17
|
+
params: ClerkInvitationListParams,
|
|
18
|
+
): Promise<PaginatedResponse<Invitation[]>> => {
|
|
19
|
+
const session = await auth();
|
|
20
|
+
if (!session.hasPermission("invitations", "read")) {
|
|
21
|
+
throw new Error(apiRes.forbidden);
|
|
22
|
+
}
|
|
23
|
+
const client = await clerkClient();
|
|
24
|
+
const invitationsList = await client.invitations.getInvitationList(params);
|
|
25
|
+
return invitationsList;
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
export const invitationDeleteAction = async (id: string) => {
|
|
29
|
+
const session = await auth();
|
|
30
|
+
if (!session.hasPermission("invitations", "delete")) {
|
|
31
|
+
return apiRes.actionFail(apiRes.forbidden);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!id || typeof id !== "string")
|
|
35
|
+
return apiRes.actionFail(apiRes.wrongType("id", "string"));
|
|
36
|
+
|
|
37
|
+
const client = await clerkClient();
|
|
38
|
+
await client.invitations.revokeInvitation(id);
|
|
39
|
+
|
|
40
|
+
revalidatePath("/dashboard/security/invitations");
|
|
41
|
+
return apiRes.actionSuccess();
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export async function invitationCreateAction(
|
|
45
|
+
_: InvitationCreateState,
|
|
46
|
+
formData: FormData,
|
|
47
|
+
): Promise<InvitationCreateState> {
|
|
48
|
+
const session = await auth();
|
|
49
|
+
if (!session.hasPermission("invitations", "create")) {
|
|
50
|
+
return apiRes.actionFail(apiRes.forbidden);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const headersList = await headers();
|
|
54
|
+
const host = headersList.get("host");
|
|
55
|
+
const protocol = host?.startsWith("localhost") ? "http" : "https";
|
|
56
|
+
|
|
57
|
+
const formDataObj = Object.fromEntries(formData.entries());
|
|
58
|
+
const { success, data, error } =
|
|
59
|
+
createInvitationSchema.safeParse(formDataObj);
|
|
60
|
+
if (!success) {
|
|
61
|
+
const flattenedError = z.flattenError(error);
|
|
62
|
+
return apiRes.actionFail(
|
|
63
|
+
flattenedError.formErrors.join("\n"),
|
|
64
|
+
flattenedError.fieldErrors,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const publicMetadata = getInvitationPublicMetadata(data.role);
|
|
69
|
+
const client = await clerkClient();
|
|
70
|
+
await client.invitations.createInvitation({
|
|
71
|
+
...data,
|
|
72
|
+
redirectUrl: `${protocol}://${host}/auth/signUp`,
|
|
73
|
+
publicMetadata,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
revalidatePath("/dashboard/security/invitations");
|
|
77
|
+
return apiRes.actionSuccess();
|
|
78
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
import { clerkClient, type User } from "@clerk/nextjs/server";
|
|
3
|
+
import { auth } from "../auth";
|
|
4
|
+
import { apiRes } from "@studiocubics/utils";
|
|
5
|
+
import type { PaginatedResponse } from "../clerk";
|
|
6
|
+
import type { SystemUserRoleUpdateState } from "../../ui/System/SystemUser/SystemUserRoleForm/SystemUserRoleForm";
|
|
7
|
+
import { cache } from "react";
|
|
8
|
+
import type { SystemUserDetailsUpdateState } from "../../ui/_index";
|
|
9
|
+
|
|
10
|
+
export const systemUserListReadAction = cache(
|
|
11
|
+
async (
|
|
12
|
+
params: ClerkUserListParams,
|
|
13
|
+
removeCurrent: boolean = false,
|
|
14
|
+
): Promise<PaginatedResponse<User[]>> => {
|
|
15
|
+
const session = await auth();
|
|
16
|
+
|
|
17
|
+
if (!session.hasPermission("systemUsers", "read")) {
|
|
18
|
+
throw new Error(apiRes.forbidden);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const client = await clerkClient();
|
|
22
|
+
const systemUserList = await client.users.getUserList(params);
|
|
23
|
+
|
|
24
|
+
if (removeCurrent) {
|
|
25
|
+
systemUserList.data = systemUserList.data.filter(
|
|
26
|
+
(su) => su.id !== session.userId,
|
|
27
|
+
);
|
|
28
|
+
systemUserList.totalCount--;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return systemUserList;
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export const systemUserReadAction = cache(async (id: string) => {
|
|
36
|
+
const session = await auth();
|
|
37
|
+
|
|
38
|
+
if (!session.hasPermission("systemUsers", "read")) {
|
|
39
|
+
throw new Error(apiRes.forbidden);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const client = await clerkClient();
|
|
43
|
+
const systemUser = await client.users.getUser(id);
|
|
44
|
+
|
|
45
|
+
if (!systemUser) {
|
|
46
|
+
throw new Error(apiRes.notFound(`System User: ${id}`));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return systemUser;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export async function systemUserRoleUpdateAction(
|
|
53
|
+
userId: string,
|
|
54
|
+
_: SystemUserRoleUpdateState,
|
|
55
|
+
formData: FormData,
|
|
56
|
+
): Promise<SystemUserRoleUpdateState> {
|
|
57
|
+
const session = await auth();
|
|
58
|
+
|
|
59
|
+
if (!session.hasPermission("systemUsers", "update")) {
|
|
60
|
+
throw new Error(apiRes.forbidden);
|
|
61
|
+
}
|
|
62
|
+
const role = formData.get("role");
|
|
63
|
+
console.log("userId", userId);
|
|
64
|
+
console.log("role", role);
|
|
65
|
+
return apiRes.actionSuccess();
|
|
66
|
+
|
|
67
|
+
// const client = await clerkClient();
|
|
68
|
+
// const { publicMetadata } = await client.users.getUser(userId);
|
|
69
|
+
// const patchedUser = await client.users.updateUserMetadata(userId, {
|
|
70
|
+
// publicMetadata: {
|
|
71
|
+
// ...publicMetadata,
|
|
72
|
+
// role,
|
|
73
|
+
// },
|
|
74
|
+
// });
|
|
75
|
+
// return apiRes.actionSuccess(patchedUser);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function systemUserDetailsUpdateAction(
|
|
79
|
+
userId: string,
|
|
80
|
+
_: SystemUserDetailsUpdateState,
|
|
81
|
+
formData: FormData,
|
|
82
|
+
): Promise<SystemUserDetailsUpdateState> {
|
|
83
|
+
const session = await auth();
|
|
84
|
+
|
|
85
|
+
if (!session.hasPermission("systemUsers", "update")) {
|
|
86
|
+
throw new Error(apiRes.forbidden);
|
|
87
|
+
}
|
|
88
|
+
console.log("firstName", formData.get("firstName"));
|
|
89
|
+
console.log("lastName", formData.get("firstName"));
|
|
90
|
+
console.log("imageFile", formData.get("imageFile"));
|
|
91
|
+
|
|
92
|
+
console.log("userId", userId);
|
|
93
|
+
return apiRes.actionSuccess();
|
|
94
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
import { auth as clerkAuth } from "@clerk/nextjs/server";
|
|
3
|
+
import { hasPermissionForClaims } from "./hasPermission";
|
|
4
|
+
import type { Resource, RoleActions } from "./rbacConfig";
|
|
5
|
+
type ClerkAuthResult = Awaited<ReturnType<typeof clerkAuth>>;
|
|
6
|
+
|
|
7
|
+
export type AuthWithPermissions = ClerkAuthResult & {
|
|
8
|
+
hasPermission: (
|
|
9
|
+
resource: Resource,
|
|
10
|
+
action: RoleActions,
|
|
11
|
+
record?: Record<string, any>,
|
|
12
|
+
) => Promise<boolean>;
|
|
13
|
+
};
|
|
14
|
+
export async function auth(
|
|
15
|
+
options?: Parameters<typeof clerkAuth>[0],
|
|
16
|
+
): Promise<AuthWithPermissions> {
|
|
17
|
+
const clerkAuthResult = await clerkAuth(options);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
...clerkAuthResult,
|
|
21
|
+
hasPermission: async (
|
|
22
|
+
resource: Resource,
|
|
23
|
+
action: RoleActions,
|
|
24
|
+
record?: Record<string, any>,
|
|
25
|
+
) => {
|
|
26
|
+
return hasPermissionForClaims({
|
|
27
|
+
claims: clerkAuthResult.sessionClaims,
|
|
28
|
+
resource,
|
|
29
|
+
action,
|
|
30
|
+
record,
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { EmailAddress, User } from "@clerk/nextjs/server";
|
|
2
|
+
import { Role } from "@studiocubics/cms";
|
|
3
|
+
import { WithSign } from "@/clerk/Users";
|
|
4
|
+
import { useUser } from "@clerk/nextjs";
|
|
5
|
+
|
|
6
|
+
type WithSign<T extends string> = `+${T}` | `-${T}` | T;
|
|
7
|
+
export type PaginatedResponse<T> = { data: T; totalCount: number };
|
|
8
|
+
type SessionStatus =
|
|
9
|
+
| "abandoned"
|
|
10
|
+
| "active"
|
|
11
|
+
| "ended"
|
|
12
|
+
| "expired"
|
|
13
|
+
| "removed"
|
|
14
|
+
| "replaced"
|
|
15
|
+
| "revoked";
|
|
16
|
+
|
|
17
|
+
type UserListOrderBy =
|
|
18
|
+
| "created_at"
|
|
19
|
+
| "updated_at"
|
|
20
|
+
| "email_address"
|
|
21
|
+
| "web3wallet"
|
|
22
|
+
| "first_name"
|
|
23
|
+
| "last_name"
|
|
24
|
+
| "phone_number"
|
|
25
|
+
| "username"
|
|
26
|
+
| "last_active_at"
|
|
27
|
+
| "last_sign_in_at";
|
|
28
|
+
|
|
29
|
+
declare global {
|
|
30
|
+
type ClerkClientUser = ReturnType<typeof useUser>["user"];
|
|
31
|
+
type ClerkSessionClaims = NonNullable<
|
|
32
|
+
Awaited<ReturnType<typeof auth>>["sessionClaims"]
|
|
33
|
+
>;
|
|
34
|
+
interface CustomJwtSessionClaims {
|
|
35
|
+
metadata: {
|
|
36
|
+
role?: Role;
|
|
37
|
+
onboardingComplete?: boolean;
|
|
38
|
+
// TODO add currentStatus?:"online"|"offline"
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
interface ClerkUserListParams extends Record<string, any> {
|
|
42
|
+
limit?: number;
|
|
43
|
+
offset?: number;
|
|
44
|
+
orderBy?: WithSign<UserListOrderBy>;
|
|
45
|
+
query?: string;
|
|
46
|
+
}
|
|
47
|
+
interface ClerkUserSessionListParams {
|
|
48
|
+
clientId?: string;
|
|
49
|
+
userId?: string;
|
|
50
|
+
status?: SessionStatus;
|
|
51
|
+
limit?: number;
|
|
52
|
+
offset?: number;
|
|
53
|
+
}
|
|
54
|
+
interface ClerkUserSession {
|
|
55
|
+
id: string;
|
|
56
|
+
clientId: string;
|
|
57
|
+
userId: string;
|
|
58
|
+
status: SessionStatus;
|
|
59
|
+
lastActiveAt: number;
|
|
60
|
+
expireAt: number;
|
|
61
|
+
abandonAt: number;
|
|
62
|
+
createdAt: number;
|
|
63
|
+
updatedAt: number;
|
|
64
|
+
lastActiveOrganizationId?: string;
|
|
65
|
+
latestActivity?: ClerkUserSessionActivity;
|
|
66
|
+
actor: Record<string, unknown> | null;
|
|
67
|
+
}
|
|
68
|
+
interface ClerkUserSessionActivity {
|
|
69
|
+
id: string;
|
|
70
|
+
isMobile: boolean;
|
|
71
|
+
ipAddress?: string;
|
|
72
|
+
city?: string;
|
|
73
|
+
country?: string;
|
|
74
|
+
browserVersion?: string;
|
|
75
|
+
browserName?: string;
|
|
76
|
+
deviceType?: string;
|
|
77
|
+
}
|
|
78
|
+
interface ClerkInvitation {
|
|
79
|
+
id: string;
|
|
80
|
+
emailAddress: string;
|
|
81
|
+
url: string;
|
|
82
|
+
publicMetadata: UserPublicMetadata;
|
|
83
|
+
createdAt: number;
|
|
84
|
+
updatedAt: number;
|
|
85
|
+
status: "pending" | "accepted" | "revoked";
|
|
86
|
+
revoked?: boolean;
|
|
87
|
+
}
|
|
88
|
+
interface ClerkInvitationCreateParams {
|
|
89
|
+
emailAddress: string;
|
|
90
|
+
expiresInDays?: number;
|
|
91
|
+
ignoreExisting?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Whether an email invitation should be sent to the given email address. Defaults to true.
|
|
94
|
+
*/
|
|
95
|
+
notify?: boolean;
|
|
96
|
+
publicMetadata?: UserPublicMetadata;
|
|
97
|
+
redirectUrl?: string;
|
|
98
|
+
templateSlug?: "invitation" | "waitlist_invitation";
|
|
99
|
+
}
|
|
100
|
+
interface ClerkInvitationListParams extends Record<string, any> {
|
|
101
|
+
status?: "pending" | "accepted" | "revoked";
|
|
102
|
+
limit?: number;
|
|
103
|
+
offset?: number;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
import { auth } from "@clerk/nextjs/server";
|
|
3
|
+
import {
|
|
4
|
+
type Resource,
|
|
5
|
+
type RoleActions,
|
|
6
|
+
type Role,
|
|
7
|
+
RBAC_CONFIG,
|
|
8
|
+
} from "./rbacConfig";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Evaluates RBAC permissions using explicitly provided session claims.
|
|
12
|
+
*
|
|
13
|
+
* @returns `true` if the role defined in `sessionClaims.metadata.role`
|
|
14
|
+
* is allowed to perform `action` on `resource` under RBAC rules.
|
|
15
|
+
*/
|
|
16
|
+
export async function hasPermissionForClaims(args: {
|
|
17
|
+
claims: ClerkSessionClaims;
|
|
18
|
+
resource: Resource;
|
|
19
|
+
action: RoleActions;
|
|
20
|
+
record?: Record<string, any>;
|
|
21
|
+
}): Promise<boolean> {
|
|
22
|
+
if (!("metadata" in args.claims)) return false;
|
|
23
|
+
const role = args.claims?.metadata.role;
|
|
24
|
+
if (!role) return false;
|
|
25
|
+
return checkPermissions({ role, ...args });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Evaluates RBAC permissions for the currently authenticated user.
|
|
30
|
+
*
|
|
31
|
+
* Use this when you do NOT already have session claims.
|
|
32
|
+
*
|
|
33
|
+
* @returns `true` if the current user's role is allowed
|
|
34
|
+
* to perform `action` on `resource` under RBAC rules.
|
|
35
|
+
*/
|
|
36
|
+
export async function hasPermission(args: {
|
|
37
|
+
resource: Resource;
|
|
38
|
+
action: RoleActions;
|
|
39
|
+
record?: Record<string, any>;
|
|
40
|
+
}): Promise<boolean> {
|
|
41
|
+
let claims;
|
|
42
|
+
|
|
43
|
+
const a = await auth();
|
|
44
|
+
claims = a.sessionClaims;
|
|
45
|
+
|
|
46
|
+
const role = claims?.metadata.role;
|
|
47
|
+
if (!role) return false;
|
|
48
|
+
return checkPermissions({
|
|
49
|
+
role,
|
|
50
|
+
claims,
|
|
51
|
+
...args,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
export type PermissionCheckInput = {
|
|
55
|
+
role: Role;
|
|
56
|
+
claims: ClerkSessionClaims;
|
|
57
|
+
resource: Resource;
|
|
58
|
+
action: RoleActions;
|
|
59
|
+
record?: Record<string, any>;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Core RBAC policy evaluator.
|
|
64
|
+
*
|
|
65
|
+
* This is the authorization engine shared by all permission checks.
|
|
66
|
+
* It does NOT perform authentication or session resolution.
|
|
67
|
+
*
|
|
68
|
+
* Rules:
|
|
69
|
+
* - `isSystem` roles bypass all checks
|
|
70
|
+
* - permissions are matched by `{ resource, action }`
|
|
71
|
+
* - optional ownership constraints are enforced via `ownerField`
|
|
72
|
+
* @param {PermissionCheckInput} args
|
|
73
|
+
* @returns `true` if at least one permission grants access
|
|
74
|
+
*/
|
|
75
|
+
function checkPermissions(args: PermissionCheckInput) {
|
|
76
|
+
const def = RBAC_CONFIG[args.role];
|
|
77
|
+
if (!def) return false;
|
|
78
|
+
|
|
79
|
+
// Allow system
|
|
80
|
+
if (def.isSystem) return true;
|
|
81
|
+
|
|
82
|
+
// Iterate over all permissions
|
|
83
|
+
for (const p of def.permissions) {
|
|
84
|
+
if (p.resource !== args.resource) continue;
|
|
85
|
+
if (!p.actions?.[args.action]) continue;
|
|
86
|
+
|
|
87
|
+
// Ownership conditions
|
|
88
|
+
if (p.conditions?.ownerField && args.record) {
|
|
89
|
+
const ownerField = p.conditions.ownerField;
|
|
90
|
+
if (args.record[ownerField] !== args.claims.id) continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export const roles = ["admin", "manager", "member"] as const;
|
|
2
|
+
export const resources = ["systemUsers", "projects", "invitations"] as const;
|
|
3
|
+
|
|
4
|
+
export const RBAC_CONFIG: {
|
|
5
|
+
[K in Role]: RoleDoc;
|
|
6
|
+
} = {
|
|
7
|
+
admin: {
|
|
8
|
+
desc: "Full system authority with unrestricted access and management capabilities.",
|
|
9
|
+
isSystem: true,
|
|
10
|
+
permissions: [], // system role bypasses all checks
|
|
11
|
+
},
|
|
12
|
+
manager: {
|
|
13
|
+
desc: "Responsible for overseeing teams and resources; can manage existing items and invite users.",
|
|
14
|
+
inherits: ["member"],
|
|
15
|
+
permissions: [
|
|
16
|
+
{
|
|
17
|
+
resource: "systemUsers",
|
|
18
|
+
actions: { create: true, read: true, update: true },
|
|
19
|
+
conditions: { ownerField: "id" },
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
resource: "projects",
|
|
23
|
+
actions: { create: true, read: true, update: true, delete: true },
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
resource: "invitations",
|
|
27
|
+
actions: { create: true, read: true, update: true, delete: true },
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
member: {
|
|
32
|
+
desc: "Standard user with read-level access to assigned resources; limited modification rights.",
|
|
33
|
+
permissions: [
|
|
34
|
+
{ resource: "systemUsers", actions: { read: true } },
|
|
35
|
+
{ resource: "projects", actions: { read: true } },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
40
|
+
export type Role = (typeof roles)[number];
|
|
41
|
+
export type Resource = (typeof resources)[number];
|
|
42
|
+
|
|
43
|
+
export type RoleActions = "create" | "read" | "update" | "delete";
|
|
44
|
+
|
|
45
|
+
export type ResourcePermission = {
|
|
46
|
+
resource: Resource;
|
|
47
|
+
actions: Partial<Record<RoleActions, boolean>>;
|
|
48
|
+
conditions?: {
|
|
49
|
+
ownerField?: string; // e.g. “creatorId”
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
export type RoleDoc = {
|
|
53
|
+
desc?: string;
|
|
54
|
+
inherits?: string[];
|
|
55
|
+
isSystem?: boolean;
|
|
56
|
+
permissions: ResourcePermission[];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const getInvitationPublicMetadata = (role: Role) => {
|
|
60
|
+
switch (role) {
|
|
61
|
+
case "admin":
|
|
62
|
+
return { role, onboardingComplete: true };
|
|
63
|
+
case "manager":
|
|
64
|
+
return { role, onboardingComplete: true };
|
|
65
|
+
default:
|
|
66
|
+
return { role, onboardingComplete: false };
|
|
67
|
+
}
|
|
68
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./invitation";
|