docs: added rbac recipe (#4895)
* docs: added rbac recipe * fix eslint errors * fixed indentation causing build error * fix build warning * small fixes --------- Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
1 parent
4d439bed38
commit
f75d9806f9
9 files changed
+1255
-258
No files matched your search
@@ -60,39 +60,39 @@ The resources in this section are used for typing, layout, and design purposes,
|
||||
Each of the collapsible elements below hold the path to the file that you should create, and the content of that file.
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
src/admin/types/icon-type.ts
|
||||
</summary>
|
||||
<summary>
|
||||
src/admin/types/icon-type.ts
|
||||
</summary>
|
||||
|
||||
```tsx title=src/admin/types/icon-type.ts
|
||||
import React from "react"
|
||||
```tsx title=src/admin/types/icon-type.ts
|
||||
import React from "react"
|
||||
|
||||
type IconProps = {
|
||||
type IconProps = {
|
||||
color?: string
|
||||
size?: string | number
|
||||
} & React.SVGAttributes<SVGElement>
|
||||
} & React.SVGAttributes<SVGElement>
|
||||
|
||||
export default IconProps
|
||||
```
|
||||
export default IconProps
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
src/admin/components/shared/icons/check-circle-fill-icon.tsx
|
||||
</summary>
|
||||
<summary>
|
||||
src/admin/components/shared/icons/check-circle-fill-icon.tsx
|
||||
</summary>
|
||||
|
||||
<!-- eslint-disable max-len -->
|
||||
|
||||
```tsx title=src/admin/components/shared/icons/check-circle-fill-icon.tsx
|
||||
import React from "react"
|
||||
import IconProps from "../../../types/icon-type"
|
||||
```tsx title=src/admin/components/shared/icons/check-circle-fill-icon.tsx
|
||||
import React from "react"
|
||||
import IconProps from "../../../types/icon-type"
|
||||
|
||||
const CheckCircleFillIcon: React.FC<IconProps> = ({
|
||||
const CheckCircleFillIcon: React.FC<IconProps> = ({
|
||||
size = "24",
|
||||
color = "currentColor",
|
||||
...attributes
|
||||
}) => {
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
@@ -110,27 +110,27 @@ Each of the collapsible elements below hold the path to the file that you should
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default CheckCircleFillIcon
|
||||
```
|
||||
export default CheckCircleFillIcon
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
src/admin/components/shared/icons/cross-icon.tsx
|
||||
</summary>
|
||||
<summary>
|
||||
src/admin/components/shared/icons/cross-icon.tsx
|
||||
</summary>
|
||||
|
||||
```tsx title=src/admin/components/shared/icons/cross-icon.tsx
|
||||
import React from "react"
|
||||
import IconProps from "../../../types/icon-type"
|
||||
```tsx title=src/admin/components/shared/icons/cross-icon.tsx
|
||||
import React from "react"
|
||||
import IconProps from "../../../types/icon-type"
|
||||
|
||||
const CrossIcon: React.FC<IconProps> = ({
|
||||
const CrossIcon: React.FC<IconProps> = ({
|
||||
size = "20",
|
||||
color = "currentColor",
|
||||
...attributes
|
||||
}) => {
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
@@ -156,25 +156,25 @@ Each of the collapsible elements below hold the path to the file that you should
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default CrossIcon
|
||||
```
|
||||
export default CrossIcon
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
src/admin/components/shared/icons/get-started-icon.tsx
|
||||
</summary>
|
||||
<summary>
|
||||
src/admin/components/shared/icons/get-started-icon.tsx
|
||||
</summary>
|
||||
|
||||
<!-- eslint-disable max-len -->
|
||||
|
||||
```tsx title=src/admin/components/shared/icons/get-started-icon.tsx
|
||||
import React from "react"
|
||||
import IconProps from "../../../types/icon-type"
|
||||
```tsx title=src/admin/components/shared/icons/get-started-icon.tsx
|
||||
import React from "react"
|
||||
import IconProps from "../../../types/icon-type"
|
||||
|
||||
const GetStartedIcon: React.FC<IconProps> = () => (
|
||||
const GetStartedIcon: React.FC<IconProps> = () => (
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
@@ -439,29 +439,29 @@ Each of the collapsible elements below hold the path to the file that you should
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
)
|
||||
|
||||
export default GetStartedIcon
|
||||
```
|
||||
export default GetStartedIcon
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
src/admin/components/shared/icons/clipboard-copy-icon.tsx
|
||||
</summary>
|
||||
<summary>
|
||||
src/admin/components/shared/icons/clipboard-copy-icon.tsx
|
||||
</summary>
|
||||
|
||||
<!-- eslint-disable max-len -->
|
||||
|
||||
```tsx title=src/admin/components/shared/icons/clipboard-copy-icon.tsx
|
||||
import React from "react"
|
||||
import IconProps from "../../../types/icon-type"
|
||||
```tsx title=src/admin/components/shared/icons/clipboard-copy-icon.tsx
|
||||
import React from "react"
|
||||
import IconProps from "../../../types/icon-type"
|
||||
|
||||
const ClipboardCopyIcon: React.FC<IconProps> = ({
|
||||
const ClipboardCopyIcon: React.FC<IconProps> = ({
|
||||
size = "20",
|
||||
color = "currentColor",
|
||||
...attributes
|
||||
}) => {
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
@@ -501,29 +501,29 @@ Each of the collapsible elements below hold the path to the file that you should
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default ClipboardCopyIcon
|
||||
```
|
||||
export default ClipboardCopyIcon
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
src/admin/components/shared/icons/computer-desktop-icon.tsx
|
||||
</summary>
|
||||
<summary>
|
||||
src/admin/components/shared/icons/computer-desktop-icon.tsx
|
||||
</summary>
|
||||
|
||||
<!-- eslint-disable max-len -->
|
||||
|
||||
```tsx title=src/admin/components/shared/icons/computer-desktop-icon.tsx
|
||||
import React from "react"
|
||||
import IconProps from "../../../types/icon-type"
|
||||
```tsx title=src/admin/components/shared/icons/computer-desktop-icon.tsx
|
||||
import React from "react"
|
||||
import IconProps from "../../../types/icon-type"
|
||||
|
||||
const ComputerDesktopIcon: React.FC<IconProps> = ({
|
||||
const ComputerDesktopIcon: React.FC<IconProps> = ({
|
||||
size = "24",
|
||||
color = "currentColor",
|
||||
...attributes
|
||||
}) => {
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
@@ -541,29 +541,29 @@ Each of the collapsible elements below hold the path to the file that you should
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default ComputerDesktopIcon
|
||||
```
|
||||
export default ComputerDesktopIcon
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
src/admin/components/shared/icons/dollar-sign-icon.tsx
|
||||
</summary>
|
||||
<summary>
|
||||
src/admin/components/shared/icons/dollar-sign-icon.tsx
|
||||
</summary>
|
||||
|
||||
<!-- eslint-disable max-len -->
|
||||
|
||||
```tsx title=src/admin/components/shared/icons/dollar-sign-icon.tsx
|
||||
import React from "react"
|
||||
import IconProps from "../../../types/icon-type"
|
||||
```tsx title=src/admin/components/shared/icons/dollar-sign-icon.tsx
|
||||
import React from "react"
|
||||
import IconProps from "../../../types/icon-type"
|
||||
|
||||
const DollarSignIcon: React.FC<IconProps> = ({
|
||||
const DollarSignIcon: React.FC<IconProps> = ({
|
||||
size = "24",
|
||||
color = "currentColor",
|
||||
...attributes
|
||||
}) => {
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
@@ -589,27 +589,27 @@ Each of the collapsible elements below hold the path to the file that you should
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default DollarSignIcon
|
||||
```
|
||||
export default DollarSignIcon
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
src/admin/components/shared/accordion.tsx
|
||||
</summary>
|
||||
<summary>
|
||||
src/admin/components/shared/accordion.tsx
|
||||
</summary>
|
||||
|
||||
<!-- eslint-disable max-len -->
|
||||
|
||||
```tsx title=src/admin/components/shared/accordion.tsx
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
import CheckCircleFillIcon from "./icons/check-circle-fill-icon"
|
||||
```tsx title=src/admin/components/shared/accordion.tsx
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
import CheckCircleFillIcon from "./icons/check-circle-fill-icon"
|
||||
|
||||
type AccordionItemProps = AccordionPrimitive.AccordionItemProps & {
|
||||
type AccordionItemProps = AccordionPrimitive.AccordionItemProps & {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
description?: string;
|
||||
@@ -621,24 +621,24 @@ Each of the collapsible elements below hold the path to the file that you should
|
||||
complete?: boolean;
|
||||
active?: boolean;
|
||||
triggerable?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const Accordion: React.FC<
|
||||
const Accordion: React.FC<
|
||||
| (AccordionPrimitive.AccordionSingleProps &
|
||||
React.RefAttributes<HTMLDivElement>)
|
||||
| (AccordionPrimitive.AccordionMultipleProps &
|
||||
React.RefAttributes<HTMLDivElement>)
|
||||
> & {
|
||||
> & {
|
||||
Item: React.FC<AccordionItemProps>;
|
||||
} = ({ children, ...props }) => {
|
||||
} = ({ children, ...props }) => {
|
||||
return (
|
||||
<AccordionPrimitive.Root {...props}>
|
||||
{children}
|
||||
</AccordionPrimitive.Root>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const Item: React.FC<AccordionItemProps> = ({
|
||||
const Item: React.FC<AccordionItemProps> = ({
|
||||
title,
|
||||
subtitle,
|
||||
description,
|
||||
@@ -653,7 +653,7 @@ Each of the collapsible elements below hold the path to the file that you should
|
||||
active,
|
||||
triggerable,
|
||||
...props
|
||||
}) => {
|
||||
}) => {
|
||||
const headerClass = clsx({
|
||||
"inter-small-semibold": headingSize === "small",
|
||||
"inter-base-medium": headingSize === "medium",
|
||||
@@ -724,11 +724,11 @@ Each of the collapsible elements below hold the path to the file that you should
|
||||
</AccordionPrimitive.Content>
|
||||
</AccordionPrimitive.Item>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Accordion.Item = Item
|
||||
Accordion.Item = Item
|
||||
|
||||
const MorphingTrigger = () => {
|
||||
const MorphingTrigger = () => {
|
||||
return (
|
||||
<div className="btn-ghost rounded-rounded group relative p-[6px]">
|
||||
<div className="h-5 w-5">
|
||||
@@ -737,33 +737,33 @@ Each of the collapsible elements below hold the path to the file that you should
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Accordion
|
||||
```
|
||||
export default Accordion
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
src/admin/components/shared/spinner.tsx
|
||||
</summary>
|
||||
<summary>
|
||||
src/admin/components/shared/spinner.tsx
|
||||
</summary>
|
||||
|
||||
<!-- eslint-disable max-len -->
|
||||
|
||||
```tsx title=src/admin/components/shared/spinner.tsx
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
```tsx title=src/admin/components/shared/spinner.tsx
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
|
||||
type SpinnerProps = {
|
||||
type SpinnerProps = {
|
||||
size?: "large" | "medium" | "small";
|
||||
variant?: "primary" | "secondary";
|
||||
};
|
||||
};
|
||||
|
||||
const Spinner: React.FC<SpinnerProps> = ({
|
||||
const Spinner: React.FC<SpinnerProps> = ({
|
||||
size = "large",
|
||||
variant = "primary",
|
||||
}) => {
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
@@ -784,33 +784,33 @@ Each of the collapsible elements below hold the path to the file that you should
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Spinner
|
||||
```
|
||||
export default Spinner
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
src/admin/components/shared/button.tsx
|
||||
</summary>
|
||||
<summary>
|
||||
src/admin/components/shared/button.tsx
|
||||
</summary>
|
||||
|
||||
<!-- eslint-disable max-len -->
|
||||
<!-- eslint-disable react/display-name -->
|
||||
|
||||
```tsx title=src/admin/components/shared/button.tsx
|
||||
import clsx from "clsx"
|
||||
import React, { Children } from "react"
|
||||
import Spinner from "./spinner"
|
||||
```tsx title=src/admin/components/shared/button.tsx
|
||||
import clsx from "clsx"
|
||||
import React, { Children } from "react"
|
||||
import Spinner from "./spinner"
|
||||
|
||||
export type ButtonProps = {
|
||||
export type ButtonProps = {
|
||||
variant: "primary" | "secondary" | "ghost" | "danger" | "nuclear";
|
||||
size?: "small" | "medium" | "large";
|
||||
loading?: boolean;
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
variant = "primary",
|
||||
@@ -868,37 +868,37 @@ Each of the collapsible elements below hold the path to the file that you should
|
||||
</button>
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
export default Button
|
||||
```
|
||||
export default Button
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
src/admin/components/shared/code-snippets.tsx
|
||||
</summary>
|
||||
<summary>
|
||||
src/admin/components/shared/code-snippets.tsx
|
||||
</summary>
|
||||
|
||||
<!-- eslint-disable max-len -->
|
||||
|
||||
```tsx title=src/admin/components/shared/code-snippets.tsx
|
||||
import React, { useState } from "react"
|
||||
import clsx from "clsx"
|
||||
import copy from "copy-to-clipboard"
|
||||
import { Highlight, themes } from "prism-react-renderer"
|
||||
import ClipboardCopyIcon from "./icons/clipboard-copy-icon"
|
||||
import CheckCircleFillIcon from "./icons/check-circle-fill-icon"
|
||||
```tsx title=src/admin/components/shared/code-snippets.tsx
|
||||
import React, { useState } from "react"
|
||||
import clsx from "clsx"
|
||||
import copy from "copy-to-clipboard"
|
||||
import { Highlight, themes } from "prism-react-renderer"
|
||||
import ClipboardCopyIcon from "./icons/clipboard-copy-icon"
|
||||
import CheckCircleFillIcon from "./icons/check-circle-fill-icon"
|
||||
|
||||
const CodeSnippets = ({
|
||||
const CodeSnippets = ({
|
||||
snippets,
|
||||
}: {
|
||||
}: {
|
||||
snippets: {
|
||||
label: string;
|
||||
language: string;
|
||||
code: string;
|
||||
}[];
|
||||
}) => {
|
||||
}) => {
|
||||
const [active, setActive] = useState(snippets[0])
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
@@ -971,29 +971,29 @@ Each of the collapsible elements below hold the path to the file that you should
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default CodeSnippets
|
||||
```
|
||||
export default CodeSnippets
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
src/admin/components/shared/container.tsx
|
||||
</summary>
|
||||
<summary>
|
||||
src/admin/components/shared/container.tsx
|
||||
</summary>
|
||||
|
||||
<!-- eslint-disable max-len -->
|
||||
|
||||
```tsx title=src/admin/components/shared/container.tsx
|
||||
import React, { PropsWithChildren } from "react"
|
||||
```tsx title=src/admin/components/shared/container.tsx
|
||||
import React, { PropsWithChildren } from "react"
|
||||
|
||||
type Props = PropsWithChildren<{
|
||||
type Props = PropsWithChildren<{
|
||||
title?: string;
|
||||
description?: string;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
export const Container = ({ title, description, children }: Props) => {
|
||||
export const Container = ({ title, description, children }: Props) => {
|
||||
return (
|
||||
<div className="border border-grey-20 rounded-rounded bg-white py-6 px-8 flex flex-col mb-base relative">
|
||||
<div>
|
||||
@@ -1009,23 +1009,23 @@ Each of the collapsible elements below hold the path to the file that you should
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
src/admin/components/shared/badge.tsx
|
||||
</summary>
|
||||
<summary>
|
||||
src/admin/components/shared/badge.tsx
|
||||
</summary>
|
||||
|
||||
<!-- eslint-disable max-len -->
|
||||
|
||||
```tsx title=src/admin/components/shared/badge.tsx
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
```tsx title=src/admin/components/shared/badge.tsx
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
|
||||
type BadgeProps = {
|
||||
type BadgeProps = {
|
||||
variant:
|
||||
| "primary"
|
||||
| "danger"
|
||||
@@ -1035,15 +1035,15 @@ Each of the collapsible elements below hold the path to the file that you should
|
||||
| "default"
|
||||
| "disabled"
|
||||
| "new-feature"
|
||||
} & React.HTMLAttributes<HTMLDivElement>
|
||||
} & React.HTMLAttributes<HTMLDivElement>
|
||||
|
||||
const Badge: React.FC<BadgeProps> = ({
|
||||
const Badge: React.FC<BadgeProps> = ({
|
||||
children,
|
||||
variant,
|
||||
onClick,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
}) => {
|
||||
const variantClassname = clsx({
|
||||
["badge-primary"]: variant === "primary",
|
||||
["badge-danger"]: variant === "danger",
|
||||
@@ -1065,26 +1065,26 @@ Each of the collapsible elements below hold the path to the file that you should
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Badge
|
||||
```
|
||||
export default Badge
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
src/admin/components/shared/icon-badge.tsx
|
||||
</summary>
|
||||
<summary>
|
||||
src/admin/components/shared/icon-badge.tsx
|
||||
</summary>
|
||||
|
||||
<!-- eslint-disable max-len -->
|
||||
|
||||
```tsx title=src/admin/components/shared/icon-badge.tsx
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
import Badge from "./badge"
|
||||
```tsx title=src/admin/components/shared/icon-badge.tsx
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
import Badge from "./badge"
|
||||
|
||||
type IconBadgeProps = {
|
||||
type IconBadgeProps = {
|
||||
variant?:
|
||||
| "primary"
|
||||
| "danger"
|
||||
@@ -1093,14 +1093,14 @@ Each of the collapsible elements below hold the path to the file that you should
|
||||
| "ghost"
|
||||
| "default"
|
||||
| "disabled";
|
||||
} & React.HTMLAttributes<HTMLDivElement>;
|
||||
} & React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
const IconBadge: React.FC<IconBadgeProps> = ({
|
||||
const IconBadge: React.FC<IconBadgeProps> = ({
|
||||
children,
|
||||
variant,
|
||||
className,
|
||||
...rest
|
||||
}) => {
|
||||
}) => {
|
||||
return (
|
||||
<Badge
|
||||
variant={variant ?? "default"}
|
||||
@@ -1113,10 +1113,10 @@ Each of the collapsible elements below hold the path to the file that you should
|
||||
{children}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default IconBadge
|
||||
```
|
||||
export default IconBadge
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
@@ -1798,36 +1798,36 @@ You can learn more about Admin Widgets in [this documentation](./widgets.md).
|
||||
In this section, you’ll create the components for each step in the onboarding flow. You’ll then update the `OnboardingFlow` widget to use these components.
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
ProductsList component
|
||||
<summary>
|
||||
ProductsList component
|
||||
|
||||
</summary>
|
||||
</summary>
|
||||
|
||||
The `ProductsList` component is used in the first step of the onboarding widget. It allows the user to either open the Create Product modal or create a sample product.
|
||||
The `ProductsList` component is used in the first step of the onboarding widget. It allows the user to either open the Create Product modal or create a sample product.
|
||||
|
||||
Create the file `src/admin/components/onboarding-flow/products/products-list.tsx` with the following content:
|
||||
Create the file `src/admin/components/onboarding-flow/products/products-list.tsx` with the following content:
|
||||
|
||||
<!-- eslint-disable max-len -->
|
||||
|
||||
```tsx title=src/admin/components/onboarding-flow/products/products-list.tsx
|
||||
import React from "react"
|
||||
import Button from "../../shared/button"
|
||||
import {
|
||||
```tsx title=src/admin/components/onboarding-flow/products/products-list.tsx
|
||||
import React from "react"
|
||||
import Button from "../../shared/button"
|
||||
import {
|
||||
useAdminCreateProduct,
|
||||
useAdminCreateCollection,
|
||||
} from "medusa-react"
|
||||
import {
|
||||
} from "medusa-react"
|
||||
import {
|
||||
useAdminRegions,
|
||||
} from "medusa-react"
|
||||
import {
|
||||
} from "medusa-react"
|
||||
import {
|
||||
StepContentProps,
|
||||
} from "../../../widgets/onboarding-flow/onboarding-flow"
|
||||
} from "../../../widgets/onboarding-flow/onboarding-flow"
|
||||
|
||||
enum ProductStatus {
|
||||
enum ProductStatus {
|
||||
PUBLISHED = "published",
|
||||
}
|
||||
}
|
||||
|
||||
const ProductsList = ({ onNext, isComplete }: StepContentProps) => {
|
||||
const ProductsList = ({ onNext, isComplete }: StepContentProps) => {
|
||||
const { mutateAsync: createCollection, isLoading: collectionLoading } =
|
||||
useAdminCreateCollection()
|
||||
const { mutateAsync: createProduct, isLoading: productLoading } =
|
||||
@@ -1929,37 +1929,37 @@ In this section, you’ll create the components for each step in the onboarding
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductsList
|
||||
```
|
||||
export default ProductsList
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
ProductDetail component
|
||||
<summary>
|
||||
ProductDetail component
|
||||
|
||||
</summary>
|
||||
</summary>
|
||||
|
||||
The `ProductDetail` component is used in the second step of the onboarding. It shows the user a code snippet to preview the product they created in the first step.
|
||||
The `ProductDetail` component is used in the second step of the onboarding. It shows the user a code snippet to preview the product they created in the first step.
|
||||
|
||||
Create the file `src/admin/components/onboarding-flow/products/product-detail.tsx` with the following content:
|
||||
Create the file `src/admin/components/onboarding-flow/products/product-detail.tsx` with the following content:
|
||||
|
||||
<!-- eslint-disable max-len -->
|
||||
|
||||
```tsx title=src/admin/components/onboarding-flow/products/product-detail.tsx
|
||||
import React from "react"
|
||||
import {
|
||||
```tsx title=src/admin/components/onboarding-flow/products/product-detail.tsx
|
||||
import React from "react"
|
||||
import {
|
||||
useAdminPublishableApiKeys,
|
||||
} from "medusa-react"
|
||||
import Button from "../../shared/button"
|
||||
import CodeSnippets from "../../shared/code-snippets"
|
||||
import {
|
||||
} from "medusa-react"
|
||||
import Button from "../../shared/button"
|
||||
import CodeSnippets from "../../shared/code-snippets"
|
||||
import {
|
||||
StepContentProps,
|
||||
} from "../../../widgets/onboarding-flow/onboarding-flow"
|
||||
} from "../../../widgets/onboarding-flow/onboarding-flow"
|
||||
|
||||
const ProductDetail = ({ onNext, isComplete, data }: StepContentProps) => {
|
||||
const ProductDetail = ({ onNext, isComplete, data }: StepContentProps) => {
|
||||
const {
|
||||
publishable_api_keys: keys,
|
||||
isLoading,
|
||||
@@ -2015,48 +2015,48 @@ In this section, you’ll create the components for each step in the onboarding
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductDetail
|
||||
```
|
||||
export default ProductDetail
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
OrdersList component
|
||||
<summary>
|
||||
OrdersList component
|
||||
|
||||
</summary>
|
||||
</summary>
|
||||
|
||||
The `OrdersList` component is used in the third step of the onboarding. It allows the user to create a sample order.
|
||||
The `OrdersList` component is used in the third step of the onboarding. It allows the user to create a sample order.
|
||||
|
||||
Create the file `src/admin/components/onboarding-flow/orders/orders-list.tsx` with the following content:
|
||||
Create the file `src/admin/components/onboarding-flow/orders/orders-list.tsx` with the following content:
|
||||
|
||||
<!-- eslint-disable max-len -->
|
||||
|
||||
```tsx title=src/admin/components/onboarding-flow/orders/orders-list.tsx
|
||||
import React from "react"
|
||||
import Button from "../../shared/button"
|
||||
import {
|
||||
```tsx title=src/admin/components/onboarding-flow/orders/orders-list.tsx
|
||||
import React from "react"
|
||||
import Button from "../../shared/button"
|
||||
import {
|
||||
useAdminProduct,
|
||||
} from "medusa-react"
|
||||
import {
|
||||
} from "medusa-react"
|
||||
import {
|
||||
useAdminCreateDraftOrder,
|
||||
} from "medusa-react"
|
||||
import {
|
||||
} from "medusa-react"
|
||||
import {
|
||||
useAdminShippingOptions,
|
||||
} from "medusa-react"
|
||||
import {
|
||||
} from "medusa-react"
|
||||
import {
|
||||
useAdminRegions,
|
||||
} from "medusa-react"
|
||||
import {
|
||||
} from "medusa-react"
|
||||
import {
|
||||
useMedusa,
|
||||
} from "medusa-react"
|
||||
import {
|
||||
} from "medusa-react"
|
||||
import {
|
||||
StepContentProps,
|
||||
} from "../../../widgets/onboarding-flow/onboarding-flow"
|
||||
} from "../../../widgets/onboarding-flow/onboarding-flow"
|
||||
|
||||
const OrdersList = ({ onNext, isComplete, data }: StepContentProps) => {
|
||||
const OrdersList = ({ onNext, isComplete, data }: StepContentProps) => {
|
||||
const { product } = useAdminProduct(data.product_id)
|
||||
const { mutateAsync: createDraftOrder, isLoading } =
|
||||
useAdminCreateDraftOrder()
|
||||
@@ -2119,32 +2119,32 @@ In this section, you’ll create the components for each step in the onboarding
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default OrdersList
|
||||
```
|
||||
export default OrdersList
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
OrderDetail component
|
||||
<summary>
|
||||
OrderDetail component
|
||||
|
||||
</summary>
|
||||
</summary>
|
||||
|
||||
The `OrderDetail` component is used in the fourth and final step of the onboarding. It educates the user on the next steps when developing with Medusa.
|
||||
The `OrderDetail` component is used in the fourth and final step of the onboarding. It educates the user on the next steps when developing with Medusa.
|
||||
|
||||
Create the file `src/admin/components/onboarding-flow/orders/order-detail.tsx` with the following content:
|
||||
Create the file `src/admin/components/onboarding-flow/orders/order-detail.tsx` with the following content:
|
||||
|
||||
<!-- eslint-disable max-len -->
|
||||
|
||||
```tsx title=src/admin/components/onboarding-flow/orders/order-detail.tsx
|
||||
import React from "react"
|
||||
import IconBadge from "../../shared/icon-badge"
|
||||
import ComputerDesktopIcon from "../../shared/icons/computer-desktop-icon"
|
||||
import DollarSignIcon from "../../shared/icons/dollar-sign-icon"
|
||||
```tsx title=src/admin/components/onboarding-flow/orders/order-detail.tsx
|
||||
import React from "react"
|
||||
import IconBadge from "../../shared/icon-badge"
|
||||
import ComputerDesktopIcon from "../../shared/icons/computer-desktop-icon"
|
||||
import DollarSignIcon from "../../shared/icons/dollar-sign-icon"
|
||||
|
||||
const OrderDetail = () => {
|
||||
const OrderDetail = () => {
|
||||
return (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
@@ -2228,10 +2228,10 @@ In this section, you’ll create the components for each step in the onboarding
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default OrderDetail
|
||||
```
|
||||
export default OrderDetail
|
||||
```
|
||||
</details>
|
||||
|
||||
After creating the above components, import them at the top of `src/admin/widgets/onboarding-flow/onboarding-flow.tsx`:
|
||||
|
||||
@@ -5,6 +5,7 @@ addHowToData: true
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import Badge from '@site/src/components/Badge';
|
||||
|
||||
# How to Use Product Categories in a Storefront
|
||||
|
||||
|
||||
@@ -63,15 +63,19 @@ Follow this guide if you want to implement subscription-based purhcases with Med
|
||||
|
||||
---
|
||||
|
||||
## Other Recipes
|
||||
## Recipe: Role-Based Access Control
|
||||
|
||||
This section includes general recipes available to guide you through your development with Medusa.
|
||||
Follow this guide if you want to implement role-based access control (RBAC) in Medusa.
|
||||
|
||||
<LearningPathList ignore={[
|
||||
"simple-quickstart",
|
||||
"marketplace",
|
||||
"subscriptions"
|
||||
]} />
|
||||
<DocCard item={{
|
||||
type: 'link',
|
||||
href: '/recipes/rbac',
|
||||
label: 'Build RBAC',
|
||||
customProps: {
|
||||
icon: Icons['credit-card-solid'],
|
||||
description: 'Learn how you can implement role-based access control in Medusa.'
|
||||
}
|
||||
}} />
|
||||
|
||||
<Feedback
|
||||
event="survey_use_cases"
|
||||
@@ -81,6 +85,8 @@ This section includes general recipes available to guide you through your develo
|
||||
showLongForm={false}
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
<!-- vale docs.HeadingPunctuation = NO -->
|
||||
|
||||
## Can't find your path?
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
addHowToData: true
|
||||
---
|
||||
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
import DocCard from '@theme/DocCard';
|
||||
import Icons from '@theme/Icon';
|
||||
@@ -352,6 +356,62 @@ Payment and fulfillment providers are associated with regions, which are not ass
|
||||
|
||||
---
|
||||
|
||||
## Customize Admin
|
||||
|
||||
As you add marketplace features to your store, you'll most likely need to customize the admin to provide an interface to manage these features.
|
||||
|
||||
Medusa's [admin plugin](../admin/quickstart.mdx) can be extended to add widgets or new blocks to existing pages, add UI routes to add new pages, or add setting pages.
|
||||
|
||||
<DocCardList colSize={4} items={[
|
||||
{
|
||||
type: 'link',
|
||||
href: '/admin/widgets',
|
||||
label: 'Create Admin Widget',
|
||||
customProps: {
|
||||
icon: Icons['academic-cap-solid'],
|
||||
description: 'Learn how to add widgets into existing admin pages.',
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
href: '/admin/routes',
|
||||
label: 'Create Admin UI Routes',
|
||||
customProps: {
|
||||
icon: Icons['academic-cap-solid'],
|
||||
description: 'Learn how to add new pages to your Medusa admin.',
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
href: '/admin/setting-pages',
|
||||
label: 'Create Admin Setting Page',
|
||||
customProps: {
|
||||
icon: Icons['academic-cap-solid'],
|
||||
description: 'Learn how to add new page to the Medusa admin settings.',
|
||||
}
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Implement Role-Based Access Control
|
||||
|
||||
In your marketplace, you may need to implement role-based access control (RBAC) within stores. This will restrict some users' permissions to specified functionalities or endpoints.
|
||||
|
||||
If you want to implement this functionality, you can follow the RBAC recipe.
|
||||
|
||||
<DocCard item={{
|
||||
type: 'link',
|
||||
href: '/recipes/rbac',
|
||||
label: 'RBAC Recipe',
|
||||
customProps: {
|
||||
icon: Icons['academic-cap-solid'],
|
||||
description: 'Learn how to implement role-based access control (RBAC) in Medusa.',
|
||||
}
|
||||
}} />
|
||||
|
||||
---
|
||||
|
||||
## Build a Storefront
|
||||
|
||||
Medusa provides a Next.js Starter Template that you can use with Medusa. Since you've customized your Medusa project, you'll need to either customize the existing Next.js Starter Template, or create a custom storefront.
|
||||
|
||||
@@ -0,0 +1,805 @@
|
||||
---
|
||||
addHowToData: true
|
||||
---
|
||||
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
import DocCard from '@theme/DocCard';
|
||||
import Icons from '@theme/Icon';
|
||||
import LearningPath from '@site/src/components/LearningPath';
|
||||
|
||||
# Role-Based Access Control (RBAC) Recipe
|
||||
|
||||
This document guides you through the different documentation resources that will help you build a marketplace with Medusa.
|
||||
|
||||
## Overview
|
||||
|
||||
Role-Based Access Control (RBAC) refers to the level of access a user has. Typically, in e-commerce, you may require RBAC if you want users to only be able to perform certain actions.
|
||||
|
||||
For example, you may want a content-manager user who can only access CMS functionalities and another manager user who can only access order functionalities. RBAC is also useful in [marketplace use cases](./marketplace.mdx).
|
||||
|
||||
This recipe gives you a high-level approach to implementing RBAC in Medusa. The examples included in this recipe provide a simple implementation to give you an idea of how you can implement this functionality in your Medusa backend.
|
||||
|
||||
You may also follow this path that takes you through the different documentation pages that will help you to implement RBAC in Medusa.
|
||||
|
||||
<LearningPath pathName="rbac" />
|
||||
|
||||
---
|
||||
|
||||
## Create Role and Permission Entities
|
||||
|
||||
When implementing RBAC, you typically require the availability of roles and permissions. A role would include different permissions, such as the ability to access the products’ route, and it can be assigned to one or more users.
|
||||
|
||||
So, the first step would be to create the `Role` and `Permission` entities to represent this data. Also, since you’ll be creating relations to other entities, such as the `User` entity, you need to extend the core entities to implement these relations.
|
||||
|
||||
<DocCardList colSize={6} items={[
|
||||
{
|
||||
type: 'link',
|
||||
href: '/development/entities/create',
|
||||
label: 'Create Entities',
|
||||
customProps: {
|
||||
icon: Icons['academic-cap-solid'],
|
||||
description: 'Learn how to create an entity.',
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
href: '/development/entities/extend-entity',
|
||||
label: 'Extend Entities',
|
||||
customProps: {
|
||||
icon: Icons['academic-cap-solid'],
|
||||
description: 'Learn how to extend a core entity.',
|
||||
}
|
||||
},
|
||||
]} />
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
Example Implementation
|
||||
</summary>
|
||||
|
||||
This is an example implementation of how you can create the Role and Permission entities, and extend the `User` and `Store` entities.
|
||||
|
||||
Creating an entity requires creating an entity class, a repository, and a migration. You can learn more [here](../development/entities/create.mdx). You’ll be creating the migration at the end of this example section.
|
||||
|
||||
Create the file `src/models/role.ts` with the following content:
|
||||
|
||||
```ts title=src/models/role.ts
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
} from "typeorm"
|
||||
import { BaseEntity } from "@medusajs/medusa"
|
||||
import { generateEntityId } from "@medusajs/medusa/dist/utils"
|
||||
import { Permission } from "./permission"
|
||||
import { User } from "./user"
|
||||
import { Store } from "./store"
|
||||
|
||||
@Entity()
|
||||
export class Role extends BaseEntity {
|
||||
@Column({ type: "varchar" })
|
||||
name: string
|
||||
|
||||
// only helpful if you're integrating in a marketplace
|
||||
@Index()
|
||||
@Column({ nullable: true })
|
||||
store_id: string
|
||||
|
||||
@ManyToMany(() => Permission)
|
||||
@JoinTable({
|
||||
name: "role_permissions",
|
||||
joinColumn: {
|
||||
name: "role_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "permission_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
})
|
||||
permissions: Permission[]
|
||||
|
||||
@OneToMany(() => User, (user) => user.teamRole)
|
||||
@JoinColumn({ name: "id", referencedColumnName: "role_id" })
|
||||
users: User[]
|
||||
|
||||
@ManyToOne(() => Store, (store) => store.roles)
|
||||
@JoinColumn({ name: "store_id" })
|
||||
store: Store
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert(): void {
|
||||
this.id = generateEntityId(this.id, "role")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This creates the `Role` entity. You’ll see errors in your editors, which you’ll resolve by following along the example.
|
||||
|
||||
The `Role` entity has the following attributes:
|
||||
|
||||
- `id`: the ID of the role, which is available implicitly by extending `BaseEntity`
|
||||
- `name`: the name of the role
|
||||
- `store_id`: the ID of the store this role belongs to. This is only useful if you’re implementing RBAC in a marketplace. Otherwise, you may omit this relation.
|
||||
|
||||
It also has the following relations:
|
||||
|
||||
- `permissions`: an array of permissions included in this role.
|
||||
- `store`: the Store this role belongs to.
|
||||
- `users`: the users associated with this role.
|
||||
|
||||
Then, create the file `src/repositories/role.ts` with the following content:
|
||||
|
||||
```ts title=src/repositories/role.ts
|
||||
import { Role } from "../models/role"
|
||||
import {
|
||||
dataSource,
|
||||
} from "@medusajs/medusa/dist/loaders/database"
|
||||
|
||||
export const RoleRepository = dataSource
|
||||
.getRepository(Role)
|
||||
|
||||
export default RoleRepository
|
||||
```
|
||||
|
||||
Next, create the file `src/models/permission.ts` with the following content:
|
||||
|
||||
```ts title=src/models/permission.ts
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
Entity,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
} from "typeorm"
|
||||
import { BaseEntity } from "@medusajs/medusa"
|
||||
import {
|
||||
DbAwareColumn,
|
||||
generateEntityId,
|
||||
} from "@medusajs/medusa/dist/utils"
|
||||
import { Role } from "./role"
|
||||
|
||||
@Entity()
|
||||
export class Permission extends BaseEntity {
|
||||
@Column({ type: "varchar" })
|
||||
name: string
|
||||
|
||||
// holds the permissions
|
||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||
metadata: Record<string, boolean>
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert(): void {
|
||||
this.id = generateEntityId(this.id, "perm")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This creates a `Permission` entity that has the following attributes:
|
||||
|
||||
- `id`: the ID of the permission, which is implicitly available through extending `BaseEntity`.
|
||||
- `name`: the name of the permission.
|
||||
- `metadata`: an object that will include the permissions. The object keys will be an admin path in the backend, and the value will be a boolean indicating whether the user has access to that path or not.
|
||||
|
||||
Then, create the file `src/repositories/permission.ts` with the following content:
|
||||
|
||||
```ts title=src/repositories/permission.ts
|
||||
import { Permission } from "../models/permission"
|
||||
import {
|
||||
dataSource,
|
||||
} from "@medusajs/medusa/dist/loaders/database"
|
||||
|
||||
export const PermissionRepository = dataSource
|
||||
.getRepository(Permission)
|
||||
|
||||
export default PermissionRepository
|
||||
```
|
||||
|
||||
Next, you’ll extend the `User` and `Store` entities. As mentioned earlier, extending the `Store` entity and adding the relation is only useful if you’re implementing RBAC in a marketplace or similar use cases. So, if this doesn’t apply to you, you may skip it.
|
||||
|
||||
Create the file `src/models/user.ts` with the following content:
|
||||
|
||||
```ts title=src/models/user.ts
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
} from "typeorm"
|
||||
import {
|
||||
// alias the core entity to not cause a naming conflict
|
||||
User as MedusaUser,
|
||||
} from "@medusajs/medusa"
|
||||
import { Role } from "./role"
|
||||
|
||||
@Entity()
|
||||
export class User extends MedusaUser {
|
||||
@Index()
|
||||
@Column({ nullable: true })
|
||||
role_id: string | null
|
||||
|
||||
@ManyToOne(() => Role, (role) => role.users)
|
||||
@JoinColumn({ name: "role_id" })
|
||||
teamRole: Role | null
|
||||
}
|
||||
```
|
||||
|
||||
This adds a new attribute `role_id` to the core `User` entity and a `teamRole` relation that optionally associates the user with a role.
|
||||
|
||||
Next, create the file `src/models/store.ts` with the following content:
|
||||
|
||||
```ts title=src/models/store.ts
|
||||
import { Entity, JoinColumn, OneToMany } from "typeorm"
|
||||
import {
|
||||
// alias the core entity to not cause a naming conflict
|
||||
Store as MedusaStore,
|
||||
} from "@medusajs/medusa"
|
||||
import { Role } from "./role"
|
||||
|
||||
@Entity()
|
||||
export class Store extends MedusaStore {
|
||||
@OneToMany(() => Role, (role) => role.store)
|
||||
@JoinColumn({ name: "id", referencedColumnName: "store_id" })
|
||||
roles: Role[]
|
||||
}
|
||||
```
|
||||
|
||||
This adds a `roles` relation to the core `Store` entity.
|
||||
|
||||
Optionally, if you’re using TypeScript, create the file `src/index.d.ts` with the following content:
|
||||
|
||||
```ts title=src/index.d.ts
|
||||
import { Role } from "./models/role"
|
||||
|
||||
export declare module "@medusajs/medusa/dist/models/user" {
|
||||
|
||||
declare interface User {
|
||||
role_id: string | null;
|
||||
teamRole: Role | null
|
||||
}
|
||||
|
||||
declare interface Store {
|
||||
roles: Role[]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This ensures that your TypeScript validation and editor autocomplete recognize the new attributes and relations you added on the core entities.
|
||||
|
||||
Finally, you need to create a migration to reflect these changes in the database.
|
||||
|
||||
You can learn about creating migrations [here](../development/entities/migrations/create.md). An example of a migration file based on the entities created above:
|
||||
|
||||
<!-- eslint-disable max-len -->
|
||||
|
||||
```ts title=src/migrations/1693225851284-AddRolesAndPermissions.ts
|
||||
import { MigrationInterface, QueryRunner, Table, TableIndex } from "typeorm"
|
||||
|
||||
export class AddRolesAndPermissions1693225851284 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "user" ADD "role_id" character varying`)
|
||||
await queryRunner.query(`CREATE TABLE "permission" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "name" character varying NOT NULL, "metadata" jsonb, CONSTRAINT "PK_3b8b97af9d9d8807e41e6f48362" PRIMARY KEY ("id"))`)
|
||||
await queryRunner.query(`CREATE TABLE "role" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "name" character varying NOT NULL, "store_id" character varying, CONSTRAINT "PK_b36bcfe02fc8de3c57a8b2391c2" PRIMARY KEY ("id"))`)
|
||||
await queryRunner.query(`CREATE INDEX "IDX_29259dd58b1052aef9be56941d" ON "role" ("store_id") `)
|
||||
await queryRunner.query(`CREATE TABLE "role_permissions" ("role_id" character varying NOT NULL, "permission_id" character varying NOT NULL, CONSTRAINT "PK_25d24010f53bb80b78e412c9656" PRIMARY KEY ("role_id", "permission_id"))`)
|
||||
await queryRunner.query(`CREATE INDEX "IDX_178199805b901ccd220ab7740e" ON "role_permissions" ("role_id") `)
|
||||
await queryRunner.query(`CREATE INDEX "IDX_17022daf3f885f7d35423e9971" ON "role_permissions" ("permission_id") `)
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "role_permissions" ADD CONSTRAINT "FK_178199805b901ccd220ab7740ec" FOREIGN KEY ("role_id") REFERENCES "role"("id") ON DELETE CASCADE ON UPDATE CASCADE`)
|
||||
await queryRunner.query(`ALTER TABLE "role_permissions" ADD CONSTRAINT "FK_17022daf3f885f7d35423e9971e" FOREIGN KEY ("permission_id") REFERENCES "permission"("id") ON DELETE CASCADE ON UPDATE CASCADE`)
|
||||
await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_fb2e442d14add3cefbdf33c4561" FOREIGN KEY ("role_id") REFERENCES "role"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`)
|
||||
await queryRunner.query(`ALTER TABLE "role" ADD CONSTRAINT "FK_29259dd58b1052aef9be56941d4" FOREIGN KEY ("store_id") REFERENCES "store"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`)
|
||||
await queryRunner.query(`CREATE INDEX "IDX_fb2e442d14add3cefbdf33c456" ON "user" ("role_id") `)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "role_permissions" DROP CONSTRAINT "FK_17022daf3f885f7d35423e9971e"`)
|
||||
await queryRunner.query(`ALTER TABLE "role_permissions" DROP CONSTRAINT "FK_178199805b901ccd220ab7740ec"`)
|
||||
await queryRunner.query(`ALTER TABLE "role" DROP CONSTRAINT "FK_29259dd58b1052aef9be56941d4"`)
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_fb2e442d14add3cefbdf33c456"`)
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_fb2e442d14add3cefbdf33c4561"`)
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "role_id"`)
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_17022daf3f885f7d35423e9971"`)
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_178199805b901ccd220ab7740e"`)
|
||||
await queryRunner.query(`DROP TABLE "role_permissions"`)
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_29259dd58b1052aef9be56941d"`)
|
||||
await queryRunner.query(`DROP TABLE "role"`)
|
||||
await queryRunner.query(`DROP TABLE "permission"`)
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Finally, to reflect these changes, run the `build` command in the root directory of your medusa backend:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run build
|
||||
```
|
||||
|
||||
Then, run the migrations:
|
||||
|
||||
```bash
|
||||
npx medusa migrations run
|
||||
```
|
||||
|
||||
This will reflect the entity changes in your database.
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## Create Guard Middleware
|
||||
|
||||
To ensure that users who have the privilege can access an endpoint, you must create a middleware that guards admin routes. This middleware will run on all authenticated admin requests to ensure that only allowed users can access an endpoint.
|
||||
|
||||
Since the Medusa backend uses Express, you can create a middleware and attach it to all admin routes.
|
||||
|
||||
<DocCard item={{
|
||||
type: 'link',
|
||||
href: '/development/endpoints/add-middleware',
|
||||
label: 'Create a Middleware',
|
||||
customProps: {
|
||||
icon: Icons['academic-cap-solid'],
|
||||
description: 'Learn how to create a middleware in Medusa.',
|
||||
}
|
||||
}} />
|
||||
|
||||
<details>
|
||||
<summary>Example Implementation</summary>
|
||||
|
||||
In this example, you’ll create a middleware that runs on all admin-authenticated routes and checks the logged-in user’s permissions before giving them access to an endpoint.
|
||||
|
||||
Create the file `src/api/middlewares/permission.ts` with the following content:
|
||||
|
||||
```ts title=src/api/middlewares/permission.ts
|
||||
import { UserService } from "@medusajs/medusa"
|
||||
import { NextFunction, Request, Response } from "express"
|
||||
|
||||
export default async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
if (!req.user || !req.user.userId) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
// retrieve currently logged-in user
|
||||
const userService = req.scope.resolve(
|
||||
"userService"
|
||||
) as UserService
|
||||
const loggedInUser = await userService.retrieve(
|
||||
req.user.userId,
|
||||
{
|
||||
select: ["id"],
|
||||
relations: ["teamRole", "teamRole.permissions"],
|
||||
})
|
||||
|
||||
if (!loggedInUser.teamRole) {
|
||||
// considered as super user
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
const isAllowed = loggedInUser.teamRole?.permissions.some(
|
||||
(permission) => {
|
||||
const metadataKey = Object.keys(permission.metadata).find(
|
||||
(key) => key === req.path
|
||||
)
|
||||
if (!metadataKey) {
|
||||
return false
|
||||
}
|
||||
|
||||
// boolean value
|
||||
return permission.metadata[metadataKey]
|
||||
}
|
||||
)
|
||||
|
||||
if (isAllowed) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// deny access
|
||||
res.sendStatus(401)
|
||||
}
|
||||
```
|
||||
|
||||
In this middleware, you ensure that there is a logged-in user and the logged-in user has a role. If not, the user is admitted to access the endpoint. Here, you presume that logged-in users who don’t have a role are “super-admin” users who can access all endpoints. You may choose to implement this differently.
|
||||
|
||||
If there’s a logged-in user that has a role, you check that the role’s permissions give them access to the current endpoint. You do that by checking if a permission’s metadata has a key with the same request’s path. It may be better here to check for matching using regular expressions, for example, to check routes with path parameters.
|
||||
|
||||
Otherwise, if the user’s role doesn’t provide them with enough permissions, you return a `401` response code.
|
||||
|
||||
:::tip
|
||||
|
||||
Notice that you use `req.path` here to get the current endpoint path. However, in middlewares, this doesn’t include the mount point which is `/admin`. So, for example, if the endpoint path is `/admin/products`, `req.path` will be `/products`. You can alternatively use `req.originalUrl`. Learn more in [Express’s documentation](https://expressjs.com/en/api.html#req.originalUrl).
|
||||
|
||||
:::
|
||||
|
||||
Next, to ensure that this middleware is used, import it in `src/api/index.ts` and apply it on admin routes:
|
||||
|
||||
```ts title=src/api/index.ts
|
||||
import permissionMiddleware from "./middlewares/permission"
|
||||
|
||||
export default (rootDirectory: string): Router | Router[] => {
|
||||
// ...
|
||||
const router = Router()
|
||||
// ...
|
||||
|
||||
// use middleware on admin routes
|
||||
router.use("/admin", permissionMiddleware)
|
||||
|
||||
return router
|
||||
}
|
||||
```
|
||||
|
||||
This assumes you already have a router with all necessary CORS configurations and body parsing middlewares. If not, you can refer to the [Create Endpoint documentation](../development/endpoints/create.mdx) for more details.
|
||||
|
||||
Make sure to use the permission middleware after all router configurations if you want the middleware to work on your custom admin routes.
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## Create Endpoints and Services
|
||||
|
||||
To manage the roles and permissions, you’ll need to create custom endpoints, typically for Create, Read, Update, and Delete (CRUD) operations.
|
||||
|
||||
You’ll also need to create a service for each of `Role` and `Permission` entities to perform these operations on them. The entity uses the service within its code.
|
||||
|
||||
Furthermore, you may need to extend core services if you need to perform actions on core entities that you’ve extended, such as the `User` entity.
|
||||
|
||||
<DocCardList colSize={4} items={[
|
||||
{
|
||||
type: 'link',
|
||||
href: '/development/endpoints/create',
|
||||
label: 'Create Endpoint',
|
||||
customProps: {
|
||||
icon: Icons['academic-cap-solid'],
|
||||
description: 'Learn how to create an endpoint in Medusa.',
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
href: '/development/services/create-service',
|
||||
label: 'Create Service',
|
||||
customProps: {
|
||||
icon: Icons['academic-cap-solid'],
|
||||
description: 'Learn how to create a service in Medusa',
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
href: '/development/services/extend-service',
|
||||
label: 'Extend Service',
|
||||
customProps: {
|
||||
icon: Icons['academic-cap-solid'],
|
||||
description: 'Learn how to extend a core service in Medusa',
|
||||
}
|
||||
},
|
||||
]} />
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
Example Implementation
|
||||
</summary>
|
||||
|
||||
In this example, you’ll only implement two endpoints for simplicity: create role endpoint that create a new role with permissions, and associate user endpoint that associates a user with a role.
|
||||
|
||||
You’ll also create basic services for `Role` and `Permission` to perform the functionalities of each of these endpoints and extend the core `UserService` to allow associating roles with users.
|
||||
|
||||
Start by creating the file `src/services/permission.ts` with the following content:
|
||||
|
||||
```ts title=src/services/permission.ts
|
||||
import { TransactionBaseService } from "@medusajs/medusa"
|
||||
import { Permission } from "../models/permission"
|
||||
import PermissionRepository from "../repositories/permission"
|
||||
|
||||
export type CreatePayload = Pick<
|
||||
Permission,
|
||||
"name" | "metadata"
|
||||
>
|
||||
|
||||
type InjectedDependencies = {
|
||||
permissionRepository: typeof PermissionRepository
|
||||
}
|
||||
|
||||
class PermissionService extends TransactionBaseService {
|
||||
protected readonly permissionRepository_:
|
||||
typeof PermissionRepository
|
||||
|
||||
constructor(container: InjectedDependencies) {
|
||||
super(container)
|
||||
this.permissionRepository_ = container.permissionRepository
|
||||
}
|
||||
|
||||
async create(data: CreatePayload) {
|
||||
// omitting validation for simplicity
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const permissionRepo = manager.withRepository(
|
||||
this.permissionRepository_
|
||||
)
|
||||
const permission = permissionRepo.create(data)
|
||||
|
||||
const result = await permissionRepo.save(permission)
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default PermissionService
|
||||
```
|
||||
|
||||
This creates the `PermissionService` with only a `create` method that can be used to create a permission.
|
||||
|
||||
Next, create the file `src/services/user.ts` with the following content:
|
||||
|
||||
```ts title=src/services/user.ts
|
||||
import {
|
||||
UserService as MedusaUserService, User,
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
UpdateUserInput,
|
||||
} from "@medusajs/medusa/dist/types/user"
|
||||
|
||||
class UserService extends MedusaUserService {
|
||||
async update(userId: string, update: UpdateUserInput & {
|
||||
role_id?: string
|
||||
}): Promise<User> {
|
||||
return super.update(userId, update)
|
||||
}
|
||||
}
|
||||
|
||||
export default UserService
|
||||
```
|
||||
|
||||
This extends the core `UserService` to allow updating a user’s role. You may also want to extend the `create` method to allow specifying the role on creation.
|
||||
|
||||
Then, create the file `src/services/role.ts` with the following content:
|
||||
|
||||
```ts title=src/services/role.ts
|
||||
import { TransactionBaseService } from "@medusajs/medusa"
|
||||
import { Role } from "../models/role"
|
||||
import RoleRepository from "../repositories/role"
|
||||
import PermissionService, {
|
||||
CreatePayload as PermissionCreatePayload,
|
||||
} from "./permission"
|
||||
import UserService from "./user"
|
||||
|
||||
type CreatePayload = Pick<Role, "name" | "store_id"> & {
|
||||
permissions?: PermissionCreatePayload[]
|
||||
}
|
||||
|
||||
type InjectedDependencies = {
|
||||
roleRepository: typeof RoleRepository
|
||||
permissionService: PermissionService
|
||||
userService: UserService
|
||||
}
|
||||
|
||||
class RoleService extends TransactionBaseService {
|
||||
protected readonly roleRpository_: typeof RoleRepository
|
||||
protected readonly permissionService_: PermissionService
|
||||
protected readonly userService_: UserService
|
||||
|
||||
constructor(container: InjectedDependencies) {
|
||||
super(container)
|
||||
|
||||
this.roleRpository_ = container.roleRepository
|
||||
this.permissionService_ = container.permissionService
|
||||
this.userService_ = container.userService
|
||||
}
|
||||
|
||||
async retrieve(id: string): Promise<Role> {
|
||||
// for simplicity, we retrieve all relations
|
||||
// however, it's best to supply the relations
|
||||
// as an optional method parameter
|
||||
const roleRepo = this.manager_.withRepository(
|
||||
this.roleRpository_
|
||||
)
|
||||
return await roleRepo.findOne({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
relations: [
|
||||
"permissions",
|
||||
"store",
|
||||
"users",
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
async create(data: CreatePayload): Promise<Role> {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
// omitting validation for simplicity
|
||||
const { permissions: permissionsData = [] } = data
|
||||
delete data.permissions
|
||||
|
||||
const roleRepo = manager.withRepository(
|
||||
this.roleRpository_
|
||||
)
|
||||
const role = roleRepo.create(data)
|
||||
|
||||
role.permissions = []
|
||||
|
||||
for (const permissionData of permissionsData) {
|
||||
role.permissions.push(
|
||||
await this.permissionService_.create(
|
||||
permissionData
|
||||
)
|
||||
)
|
||||
}
|
||||
const result = await roleRepo.save(role)
|
||||
|
||||
return await this.retrieve(result.id)
|
||||
})
|
||||
}
|
||||
|
||||
async associateUser(
|
||||
role_id: string,
|
||||
user_id: string
|
||||
): Promise<Role> {
|
||||
return this.atomicPhase_(async () => {
|
||||
// omitting validation for simplicity
|
||||
await this.userService_.update(user_id, {
|
||||
role_id,
|
||||
})
|
||||
|
||||
return await this.retrieve(role_id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default RoleService
|
||||
```
|
||||
|
||||
This creates the `RoleService` with three methods:
|
||||
|
||||
- `retrieve`: Retrieves a role with its relations.
|
||||
- `create`: Creates a new role and, if provided, its permissions as well.
|
||||
- `associateUser`: associates a user with a role.
|
||||
|
||||
Now, you can create the endpoints.
|
||||
|
||||
Start by creating the file `src/api/routes/admin/role/create-role.ts` with the following content:
|
||||
|
||||
```ts title=src/api/routes/admin/role/create-role.ts
|
||||
import { Request, Response } from "express"
|
||||
import RoleService from "../../../../services/role"
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
// omitting validation for simplicity
|
||||
const {
|
||||
name,
|
||||
store_id,
|
||||
permissions = [],
|
||||
} = req.body
|
||||
|
||||
const roleService = req.scope.resolve(
|
||||
"roleService"
|
||||
) as RoleService
|
||||
|
||||
const role = await roleService.create({
|
||||
name,
|
||||
store_id,
|
||||
permissions,
|
||||
})
|
||||
|
||||
res.json(role)
|
||||
}
|
||||
```
|
||||
|
||||
This creates the Create Role endpoint that uses the `RoleService` to create a new role. Notice that validation of received body parameters is omitted for simplicity.
|
||||
|
||||
Next, create the file `src/api/routes/admin/role/associate-user.ts` with the following content:
|
||||
|
||||
```ts title=src/api/routes/admin/role/associate-user.ts
|
||||
import { Request, Response } from "express"
|
||||
import RoleService from "../../../../services/role"
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
// omitting validation for simplicity purposes
|
||||
const {
|
||||
id,
|
||||
user_id,
|
||||
} = req.params
|
||||
|
||||
const roleService = req.scope.resolve(
|
||||
"roleService"
|
||||
) as RoleService
|
||||
const role = await roleService.associateUser(id, user_id)
|
||||
|
||||
res.json(role)
|
||||
}
|
||||
```
|
||||
|
||||
This creates the Associate User endpoint that uses the `RoleService` to associate a role with a user.
|
||||
|
||||
You now have to register and export these endpoints.
|
||||
|
||||
To do that, create the file `src/api/routes/admin/role/index.ts` with the following content:
|
||||
|
||||
```ts title=src/api/routes/admin/role/index.ts
|
||||
import { wrapHandler } from "@medusajs/utils"
|
||||
import { Router } from "express"
|
||||
import createRole from "./create-role"
|
||||
import associateUser from "./associate-user"
|
||||
|
||||
const router = Router()
|
||||
|
||||
export default (adminRouter: Router) => {
|
||||
adminRouter.use("/roles", router)
|
||||
|
||||
router.post("/", wrapHandler(createRole))
|
||||
router.post("/:id/user/:user_id", wrapHandler(associateUser))
|
||||
}
|
||||
```
|
||||
|
||||
This adds the create role endpoint under the path `/admin/roles`, and the associate user endpoint under the path `/admin/roles/:id/user/:user_id`, where `:id` is the ID of the role and `:user_id` is the ID of the user to associate with the role.
|
||||
|
||||
Finally, you can either export these routes in `src/api/routes/admin/index.ts` or, if the file is not available in your project, in `src/api/index.ts`:
|
||||
|
||||
```ts title=src/api/routes/admin/index.ts
|
||||
import roleRouter from "./role"
|
||||
|
||||
const router = Router()
|
||||
|
||||
export function attachAdminRoutes(adminRouter: Router) {
|
||||
roleRouter(adminRouter)
|
||||
// ....
|
||||
}
|
||||
```
|
||||
|
||||
To test it out, run the `build` command in the root directory of your Medusa backend project:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run build
|
||||
```
|
||||
|
||||
Then, start the backend with the following command:
|
||||
|
||||
```bash
|
||||
npx medusa develop
|
||||
```
|
||||
|
||||
Try first to log in using the [Admin User Login endpoint](https://docs.medusajs.com/api/admin#auth_postauth) with an existing admin user. Then, send a `POST` request to the `localhost:9000/admin/roles` endpoint with the following request body parameters:
|
||||
|
||||
```json
|
||||
{
|
||||
"store_id": "store_01H8XPDY8WA1Z650MZSEY4Y0V0",
|
||||
"name": "Product Manager",
|
||||
"permissions": [
|
||||
{
|
||||
"name": "Allow Products",
|
||||
"metadata": {
|
||||
"/products": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Make sure to replace the `store_id`'s value with your store’s ID. You can retrieve the store’s ID using the [Get Store Details endpoint](https://docs.medusajs.com/api/admin#store_getstore).
|
||||
|
||||
This will create a new role with a permission that allows users of this role to access the `/admin/products` endpoint. As mentioned before, because of the middleware’s implementation, you must specify the path without the `/admin` prefix. If you chose to implement this differently, such as with regular expressions, then change the permission’s metadata accordingly.
|
||||
|
||||
Next, create a new user using the [Create User endpoint](https://docs.medusajs.com/api/admin#users_postusers). Then, send a `POST` request to `localhost:9000/admin/roles/<role_id>/user/<user_id>`, where `<role_id>` is the ID of the role you created, and `<user_id>` is the ID of the user you created. This will associate the user with the role you created.
|
||||
|
||||
Finally, login with the user you created, then try to access any endpoint other than `/admin/products`. You’ll receive a `401` unauthorized response. Then, try to access the [List Products endpoint](https://docs.medusajs.com/api/admin#products_getproducts), and the user should be able to access it as expected.
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## Additional Development
|
||||
|
||||
If your use case requires other changes or functionality implementations, check out the [Medusa Development section](../development/overview.mdx) of the documentation for all available development guides.
|
||||
@@ -113,6 +113,44 @@ Implementing the logic depends on your use case, but you'll mainly need to do tw
|
||||
|
||||
---
|
||||
|
||||
## Customize Admin
|
||||
|
||||
As you add subscription features to your store, you'll most likely need to customize the admin to provide an interface to manage these features.
|
||||
|
||||
Medusa's [admin plugin](../admin/quickstart.mdx) can be extended to add widgets or new blocks to existing pages, add UI routes to add new pages, or add setting pages.
|
||||
|
||||
<DocCardList colSize={4} items={[
|
||||
{
|
||||
type: 'link',
|
||||
href: '/admin/widgets',
|
||||
label: 'Create Admin Widget',
|
||||
customProps: {
|
||||
icon: Icons['academic-cap-solid'],
|
||||
description: 'Learn how to add widgets into existing admin pages.',
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
href: '/admin/routes',
|
||||
label: 'Create Admin UI Routes',
|
||||
customProps: {
|
||||
icon: Icons['academic-cap-solid'],
|
||||
description: 'Learn how to add new pages to your Medusa admin.',
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
href: '/admin/setting-pages',
|
||||
label: 'Create Admin Setting Page',
|
||||
customProps: {
|
||||
icon: Icons['academic-cap-solid'],
|
||||
description: 'Learn how to add new page to the Medusa admin settings.',
|
||||
}
|
||||
},
|
||||
]} />
|
||||
|
||||
---
|
||||
|
||||
## Build a Storefront
|
||||
|
||||
Medusa provides a Next.js Starter Template that you can use with Medusa. Since you've customized your Medusa project, you'll need to either customize the existing Next.js Starter Template, or create a custom storefront.
|
||||
|
||||
@@ -57,6 +57,11 @@ module.exports = {
|
||||
id: "recipes/subscriptions",
|
||||
label: "Subscriptions",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
id: "recipes/rbac",
|
||||
label: "RBAC",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -95,6 +95,12 @@ const paths: LearningPathType[] = [
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Implement Role-Based Access Control",
|
||||
path: "/recipes/rbac",
|
||||
description:
|
||||
"In your marketplace, you may need to implement role-based access control (RBAC) within stores. This will restrict some users' permissions to specified functionalities or endpoints.",
|
||||
},
|
||||
{
|
||||
title: "Create a storefront",
|
||||
path: "/starters/nextjs-medusa-starter",
|
||||
@@ -193,6 +199,82 @@ const paths: LearningPathType[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rbac",
|
||||
label: "Role-based access control (RBAC)",
|
||||
description: "Implement roles and permissions for admin users in Medusa",
|
||||
steps: [
|
||||
{
|
||||
title: "Create Role and Permission Entities",
|
||||
path: "/development/entities/create",
|
||||
description:
|
||||
"When implementing RBAC, you typically require the availability of roles and permissions, both of which would require new entities. A role would include different permissions, such as the ability to access the products’ route, and it can be assigned to one or more users.",
|
||||
},
|
||||
{
|
||||
title: "Extend Entities",
|
||||
path: "/development/entities/extend-entity",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
To associate roles with users, you need to extend the{" "}
|
||||
<code>User</code> entity to add the relation between it and the new{" "}
|
||||
<code>Role</code> entity. You can also extend other entities that
|
||||
are associated with your custom one, such as the <code>Store</code>{" "}
|
||||
entity.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Create Guard Middleware",
|
||||
path: "/development/endpoints/add-middleware",
|
||||
description:
|
||||
"To ensure that users who have the privilege can access an endpoint, you must create a middleware that guards admin routes. This middleware will run on all authenticated admin requests to ensure that only allowed users can access an endpoint.",
|
||||
},
|
||||
{
|
||||
title: "Create Services",
|
||||
path: "/development/services/create-service",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
For every entity you create, such as the <code>Role</code> and{" "}
|
||||
<code>Permission</code> entities, you must create a service that
|
||||
provides create, read, update, and delete (CRUD) functionalities at
|
||||
the very least.
|
||||
<br />
|
||||
If you also extended entities, such as the <code>User</code> entity,
|
||||
you may need to{" "}
|
||||
<Link to="/development/services/extend-service">
|
||||
extend its core service
|
||||
</Link>{" "}
|
||||
<code>UserService</code> as well to perform custom functionalities
|
||||
related to your implementation.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Create Endpoints",
|
||||
path: "/development/endpoints/create",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
To manage the roles and permissions, you’ll need to create custom
|
||||
endpoints, typically for Create, Read, Update, and Delete (CRUD)
|
||||
operations.
|
||||
<br />
|
||||
After creating the endpoints, you may test adding roles and
|
||||
permissions, and how they provide different access for different
|
||||
roles and users.
|
||||
</>
|
||||
),
|
||||
},
|
||||
],
|
||||
finish: {
|
||||
type: "rating",
|
||||
step: {
|
||||
title: "Congratulations on implementing RBAC!",
|
||||
description: "Please rate your experience using this recipe.",
|
||||
eventName: "rating_path_rbac",
|
||||
},
|
||||
},
|
||||
},
|
||||
// TODO: Eventually remove these learning paths
|
||||
{
|
||||
name: "entity-and-api",
|
||||
label: "Create Entity and Expose it with Endpoints",
|
||||
|
||||
@@ -534,7 +534,7 @@ module.exports = {
|
||||
"button-danger-pressed": "linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, rgba(255, 255, 255, 0.16) 100%)",
|
||||
"button-danger-pressed-dark": "linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, rgba(255, 255, 255, 0.14) 100%)",
|
||||
"code-fade": "linear-gradient(90deg, #11182700, #111827 24px)",
|
||||
"code-fade-dark": "linear-gradient(90deg, #1E1E1E00, #1E1E1E 24px)",
|
||||
"code-fade-dark": "linear-gradient(90deg, #1B1B1F00, #1B1B1F 24px)",
|
||||
"fade": "linear-gradient(to top, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0))",
|
||||
"fade-dark": "linear-gradient(to top, rgba(27, 27, 31, 1), rgba(27, 27, 31, 0))",
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user