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
+2383
-1386
No files matched your search
+1378
-1378
File diff suppressed because it is too large.
Load diff
@@ -5,6 +5,7 @@ addHowToData: true
|
|||||||
|
|
||||||
import Tabs from '@theme/Tabs';
|
import Tabs from '@theme/Tabs';
|
||||||
import TabItem from '@theme/TabItem';
|
import TabItem from '@theme/TabItem';
|
||||||
|
import Badge from '@site/src/components/Badge';
|
||||||
|
|
||||||
# How to Use Product Categories in a Storefront
|
# 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={[
|
<DocCard item={{
|
||||||
"simple-quickstart",
|
type: 'link',
|
||||||
"marketplace",
|
href: '/recipes/rbac',
|
||||||
"subscriptions"
|
label: 'Build RBAC',
|
||||||
]} />
|
customProps: {
|
||||||
|
icon: Icons['credit-card-solid'],
|
||||||
|
description: 'Learn how you can implement role-based access control in Medusa.'
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
|
||||||
<Feedback
|
<Feedback
|
||||||
event="survey_use_cases"
|
event="survey_use_cases"
|
||||||
@@ -81,6 +85,8 @@ This section includes general recipes available to guide you through your develo
|
|||||||
showLongForm={false}
|
showLongForm={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
<!-- vale docs.HeadingPunctuation = NO -->
|
<!-- vale docs.HeadingPunctuation = NO -->
|
||||||
|
|
||||||
## Can't find your path?
|
## Can't find your path?
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
---
|
||||||
|
addHowToData: true
|
||||||
|
---
|
||||||
|
|
||||||
import DocCardList from '@theme/DocCardList';
|
import DocCardList from '@theme/DocCardList';
|
||||||
import DocCard from '@theme/DocCard';
|
import DocCard from '@theme/DocCard';
|
||||||
import Icons from '@theme/Icon';
|
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
|
## 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.
|
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
|
## 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.
|
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",
|
id: "recipes/subscriptions",
|
||||||
label: "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",
|
title: "Create a storefront",
|
||||||
path: "/starters/nextjs-medusa-starter",
|
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",
|
name: "entity-and-api",
|
||||||
label: "Create Entity and Expose it with Endpoints",
|
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": "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%)",
|
"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": "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": "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))",
|
"fade-dark": "linear-gradient(to top, rgba(27, 27, 31, 1), rgba(27, 27, 31, 0))",
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in new issue
Block a user