@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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AccordionItem,
|
|
3
|
+
CollectionItemCard,
|
|
4
|
+
SectionWrapper,
|
|
5
|
+
} from "@studiocubics/ui";
|
|
6
|
+
import { RolePermissionsTable } from "./RolePermissionsTable";
|
|
7
|
+
import type { Role, RoleDoc } from "../../../clerk/rbacConfig";
|
|
8
|
+
|
|
9
|
+
export interface RoleCardProps {
|
|
10
|
+
role: Role;
|
|
11
|
+
rbacConfig: RoleDoc;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function RoleListCard(props: RoleCardProps) {
|
|
15
|
+
const { role, rbacConfig } = props;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<AccordionItem
|
|
19
|
+
name="RoleCard"
|
|
20
|
+
summary={
|
|
21
|
+
<CollectionItemCard
|
|
22
|
+
title={role}
|
|
23
|
+
chip={rbacConfig.isSystem ? "System User" : ""}
|
|
24
|
+
description={rbacConfig.desc}
|
|
25
|
+
/>
|
|
26
|
+
}
|
|
27
|
+
>
|
|
28
|
+
<SectionWrapper title={`${role}'s Permissions`} noBorders>
|
|
29
|
+
<RolePermissionsTable {...props} />
|
|
30
|
+
</SectionWrapper>
|
|
31
|
+
</AccordionItem>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { PageLayoutPagination, type TabProps } from "@studiocubics/ui";
|
|
2
|
+
import { RoleListCard } from "./RoleListCard";
|
|
3
|
+
import { RBAC_CONFIG } from "../../../clerk/rbacConfig";
|
|
4
|
+
|
|
5
|
+
export function RolePermissionsPage({
|
|
6
|
+
securityLinks,
|
|
7
|
+
}: {
|
|
8
|
+
securityLinks: TabProps[];
|
|
9
|
+
}) {
|
|
10
|
+
return (
|
|
11
|
+
<PageLayoutPagination size="sm" title={securityLinks[1]?.children}>
|
|
12
|
+
{(Object.keys(RBAC_CONFIG) as (keyof typeof RBAC_CONFIG)[]).map((k) => {
|
|
13
|
+
const item = RBAC_CONFIG[k];
|
|
14
|
+
return <RoleListCard key={k} role={k} rbacConfig={item} />;
|
|
15
|
+
})}
|
|
16
|
+
</PageLayoutPagination>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Check, X } from "lucide-react";
|
|
4
|
+
import type { RoleCardProps } from "./RoleListCard";
|
|
5
|
+
import { useCallback, useMemo } from "react";
|
|
6
|
+
import { Table, type TableData } from "@studiocubics/ui";
|
|
7
|
+
|
|
8
|
+
export function RolePermissionsTable({ rbacConfig }: RoleCardProps) {
|
|
9
|
+
const permissions = rbacConfig.permissions;
|
|
10
|
+
const icon = useCallback((check?: boolean) => {
|
|
11
|
+
return check ? <Check /> : <X />;
|
|
12
|
+
}, []);
|
|
13
|
+
const tableData: TableData = useMemo(
|
|
14
|
+
() => [
|
|
15
|
+
["", "create", "delete", "read", "update"],
|
|
16
|
+
...permissions.map((p) => [
|
|
17
|
+
p.resource,
|
|
18
|
+
icon(p.actions.create),
|
|
19
|
+
icon(p.actions.delete),
|
|
20
|
+
icon(p.actions.read),
|
|
21
|
+
icon(p.actions.update),
|
|
22
|
+
]),
|
|
23
|
+
],
|
|
24
|
+
[permissions],
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (!permissions || !permissions.length)
|
|
28
|
+
return (
|
|
29
|
+
<p>
|
|
30
|
+
This is a root system user role, all resource actions are permitted to
|
|
31
|
+
anyone with this role. Make sure to always have atleast one admin user.
|
|
32
|
+
</p>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return <Table data={tableData} />;
|
|
36
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useClerk, useUser } from "@clerk/nextjs";
|
|
4
|
+
import { useAnchorElement, useDisclosure } from "@studiocubics/hooks";
|
|
5
|
+
import {
|
|
6
|
+
Chip,
|
|
7
|
+
ConfirmationForm,
|
|
8
|
+
Dialog,
|
|
9
|
+
IdentityDisplay,
|
|
10
|
+
type IdentityDisplayProps,
|
|
11
|
+
List,
|
|
12
|
+
ListItem,
|
|
13
|
+
Popover,
|
|
14
|
+
Skeleton,
|
|
15
|
+
} from "@studiocubics/ui";
|
|
16
|
+
import { toCapitalised } from "@studiocubics/utils";
|
|
17
|
+
import { LogOut, UserCog } from "lucide-react";
|
|
18
|
+
import type { SubmitEvent } from "react";
|
|
19
|
+
import styles from "./CurrentSystemUserButton.module.css";
|
|
20
|
+
import type { Role } from "../../../../clerk/rbacConfig";
|
|
21
|
+
|
|
22
|
+
export function CurrentSystemUserButton(
|
|
23
|
+
props: { clickable?: boolean } & Omit<IdentityDisplayProps, "profileName">,
|
|
24
|
+
) {
|
|
25
|
+
const user = useUser();
|
|
26
|
+
|
|
27
|
+
const { clickable = true, ...rest } = props;
|
|
28
|
+
const { open, anchorEl, handleClick, handleClose } = useAnchorElement();
|
|
29
|
+
|
|
30
|
+
if (!user.isLoaded) return <Skeleton height={78} />;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
<IdentityDisplay
|
|
35
|
+
onClick={clickable ? handleClick : undefined}
|
|
36
|
+
profileImage={user.user?.imageUrl ?? ""}
|
|
37
|
+
role={user.user?.primaryEmailAddress?.emailAddress}
|
|
38
|
+
{...rest}
|
|
39
|
+
profileName={
|
|
40
|
+
<div className={styles.title}>
|
|
41
|
+
<h4>{user.user?.fullName}</h4>{" "}
|
|
42
|
+
<Chip size="sm" color="primary">
|
|
43
|
+
<strong>
|
|
44
|
+
{toCapitalised(user.user?.publicMetadata.role as Role)}
|
|
45
|
+
</strong>
|
|
46
|
+
</Chip>
|
|
47
|
+
</div>
|
|
48
|
+
}
|
|
49
|
+
/>
|
|
50
|
+
{clickable && (
|
|
51
|
+
<Popover
|
|
52
|
+
anchorEl={anchorEl}
|
|
53
|
+
open={open}
|
|
54
|
+
onClose={handleClose}
|
|
55
|
+
anchorPosition={{ vertical: "top", horizontal: "right" }}
|
|
56
|
+
transformOrigin={{ vertical: "bottom", horizontal: "left" }}
|
|
57
|
+
>
|
|
58
|
+
<List className="surfaceContainer">
|
|
59
|
+
<ListItem
|
|
60
|
+
startIcon={<UserCog />}
|
|
61
|
+
href="/dashboard/security/account"
|
|
62
|
+
>
|
|
63
|
+
Account Settings
|
|
64
|
+
</ListItem>
|
|
65
|
+
<Logout />
|
|
66
|
+
</List>
|
|
67
|
+
</Popover>
|
|
68
|
+
)}
|
|
69
|
+
</>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
export function Logout() {
|
|
73
|
+
const { open, handleClose, handleStrictClose, handleOpen } = useDisclosure();
|
|
74
|
+
const clerk = useClerk();
|
|
75
|
+
async function handleSubmit(e: SubmitEvent<HTMLFormElement>) {
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
await clerk.signOut();
|
|
78
|
+
}
|
|
79
|
+
return (
|
|
80
|
+
<>
|
|
81
|
+
<ListItem
|
|
82
|
+
className={styles.logoutButton}
|
|
83
|
+
onClick={handleOpen}
|
|
84
|
+
// disabled
|
|
85
|
+
startIcon={<LogOut />}
|
|
86
|
+
color="error"
|
|
87
|
+
>
|
|
88
|
+
Log out
|
|
89
|
+
</ListItem>
|
|
90
|
+
<Dialog open={open} onClose={handleStrictClose}>
|
|
91
|
+
<ConfirmationForm
|
|
92
|
+
variant="danger"
|
|
93
|
+
formTitle="Logout"
|
|
94
|
+
onCancel={handleClose}
|
|
95
|
+
onSubmit={handleSubmit}
|
|
96
|
+
>
|
|
97
|
+
Are you sure you want to sign out of this account?
|
|
98
|
+
</ConfirmationForm>
|
|
99
|
+
</Dialog>
|
|
100
|
+
</>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { currentUser } from "@clerk/nextjs/server";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import { SystemUserPageContent } from "./SystemUserPageContent";
|
|
4
|
+
import { auth } from "../../../clerk/auth";
|
|
5
|
+
|
|
6
|
+
export async function CurrentSystemUserPage() {
|
|
7
|
+
const user = await currentUser();
|
|
8
|
+
const session = await auth();
|
|
9
|
+
const allowEdit = await session.hasPermission("systemUsers", "update");
|
|
10
|
+
if (!user) redirect("/auth/signIn/");
|
|
11
|
+
return <SystemUserPageContent user={user} allowEdit={allowEdit} />;
|
|
12
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { User } from "@clerk/nextjs/server";
|
|
4
|
+
import { useDisclosure } from "@studiocubics/hooks";
|
|
5
|
+
import {
|
|
6
|
+
ConfirmationForm,
|
|
7
|
+
Dialog,
|
|
8
|
+
ListItem,
|
|
9
|
+
type ListItemProps,
|
|
10
|
+
TextInput,
|
|
11
|
+
} from "@studiocubics/ui";
|
|
12
|
+
import { Trash } from "lucide-react";
|
|
13
|
+
|
|
14
|
+
export function SystemUserDeleteListItem(
|
|
15
|
+
props: ListItemProps & { uid: User["id"]; fullName?: User["fullName"] },
|
|
16
|
+
) {
|
|
17
|
+
const { uid, fullName, ...rest } = props;
|
|
18
|
+
const { open, handleOpen, handleStrictClose, handleClose } = useDisclosure();
|
|
19
|
+
return (
|
|
20
|
+
<>
|
|
21
|
+
<ListItem
|
|
22
|
+
{...rest}
|
|
23
|
+
color="error"
|
|
24
|
+
startIcon={<Trash />}
|
|
25
|
+
onClick={handleOpen}
|
|
26
|
+
>
|
|
27
|
+
Delete User
|
|
28
|
+
</ListItem>
|
|
29
|
+
<Dialog open={open} onClose={handleStrictClose}>
|
|
30
|
+
<ConfirmationForm
|
|
31
|
+
formTitle="Delete System User"
|
|
32
|
+
variant="danger"
|
|
33
|
+
onCancel={handleClose}
|
|
34
|
+
>
|
|
35
|
+
Are you sure you want to delete {fullName ? `${fullName}'s` : "this"}{" "}
|
|
36
|
+
system account?
|
|
37
|
+
<br />
|
|
38
|
+
All information will be deleted, this action is irreversible and
|
|
39
|
+
cannot be undone.
|
|
40
|
+
<TextInput fullWidth />
|
|
41
|
+
</ConfirmationForm>
|
|
42
|
+
</Dialog>
|
|
43
|
+
</>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button, IdentityDisplay, TransitionAnimation } from "@studiocubics/ui";
|
|
4
|
+
import styles from "./SystemUserDetails.module.css";
|
|
5
|
+
import { Edit } from "lucide-react";
|
|
6
|
+
import { useDisclosure } from "@studiocubics/hooks";
|
|
7
|
+
import { type User } from "@clerk/nextjs/server";
|
|
8
|
+
import { SystemUserDetailsForm } from "../SystemUserDetailsForm/SystemUserDetailsForm";
|
|
9
|
+
|
|
10
|
+
interface SystemUserDetailsProps {
|
|
11
|
+
id: User["id"];
|
|
12
|
+
fullName: User["fullName"];
|
|
13
|
+
firstName: User["firstName"];
|
|
14
|
+
lastName?: User["lastName"];
|
|
15
|
+
imageUrl: User["imageUrl"];
|
|
16
|
+
emailAddress?: NonNullable<User["primaryEmailAddress"]>["emailAddress"];
|
|
17
|
+
editable?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export function SystemUserDetails({
|
|
20
|
+
id,
|
|
21
|
+
fullName,
|
|
22
|
+
firstName,
|
|
23
|
+
lastName,
|
|
24
|
+
imageUrl,
|
|
25
|
+
emailAddress,
|
|
26
|
+
editable,
|
|
27
|
+
}: SystemUserDetailsProps) {
|
|
28
|
+
const { open, handleOpen, handleClose } = useDisclosure();
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
<TransitionAnimation
|
|
32
|
+
in={open}
|
|
33
|
+
transformOrigin={"top right"}
|
|
34
|
+
mountOnly
|
|
35
|
+
unmountOnExit
|
|
36
|
+
>
|
|
37
|
+
<SystemUserDetailsForm
|
|
38
|
+
imageUrl={imageUrl}
|
|
39
|
+
firstName={firstName}
|
|
40
|
+
lastName={lastName}
|
|
41
|
+
onClose={handleClose}
|
|
42
|
+
/>
|
|
43
|
+
</TransitionAnimation>
|
|
44
|
+
<TransitionAnimation
|
|
45
|
+
in={!open}
|
|
46
|
+
transformOrigin={"top"}
|
|
47
|
+
mountOnly
|
|
48
|
+
unmountOnExit
|
|
49
|
+
>
|
|
50
|
+
<div className={styles.root}>
|
|
51
|
+
<IdentityDisplay
|
|
52
|
+
profileName={fullName}
|
|
53
|
+
profileImage={imageUrl}
|
|
54
|
+
role={
|
|
55
|
+
<p>
|
|
56
|
+
{emailAddress}
|
|
57
|
+
<br />
|
|
58
|
+
<strong>{id}</strong>
|
|
59
|
+
</p>
|
|
60
|
+
}
|
|
61
|
+
/>
|
|
62
|
+
{editable && (
|
|
63
|
+
<Button size="sm" startIcon={<Edit />} onClick={handleOpen}>
|
|
64
|
+
Edit Details
|
|
65
|
+
</Button>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
</TransitionAnimation>
|
|
69
|
+
</>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { User } from "@clerk/nextjs/server";
|
|
4
|
+
import {
|
|
5
|
+
type CardProps,
|
|
6
|
+
Card,
|
|
7
|
+
ConfirmationForm,
|
|
8
|
+
IdentityDisplay,
|
|
9
|
+
Button,
|
|
10
|
+
TextInput,
|
|
11
|
+
toast,
|
|
12
|
+
} from "@studiocubics/ui";
|
|
13
|
+
import styles from "./SystemUserDetailsForm.module.css";
|
|
14
|
+
import { initialiseForm } from "@studiocubics/utils";
|
|
15
|
+
import { systemUserDetailsUpdateAction } from "../../../../clerk/_index";
|
|
16
|
+
import { useActionState, useEffect, useRef, useState } from "react";
|
|
17
|
+
|
|
18
|
+
const initialSystemUserDetailsUpdateState = initialiseForm(
|
|
19
|
+
"firstName",
|
|
20
|
+
"lastName",
|
|
21
|
+
"imageUrl",
|
|
22
|
+
);
|
|
23
|
+
export type SystemUserDetailsUpdateState =
|
|
24
|
+
typeof initialSystemUserDetailsUpdateState;
|
|
25
|
+
|
|
26
|
+
export function SystemUserDetailsForm({
|
|
27
|
+
imageUrl,
|
|
28
|
+
firstName,
|
|
29
|
+
lastName,
|
|
30
|
+
onClose,
|
|
31
|
+
...rest
|
|
32
|
+
}: {
|
|
33
|
+
imageUrl?: User["imageUrl"];
|
|
34
|
+
firstName?: User["firstName"];
|
|
35
|
+
lastName?: User["lastName"];
|
|
36
|
+
onClose(): void;
|
|
37
|
+
} & CardProps) {
|
|
38
|
+
const [state, action, pending] = useActionState(
|
|
39
|
+
systemUserDetailsUpdateAction.bind(null, "hello"),
|
|
40
|
+
initialSystemUserDetailsUpdateState,
|
|
41
|
+
);
|
|
42
|
+
const [imagePreview, setImagePreview] = useState(imageUrl);
|
|
43
|
+
const [inputImage, setInputImage] = useState<File>();
|
|
44
|
+
const imageInputRef = useRef<HTMLInputElement | null>(null);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (inputImage) setImagePreview(URL.createObjectURL(inputImage));
|
|
48
|
+
}, [inputImage]);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Card fullWidth {...rest}>
|
|
52
|
+
<ConfirmationForm
|
|
53
|
+
formTitle="Edit Details"
|
|
54
|
+
onCancel={onClose}
|
|
55
|
+
action={action}
|
|
56
|
+
disabled={pending}
|
|
57
|
+
>
|
|
58
|
+
<IdentityDisplay
|
|
59
|
+
profileImage={imagePreview}
|
|
60
|
+
profileName={
|
|
61
|
+
<div className={styles.formRow}>
|
|
62
|
+
<Button
|
|
63
|
+
size="sm"
|
|
64
|
+
variant="outlined"
|
|
65
|
+
disabled={pending}
|
|
66
|
+
onClick={() => imageInputRef.current?.click()}
|
|
67
|
+
>
|
|
68
|
+
Upload
|
|
69
|
+
</Button>
|
|
70
|
+
<input
|
|
71
|
+
type="file"
|
|
72
|
+
accept="image/jpg, image/png"
|
|
73
|
+
ref={imageInputRef}
|
|
74
|
+
name={"imageFile"}
|
|
75
|
+
onChange={(e) =>
|
|
76
|
+
e.target?.files && setInputImage(e.target.files[0])
|
|
77
|
+
}
|
|
78
|
+
hidden
|
|
79
|
+
disabled={pending}
|
|
80
|
+
/>
|
|
81
|
+
<Button
|
|
82
|
+
size="sm"
|
|
83
|
+
color="error"
|
|
84
|
+
onClick={() => toast("hello")}
|
|
85
|
+
disabled={pending}
|
|
86
|
+
>
|
|
87
|
+
Remove
|
|
88
|
+
</Button>
|
|
89
|
+
</div>
|
|
90
|
+
}
|
|
91
|
+
role={"Recommended size 1:1, up to 10MB."}
|
|
92
|
+
/>
|
|
93
|
+
<div className={styles.formRow}>
|
|
94
|
+
<TextInput
|
|
95
|
+
fullWidth
|
|
96
|
+
label="First Name"
|
|
97
|
+
defaultValue={firstName!}
|
|
98
|
+
name="firstName"
|
|
99
|
+
error={state.fieldErrors?.firstName}
|
|
100
|
+
disabled={pending}
|
|
101
|
+
/>
|
|
102
|
+
<TextInput
|
|
103
|
+
fullWidth
|
|
104
|
+
label="Last Name"
|
|
105
|
+
defaultValue={lastName!}
|
|
106
|
+
name="lastName"
|
|
107
|
+
error={state.fieldErrors?.lastName}
|
|
108
|
+
disabled={pending}
|
|
109
|
+
/>
|
|
110
|
+
</div>
|
|
111
|
+
</ConfirmationForm>
|
|
112
|
+
</Card>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { systemUserListReadAction } from "../../../clerk/actions/systemUsers";
|
|
2
|
+
import type { Role } from "../../../clerk/rbacConfig";
|
|
3
|
+
import { SystemUserListCard } from "./SystemUserListCard";
|
|
4
|
+
|
|
5
|
+
export async function SystemUserList(params: ClerkUserListParams) {
|
|
6
|
+
const systemUserList = await systemUserListReadAction(params);
|
|
7
|
+
return systemUserList.data.map((user) => (
|
|
8
|
+
<SystemUserListCard
|
|
9
|
+
key={user.id}
|
|
10
|
+
id={user.id}
|
|
11
|
+
fullName={user.fullName}
|
|
12
|
+
imageUrl={user.imageUrl}
|
|
13
|
+
emailAddress={user.primaryEmailAddress?.emailAddress}
|
|
14
|
+
role={user.publicMetadata.role as Role}
|
|
15
|
+
lastSignInAt={user.lastSignInAt}
|
|
16
|
+
/>
|
|
17
|
+
));
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Button } from "@studiocubics/ui";
|
|
2
|
+
import { Search, ChartNoAxesGantt } from "lucide-react";
|
|
3
|
+
import { InvitationCreateDialog } from "../Invitations/InvitationListActions";
|
|
4
|
+
|
|
5
|
+
export function SystemUserListActions() {
|
|
6
|
+
return (
|
|
7
|
+
<>
|
|
8
|
+
<InvitationCreateDialog />
|
|
9
|
+
<Button size="sm" square>
|
|
10
|
+
<Search />
|
|
11
|
+
</Button>
|
|
12
|
+
<Button size="sm" square>
|
|
13
|
+
<ChartNoAxesGantt />
|
|
14
|
+
</Button>
|
|
15
|
+
</>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { User } from "@clerk/nextjs/server";
|
|
4
|
+
import {
|
|
5
|
+
CollectionItemCard,
|
|
6
|
+
CollectionItemCardActions,
|
|
7
|
+
type DocumentAction,
|
|
8
|
+
List,
|
|
9
|
+
ListItem,
|
|
10
|
+
} from "@studiocubics/ui";
|
|
11
|
+
import { toCapitalised } from "@studiocubics/utils";
|
|
12
|
+
import {
|
|
13
|
+
BrickWallShield,
|
|
14
|
+
EllipsisVertical,
|
|
15
|
+
History,
|
|
16
|
+
OctagonX,
|
|
17
|
+
} from "lucide-react";
|
|
18
|
+
import Link from "next/link";
|
|
19
|
+
import { SystemUserDeleteListItem } from "./SystemUserActions";
|
|
20
|
+
import type { Route } from "next";
|
|
21
|
+
import type { Role } from "../../../clerk/rbacConfig";
|
|
22
|
+
|
|
23
|
+
export interface SystemUserListCardProps {
|
|
24
|
+
id: User["id"];
|
|
25
|
+
fullName: User["fullName"];
|
|
26
|
+
imageUrl: User["imageUrl"];
|
|
27
|
+
emailAddress?: NonNullable<User["primaryEmailAddress"]>["emailAddress"];
|
|
28
|
+
role?: Role;
|
|
29
|
+
lastSignInAt?: User["lastActiveAt"];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function SystemUserListCard(props: SystemUserListCardProps) {
|
|
33
|
+
const { id, fullName, imageUrl, emailAddress, role, lastSignInAt } = props;
|
|
34
|
+
const actions: DocumentAction[] = [
|
|
35
|
+
{
|
|
36
|
+
actionType: "link",
|
|
37
|
+
icon: <BrickWallShield />,
|
|
38
|
+
label: "View Sessions",
|
|
39
|
+
href: `/dashboard/security/systemUsers/${id}/sessions`,
|
|
40
|
+
prefetch: true,
|
|
41
|
+
// @ts-expect-error
|
|
42
|
+
// This is dumb polymorphism garbage!
|
|
43
|
+
as: Link,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
actionType: "popover",
|
|
47
|
+
icon: <EllipsisVertical />,
|
|
48
|
+
popoverChildren: (
|
|
49
|
+
<List className="surfaceContainer">
|
|
50
|
+
<SystemUserDeleteListItem uid={id} fullName={fullName} />
|
|
51
|
+
<ListItem
|
|
52
|
+
{...{
|
|
53
|
+
startIcon: <OctagonX />,
|
|
54
|
+
children: "Ban User",
|
|
55
|
+
color: "error",
|
|
56
|
+
}}
|
|
57
|
+
/>
|
|
58
|
+
</List>
|
|
59
|
+
),
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<CollectionItemCard
|
|
65
|
+
as={Link}
|
|
66
|
+
href={`/dashboard/security/systemUsers/${id}` as Route}
|
|
67
|
+
title={fullName ?? "Name is missing"}
|
|
68
|
+
thumbnail={<img src={imageUrl} alt={fullName ?? ""} />}
|
|
69
|
+
subtitle={emailAddress}
|
|
70
|
+
chip={toCapitalised(role)}
|
|
71
|
+
description={
|
|
72
|
+
lastSignInAt ? (
|
|
73
|
+
<>
|
|
74
|
+
<History size={16} />
|
|
75
|
+
 Last Active 
|
|
76
|
+
<strong>{new Date(lastSignInAt).toDateString()}</strong>
|
|
77
|
+
</>
|
|
78
|
+
) : (
|
|
79
|
+
"No activity found!"
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
footer={<CollectionItemCardActions actions={actions} />}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SYSTEM_USERS_PAGE_LIMIT as limit } from "../../../constants/pageLimits";
|
|
2
|
+
import type { SecurityPageProps } from "../types";
|
|
3
|
+
import { PageLayoutPagination } from "@studiocubics/ui";
|
|
4
|
+
import { SystemUserListActions } from "./SystemUserListActions";
|
|
5
|
+
import { SystemUserList } from "./SystemUserList";
|
|
6
|
+
import { SystemUserListPagination } from "./SystemUserListPagination";
|
|
7
|
+
|
|
8
|
+
export async function SystemUserListPage({
|
|
9
|
+
searchParams,
|
|
10
|
+
securityLinks,
|
|
11
|
+
}: SecurityPageProps) {
|
|
12
|
+
const sp = await searchParams;
|
|
13
|
+
const page = Number(sp.page ?? 1);
|
|
14
|
+
const orderBy = String(sp.orderBy ?? "created_at");
|
|
15
|
+
const query = String(sp.query ?? "");
|
|
16
|
+
const offset = (page - 1) * limit;
|
|
17
|
+
const params = {
|
|
18
|
+
limit,
|
|
19
|
+
offset,
|
|
20
|
+
orderBy,
|
|
21
|
+
query,
|
|
22
|
+
};
|
|
23
|
+
return (
|
|
24
|
+
<PageLayoutPagination
|
|
25
|
+
actions={<SystemUserListActions />}
|
|
26
|
+
size="sm"
|
|
27
|
+
title={securityLinks[2]?.children}
|
|
28
|
+
paginationComponent={<SystemUserListPagination page={page} {...params} />}
|
|
29
|
+
>
|
|
30
|
+
<SystemUserList {...params} />
|
|
31
|
+
</PageLayoutPagination>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { NextSSRPagination } from "@studiocubics/next";
|
|
2
|
+
import { systemUserListReadAction } from "../../../clerk/actions/systemUsers";
|
|
3
|
+
|
|
4
|
+
export async function SystemUserListPagination({
|
|
5
|
+
page,
|
|
6
|
+
...params
|
|
7
|
+
}: {
|
|
8
|
+
page: number;
|
|
9
|
+
limit: number;
|
|
10
|
+
} & ClerkUserListParams) {
|
|
11
|
+
const systemUsersList = await systemUserListReadAction(params);
|
|
12
|
+
return (
|
|
13
|
+
<NextSSRPagination
|
|
14
|
+
page={page}
|
|
15
|
+
limit={params.limit}
|
|
16
|
+
total={systemUsersList.totalCount}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Breadcrumbs, PageTitle } from "@studiocubics/ui";
|
|
2
|
+
import { SystemUserPageContent } from "./SystemUserPageContent";
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { auth } from "../../../clerk/auth";
|
|
5
|
+
import { systemUserReadAction } from "../../../clerk/actions/systemUsers";
|
|
6
|
+
export async function SystemUserPage({
|
|
7
|
+
params,
|
|
8
|
+
}: PageProps<"/dashboard/security/systemUsers/[userId]">) {
|
|
9
|
+
const userId = (await params).userId;
|
|
10
|
+
const session = await auth();
|
|
11
|
+
const allowEdit = await session.hasPermission("systemUsers", "update");
|
|
12
|
+
const user = await systemUserReadAction(userId);
|
|
13
|
+
return (
|
|
14
|
+
<>
|
|
15
|
+
<PageTitle
|
|
16
|
+
size="sm"
|
|
17
|
+
title={
|
|
18
|
+
<Breadcrumbs defaultActive={1}>
|
|
19
|
+
<h4>
|
|
20
|
+
<Link href={"/dashboard/security/systemUsers"}>System Users</Link>
|
|
21
|
+
</h4>
|
|
22
|
+
<h4>{user.fullName}'s Account</h4>
|
|
23
|
+
</Breadcrumbs>
|
|
24
|
+
}
|
|
25
|
+
noBorders
|
|
26
|
+
/>
|
|
27
|
+
<SystemUserPageContent user={user} allowEdit={allowEdit} />
|
|
28
|
+
</>
|
|
29
|
+
);
|
|
30
|
+
}
|