772 lines
27 KiB
Plaintext
772 lines
27 KiB
Plaintext
---
|
||
addHowToData: true
|
||
---
|
||
|
||
import DocCardList from '@theme/DocCardList';
|
||
import DocCard from '@theme/DocCard';
|
||
import Icons from '@theme/Icon';
|
||
|
||
# Implement Role-Based Access Control (RBAC)
|
||
|
||
In this document, you'll get a high-level overview of how you can implement role-based access control (RBAC) in your Medusa backend.
|
||
|
||
## 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](../../../recipes/marketplace.mdx).
|
||
|
||
This guide gives you a high-level approach to implementing RBAC in Medusa. The examples included in this guide provide a simple implementation to give you an idea of how you can implement this functionality in your Medusa backend.
|
||
|
||
---
|
||
|
||
## 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 API Route, 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 API Route.
|
||
|
||
Since the Medusa backend uses Express, you can create a middleware and attach it to all admin routes.
|
||
|
||
<DocCard item={{
|
||
type: 'link',
|
||
href: '/development/api-routes/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 API Route.
|
||
|
||
Create the file `src/api/middlewares.ts` with the following content:
|
||
|
||
```ts title="src/api/middlewares.ts"
|
||
import type {
|
||
MiddlewaresConfig,
|
||
UserService,
|
||
} from "@medusajs/medusa"
|
||
import express from "express"
|
||
import type {
|
||
MedusaRequest,
|
||
MedusaResponse,
|
||
MedusaNextFunction,
|
||
} from "@medusajs/medusa"
|
||
|
||
export const permissions = async (
|
||
req: MedusaRequest,
|
||
res: MedusaResponse,
|
||
next: MedusaNextFunction
|
||
) => {
|
||
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)
|
||
}
|
||
|
||
export const config: MiddlewaresConfig = {
|
||
routes: [
|
||
{
|
||
matcher: "/admin/*",
|
||
middlewares: [permissions],
|
||
},
|
||
],
|
||
}
|
||
```
|
||
|
||
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 API Route. Here, you presume that logged-in users who don’t have a role are “super-admin” users who can access all API Routes. 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 API Route. 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 API Route path. However, in middlewares, this doesn’t include the mount point which is `/admin`. So, for example, if the API Route 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).
|
||
|
||
:::
|
||
|
||
After defining the middleware, you apply it on all API Routes starting with `/admin/`.
|
||
|
||
</Details>
|
||
|
||
---
|
||
|
||
## Create API Routes and Services
|
||
|
||
To manage the roles and permissions, you’ll need to create custom API Routes, 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/api-routes/create',
|
||
label: 'Create API Route',
|
||
customProps: {
|
||
icon: Icons['academic-cap-solid'],
|
||
description: 'Learn how to create an API Route 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 API Routes for simplicity: create role API Route that create a new role with permissions, and associate user API Route 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 API Routes 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 API Routes.
|
||
|
||
Start by creating the file `src/api/admin/roles/route.ts` with the following content:
|
||
|
||
```ts title="src/api/admin/roles/route.ts"
|
||
import type {
|
||
MedusaRequest,
|
||
MedusaResponse,
|
||
} from "@medusajs/medusa"
|
||
import RoleService from "../../../../services/role"
|
||
|
||
export const POST = async (
|
||
req: MedusaRequest,
|
||
res: MedusaResponse
|
||
) => {
|
||
// 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 API Route at `/admin/roles` 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/roles/[id]/user/[user_id]/route.ts` with the following content:
|
||
|
||
```ts title="src/api/routes/admin/roles/[id]/user/[user_id]/route.ts"
|
||
import type {
|
||
MedusaRequest,
|
||
MedusaResponse,
|
||
} from "@medusajs/medusa"
|
||
import RoleService from "../../../../services/role"
|
||
|
||
export const POST = async (
|
||
req: MedusaRequest,
|
||
res: MedusaResponse
|
||
) => {
|
||
// 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 API Route at `/admin/roles/[id]/user/[user_id]` that uses the `RoleService` to associate a role with a user.
|
||
|
||
To test the API Routes 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 API Route](https://docs.medusajs.com/api/admin#auth_postauth) with an existing admin user. Then, send a `POST` request to the `localhost:9000/admin/roles` API Route 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 API Route](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` API Route. 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 API Route](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 API Route other than `/admin/products`. You’ll receive a `401` unauthorized response. Then, try to access the [List Products API Route](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.
|