- Create a product and set its general details such as title and
- description, its price, options, variants, images, and more. You'll then
- use the product to create a sample order.
-
-
- If you're not ready to create a product, we can create a sample product
- for you.
-
- {!isComplete && (
-
-
-
+ return (
+
+
+ Create a product and set its general details such as title and
+ description, its price, options, variants, images, and more. You'll then
+ use the product to create a sample order.
+
+
+ If you're not ready to create a product, we can create a sample product
+ for you.
+
+ {!isComplete && (
+
+
+
+ )}
+
+ )
+}
+
+export default ProductsList
+```
+
+
+
+
+
+ProductDetail component
+
+
+
+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:
+
+
+
+```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 {
+ StepContentProps,
+} from "../../../widgets/onboarding-flow/onboarding-flow"
+
+const ProductDetail = ({ onNext, isComplete, data }: StepContentProps) => {
+ const {
+ publishable_api_keys: keys,
+ isLoading,
+ } = useAdminPublishableApiKeys({
+ offset: 0,
+ limit: 1,
+ })
+ const api_key = keys?.[0]?.id || "pk_01H0PY648BTMEJR34ZDATXZTD9"
+ return (
+
+
On this page, you can view your product's details and edit them.
+
+ You can preview your product using Medusa's Store APIs. You can copy any
+ of the following code snippets to try it out.
+
+
+ {!isLoading && (
+
)}
- )
- }
-
- export default ProductsList
- ```
-
-
-
-
-
- ProductDetail component
-
-
-
- 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:
-
-
-
- ```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 {
- StepContentProps,
- } from "../../../widgets/onboarding-flow/onboarding-flow"
-
- const ProductDetail = ({ onNext, isComplete, data }: StepContentProps) => {
- const {
- publishable_api_keys: keys,
- isLoading,
- } = useAdminPublishableApiKeys({
- offset: 0,
- limit: 1,
- })
- const api_key = keys?.[0]?.id || "pk_01H0PY648BTMEJR34ZDATXZTD9"
- return (
-
-
On this page, you can view your product's details and edit them.
-
- You can preview your product using Medusa's Store APIs. You can copy any
- of the following code snippets to try it out.
-
+ With a Product created, we can now place an Order. Click the button
+ below to create a sample order.
+
+
+
+ {!isComplete && (
+
+ )}
+
+ >
+ )
+}
+
+export default OrdersList
+```
-
- OrderDetail component
-
-
+
+OrderDetail component
- 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:
+
+
+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:
-
- ```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 = () => {
- return (
- <>
-
- You finished the setup guide 🎉 You now have your first order. Feel free
- to play around with the order management functionalities, such as
- capturing payment, creating fulfillments, and more.
-
-
- Start developing with Medusa
-
-
- Medusa is a completely customizable commerce solution. We've curated
- some essential guides to kickstart your development with Medusa.
-
+ >
+ )
+}
+
+export default OrderDetail
+```
After creating the above components, import them at the top of `src/admin/widgets/onboarding-flow/onboarding-flow.tsx`:
diff --git a/www/docs/content/modules/products/storefront/use-categories.mdx b/www/docs/content/modules/products/storefront/use-categories.mdx
index a1578327e6..9904b10973 100644
--- a/www/docs/content/modules/products/storefront/use-categories.mdx
+++ b/www/docs/content/modules/products/storefront/use-categories.mdx
@@ -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
diff --git a/www/docs/content/recipes/index.mdx b/www/docs/content/recipes/index.mdx
index b62cde35f3..ad2ffdf607 100644
--- a/www/docs/content/recipes/index.mdx
+++ b/www/docs/content/recipes/index.mdx
@@ -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.
-
+
+---
+
## Can't find your path?
diff --git a/www/docs/content/recipes/marketplace.mdx b/www/docs/content/recipes/marketplace.mdx
index edee059014..13da8ac673 100644
--- a/www/docs/content/recipes/marketplace.mdx
+++ b/www/docs/content/recipes/marketplace.mdx
@@ -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.
+
+
+
+---
+
+## 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.
+
+
+
+---
+
## 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.
diff --git a/www/docs/content/recipes/rbac.mdx b/www/docs/content/recipes/rbac.mdx
new file mode 100644
index 0000000000..81bfd0d783
--- /dev/null
+++ b/www/docs/content/recipes/rbac.mdx
@@ -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.
+
+
+
+---
+
+## 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.
+
+
+
+
+
+Example Implementation
+
+
+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
+
+ @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:
+
+
+
+```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 {
+ 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 {
+ 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.
+
+
+
+---
+
+## 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.
+
+
+
+
+Example Implementation
+
+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.
+
+
+
+---
+
+## 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.
+
+
+
+
+
+Example Implementation
+
+
+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 {
+ 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 & {
+ 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 {
+ // 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 {
+ 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 {
+ 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//user/`, where `` is the ID of the role you created, and `` 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.
+
+
+
+---
+
+## 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.
diff --git a/www/docs/content/recipes/subscriptions.mdx b/www/docs/content/recipes/subscriptions.mdx
index f5171c433f..a5261abe5c 100644
--- a/www/docs/content/recipes/subscriptions.mdx
+++ b/www/docs/content/recipes/subscriptions.mdx
@@ -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.
+
+
+
+---
+
## 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.
diff --git a/www/docs/sidebars.js b/www/docs/sidebars.js
index 3e506861e9..12a8ff4fd2 100644
--- a/www/docs/sidebars.js
+++ b/www/docs/sidebars.js
@@ -57,6 +57,11 @@ module.exports = {
id: "recipes/subscriptions",
label: "Subscriptions",
},
+ {
+ type: "doc",
+ id: "recipes/rbac",
+ label: "RBAC",
+ },
],
},
{
diff --git a/www/docs/src/utils/learning-paths.tsx b/www/docs/src/utils/learning-paths.tsx
index b52abbf2c1..904a300c2c 100644
--- a/www/docs/src/utils/learning-paths.tsx
+++ b/www/docs/src/utils/learning-paths.tsx
@@ -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{" "}
+ User entity to add the relation between it and the new{" "}
+ Role entity. You can also extend other entities that
+ are associated with your custom one, such as the Store{" "}
+ 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 Role and{" "}
+ Permission entities, you must create a service that
+ provides create, read, update, and delete (CRUD) functionalities at
+ the very least.
+
+ If you also extended entities, such as the User entity,
+ you may need to{" "}
+
+ extend its core service
+ {" "}
+ UserService 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.
+
+ 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",
diff --git a/www/tailwind.config.js b/www/tailwind.config.js
index 544e48092b..f03b7a3e75 100644
--- a/www/tailwind.config.js
+++ b/www/tailwind.config.js
@@ -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))",
},