Create a Marketplace with Medusa Part 3: Implement User Management and Permissions
Tags: webdev, javascript, programming, tutorial
Published: 2022-06-21 10:35:15

**_🚨 The content of this tutorial may be outdated. You can instead check out the full code for the series in [this GitHub Repository](https://github.com/shahednasser/medusa-marketplace-tutorial).🚨_** 

In the previous parts of this series, you learned how to build a marketplace using [Medusa](https://github.com/medusajs/medusa) and [Medusa Extender](https://github.com/adrien2p/medusa-extender). You associated users, products, and orders to a store.

This part of the tutorial focuses on user management within a store. This entails adding team members to a store, restricting the visibility of users in the whole marketplace based on the store they’re in, and adding access control to manage permissions within a store’s team.

You can find the code for this tutorial in [this GitHub repository](https://github.com/shahednasser/medusa-marketplace-tutorial/tree/part-3).

You can alternatively use the Medusa Marketplace plugin as indicated in the [README of the GitHub repository](https://github.com/shahednasser/medusa-marketplace). If you’re already using it make sure to update to the latest version:

```bash
npm install medusa-marketplace@latest
```

## Prerequisites

It is assumed that you’ve followed along with the first parts of the series before continuing this part. If you haven’t, you should start from the [first part of the series](https://medusajs.com/blog/medusa-extender/).

Alternatively, clone the `[part-2` branch of the GitHub repository](https://github.com/shahednasser/medusa-marketplace-tutorial/tree/part-2) and [set up and configure your PostgreSQL database](https://docs.medusajs.com/tutorial/set-up-your-development-environment#configuring-your-server). Then, follow [the steps in the README](https://github.com/shahednasser/medusa-marketplace-tutorial/tree/part-2#seed-and-migrate-database).

### Medusa Admin

If you don’t have the [Medusa Admin](https://docs.medusajs.com/admin/quickstart) installed, it is recommended that you install it so that you can easily view products and orders, among other functionalities.

Alternatively, you can use [Medusa’s Admin API](https://docs.medusajs.com/api/admin/auth)s to access the data on your server. However, the rest of the tutorial will mostly showcase features through the Medusa Admin.

## Update Dependencies

Medusa and Medusa Extender both had [new releases](https://medusajs.com/blog/medusa-1-3-1-release/) since the last article. So, before you implement the new functionalities it’s important to update the dependencies related to these 2 first.

In the root of your Medusa server, run the following command to update dependencies:

```bash
npm i @medusajs/medusa@latest medusa-extender@latest @medusajs/medusa-cli@latest medusa-interfaces@latest
```

This will install version `1.3.1` of Medusa’s core packages, and version `1.7.2` of the Medusa Extender. 

Please note that you might receive an error if the version of `awilix` is set to anything other than `4.2.3` while using version `1.3.1`. If you get the error, please update the dependency in `package.json`:

```jsx
"awilix": "4.2.3"
```

> If at the time you’re following along with the tutorial the versions have changed, please note that there might be some difference between the versions used here and the version you have.
> 

Version `1.7.2` of Medusa Extender changes how migrations are found in the code base and now requires you to include the path in the configuration. So, in `medusa-config.js`, add the following in the exported object:

```jsx
module.exports = {
	//other options...
	projectConfig: {
		//other options...
		cli_migration_dirs: [
			'dist/**/*.migration.js'
		]
	},
};
```

### Changes Based on Update

The Medusa Extender’s newest updates bring changes for a better developer experience as well as new features. There is one change that affects the current code you have.

### Change LoggedInUser Middleware

The change entails the `loggedInUser` middleware located in `src/modules/user/middlewares/loggedInUser.middleware.ts`. Currently, the middleware runs for all route paths.

The new Medusa Extender update allows you to specify a regular expression pattern for the `path` route that the current route will be tested on.

So, change the `Middleware` decorator in `src/modules/user/middlewares/loggedInUser.middleware.ts` to the following:

```tsx
@Middleware({ requireAuth: true, routes: [{ method: "all", path: '/admin/*' }] })
```

Following this change, the middleware now will only run when the route starts with `/admin/`.

Then, change the code inside the `consume` method to the following:

```tsx
public async consume(req: MedusaAuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
    const userService = req.scope.resolve('userService') as UserService;
    const loggedInUser = await userService.retrieve(req.user.userId, {
        select: ['id', 'store_id']
    });
    req.scope.register({
        loggedInUser: {
            resolve: () => loggedInUser,
        },
    });
    next();
}
```

Previously, you had to check whether the current route is an admin route inside the middleware. As this is not necessary anymore, you just set the logged-in user right away without checking.

### Change loggedInUser in Services

This change also means that the `loggedInUser` will not always be in the scope as you previously implemented it. So, you’ll need to update how the `loggedInUser` was accessed before in multiple places.

In `src/modules/order/order.service.ts`, change the `loggedInUser` in `InjectedDependencies` to the following:

```tsx
loggedInUser?: User;
```

Then, change the `if` condition in the `buildQuery_` method to the following:

```tsx
if (Object.keys(this.container).includes('loggedInUser') && this.container.loggedInUser.store_id) {
    selector['store_id'] = this.container.loggedInUser.store_id;
}
```

As the `container` acts as a proxy object to the container object the `awilix` package gives us, you can check if a property exists in the `container` using `Object.keys(this.container).includes`. If you access the property directly and it doesn’t exist in the container, the error `AwilixResolutionError` will be thrown.

The next change is in `src/modules/product/services/product.service.ts`. Change the `loggedInUser` in `ConstructorParams` to the following:

```tsx
loggedInUser?: User;
```

Then, in the `prepareListQuery_` method change the declaration and initialization of `loggedInUser` to the following:

```tsx
const loggedInUser = Object.keys(this.container).includes('loggedInUser') ? this.container.loggedInUser : null
```

> You’re not making changes in `attachStoreToProduct` since a product can only be added if a user is logged in.
> 

Finally, in `src/modules/store/services/store.service.ts`, change the `loggedInUser` in `ConstructorParams` to the following:

```tsx
loggedInUser?: User;
```

Then, in the `retrieve` method change the `if` condition at the beginning of the method to the following:

```tsx
if (!Object.keys(this.container).includes('loggedInUser')) {
	return super.retrieve(relations);
}
```

### Test Current Code

The new update and changes will not affect how your marketplace was previously functioning. To test it out, run the server:

```bash
npm start
```

Then, you can try out the previous functionalities you implemented. Everything should work as expected.

You can run the Medusa admin to test out functionalities on it by going to the directory of the Medusa admin and running the following command:

```bash
npm start
```

## Retrieve Users By Store

Go to your Medusa admin and log in with a user that has a store. Alternatively, you can use the [Authenticate a User](https://docs.medusajs.com/api/admin/auth/authenticate-a-user) endpoint.

Then, on the Medusa admin go to Settings > Users, or use the [Retrieve all Users](https://docs.medusajs.com/api/admin/users/retrieve-all-users) REST API. At the moment, only one user is part of each store. However, you can see that all users are retrieved and not just the users that are part of the store the currently logged-in user is in. 

![Users](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u2466tltohza44mzug0a.png)

To change this, go to `src/modules/user/services/user.service.ts` and add `loggedInUser` to `ConstructorParams`:

```tsx
type ConstructorParams = {
    //...
    loggedInUser?: User;
};
```

Next, change the `Service` decorator to the following:

```jsx
@Service({ scope: 'SCOPED', override: MedusaUserService })
```

This allows the `UserService` to access the `loggedInUser` in the scope.

Finally, add the following method inside the `UserService` class:

```tsx
buildQuery_(selector, config = {}): object {
    if (Object.keys(this.container).includes('loggedInUser') && this.container.loggedInUser.store_id) {
        selector['store_id'] = this.container.loggedInUser.store_id;
    }
    
    return super.buildQuery_(selector, config);
}
```

This filters out the users based on the `store_id` of the logged-in user.

### Test Retrieve Users by Store

If you restart the Medusa server now and check the Users page, you’ll see only one user in the team now.

![Test Users](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8kj7zl019bv5d7vgl3yj.png)

## Add Users to a Store

In the previous implementation, you created a new store for every new user. In this section, you’ll change that to allow users to add other users to their store.

To do that, go to `src/modules/store/services/store.service.ts` and change the `createStoreForNewUser` method to the following:

```tsx
@OnMedusaEntityEvent.Before.Insert(User, { async: true })
public async createStoreForNewUser(
	params: MedusaEventHandlerParams<User, 'Insert'>
): Promise<EntityEventType<User, 'Insert'>> {
	const { event } = params;
	let store_id = Object.keys(this.container).includes("loggedInUser")
      ? this.container.loggedInUser.store_id
      : null;
	if (!store_id) {
		const createdStore = await this.withTransaction(event.manager).createForUser(event.entity);
		if (!!createdStore) {
			store_id = createdStore.id;
		}
	}
	
	event.entity.store_id = store_id;

	return event;
}
```

This method now checks if the currently logged-in user has a `store_id` and attaches it to the new user. Otherwise, it creates a new store for the new user.

This implementation also allows a super admin that does not belong to any store to create new users with new stores.

### Test Adding Users to a Store

The Medusa admin only includes the invite functionality to add new users, which you’ll work on in the next section. So, to test out this functionality you need to use the REST APIs.

Restart your Medusa server, then [log in using the REST API](https://docs.medusajs.com/api/admin/auth/authenticate-a-user) with a user that belongs to a store.

Next, send a request to the [Create a User](https://docs.medusajs.com/api/admin/users/create-a-user) endpoint.

![Create User Request](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yomvjfz9px0czapeo1pu.png)

You can now go to the Medusa admin or use the [Retrieve a User](https://docs.medusajs.com/api/admin/users/retrieve-all-users) endpoint. You should see one more user in the team other than the previously created user.


![Check added user](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e9npfmruew326hno3i3n.png)

## Associate Invites with Stores

When an admin user invites another user to join their team, whether through an endpoint or through the Medusa admin, a new invite is created.

Then, when the invite is accepted, a user is created using information from that invite as well as information that the person enters when they accept the invite.

In this section, you’ll make the necessary changes that associate an invite with a store and ensure that when the invite is accepted the created user is also associated with the store.

### Create Invite Entity

Create the directory `src/modules/invite`. This will hold all files related to the invites module.

Then, create the file `src/modules/invite/invite.entity.ts` with the following content:

```tsx
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";

import { Entity as MedusaEntity } from "medusa-extender";
import { Invite as MedusaInvite } from "@medusajs/medusa";
import { Store } from "../store/entities/store.entity";

@MedusaEntity({override: MedusaInvite})
@Entity()
export class Invite extends MedusaInvite {
	@Index()
	@Column({ nullable: true })
	store_id: string;

	@ManyToOne(() => Store, (store) => store.invites)
	@JoinColumn({ name: 'store_id' })
	store: Store;
}
```

You extend the `Invite` entity from Medusa’s core and add to it the `store_id` field and the `store` relation.

This relation should also be added to the `Store` entity. 

So, in `src/modules/store/entities/store.entity.ts` add the following import at the beginning of the file:

```tsx
import { Invite } from './../../invite/invite.entity';
```

And add the following inside the `Store` class:

```tsx
@OneToMany(() => Invite, (invite) => invite.store)
@JoinColumn({ name: 'id', referencedColumnName: 'store_id' })
invites: Invite[];
```

### Create Invite Repository

Next, create the file `src/modules/invite/invite.repository.ts` with the following content:

```tsx
import { Repository as MedusaRepository, Utils } from "medusa-extender";

import { EntityRepository } from "typeorm";
import { Invite } from "./invite.entity";
import { InviteRepository as MedusaInviteRepository } from "@medusajs/medusa/dist/repositories/invite";

@MedusaRepository({override: MedusaInviteRepository})
@EntityRepository(Invite)
export class InviteRepository extends Utils.repositoryMixin<Invite, MedusaInviteRepository>(MedusaInviteRepository) {}
```

This creates a repository for the `Invite` entity that extends `InviteRepository` from Medusa’s core. This allows you to retrieve invites from the database based on the `Invite` entity you created in the previous section.

### Create Migration

To reflect the new column in the database, you need to create a migration file in the invite module.

As migration files have the format `<timestamp>-invite.migration.ts`, a migration file is unique to you so you need to create it yourself.

You can generate the migration file using the following command provided by Medusa Extender’s CLI:

```
./node_modules/.bin/medex g -mi invite

```

This creates the file `src/modules/invite/<timestamp>-invite.migration.ts` for you. Open that file and replace the `up` and `down` methods with the following implementation:

```tsx
public async up(queryRunner: QueryRunner): Promise<void> {
    const query = `
        ALTER TABLE public."invite" ADD COLUMN IF NOT EXISTS "store_id" text; 
    `;
    await queryRunner.query(query);
}

public async down(queryRunner: QueryRunner): Promise<void> {
    const query = `
        ALTER TABLE public."invite" DROP COLUMN "store_id";
    `;
    await queryRunner.query(query);
}
```

The `up` method adds the `store_id` column to the `invite` table, and the `down` method drops the column.

### Run Migrations

To actually reflect these changes in the database, you need to run the migrations.

As migrations can only be run as JavaScript files, run the `build` command to transpile the TypeScript files to JavaScript files:

```tsx
npm run build
```

Then, use the `migrate` command provided by the Medusa Extender CLI to run those migrations:

```bash
./node_modules/.bin/medex migrate --run
```

> If you get an error about duplicate migrations because of migrations from the previous part of this series, go ahead and remove the old ones from the `dist` directory and try running the command again.
> 

If you check your database after the migration is run successfully, you can see that the new column `store_id` added to the `invite` table.

### Change List Invites

Similar to the Retrieve all Users functionality, the List all Invites functionality currently returns all invites irrespective of which store the invite belongs to. 

If you try to invite a user to a store using the Medusa admin or using the [REST APIs](https://docs.medusajs.com/api/admin/invites/create-an-invite), then log into another store with another user, you can see the invite there as well. Only invites that belong to the store of the currently logged-in user should be shown.


![Invites](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jrznsdt8w7knrppjw3t2.png)

So, in this section, you’ll implement the necessary changes so that only invites specific to a store are retrieved.

Create the file `src/modules/invite/invite.service.ts` with the following content:

```tsx
import { ConfigModule } from '@medusajs/medusa/dist/types/global';
import { EntityManager } from 'typeorm';
import { EventBusService } from '@medusajs/medusa';
import { Invite } from './invite.entity';
import { InviteRepository } from './invite.repository';
import { default as MedusaInviteService } from "@medusajs/medusa/dist/services/invite";
import { Service } from "medusa-extender";
import { User } from '../user/entities/user.entity';
import UserRepository from '../user/repositories/user.repository';
import UserService from '../user/services/user.service';

type InviteServiceProps = {
  manager: EntityManager;
  userService: UserService;
  userRepository: UserRepository;
  eventBusService: EventBusService;
  loggedInUser?: User;
  inviteRepository: InviteRepository;
}

@Service({ scope: 'SCOPED', override: MedusaInviteService })
export class InviteService extends MedusaInviteService {
	static readonly resolutionKey = "inviteService"

  private readonly manager: EntityManager;
  private readonly container: InviteServiceProps;
  private readonly inviteRepository: InviteRepository;

  constructor(container: InviteServiceProps, configModule: ConfigModule) {
    super(container, configModule);

    this.manager = container.manager;
    this.container = container;
    this.inviteRepository = container.inviteRepository
  }

	withTransaction(transactionManager: EntityManager): InviteService {
    if (!transactionManager) {
      return this
    }

    const cloned = new InviteService({
      ...this.container,
      manager: transactionManager
    },
    this.configModule_
    )

    cloned.transactionManager = transactionManager

    return cloned
  }

  buildQuery_(selector, config = {}): object {
    if (Object.keys(this.container).includes('loggedInUser') && this.container.loggedInUser.store_id) {
        selector['store_id'] = this.container.loggedInUser.store_id;
    }
    
    return super.buildQuery_(selector, config);
  }
}
```

You override the `InviteService` from Medusa’s core. Inside the service, you override the `buildQuery_` method to filter the invites based on the store ID of the currently logged-in user.

### Create Invite Module

Before you can test out what you just implemented, you need to create an invite module.

Create the file `src/modules/invite/invite.module.ts` with the following content:

```tsx
import { Invite } from "./invite.entity";
import { InviteMigration1655123458263 } from './1655123458263-invite.migration';
import { InviteRepository } from './invite.repository';
import { InviteService } from './invite.service';
import { Module } from "medusa-extender";

@Module({
  imports: [
    Invite,
    InviteRepository,
    InviteService,
    InviteMigration1655123458263,
  ]
})
export class InviteModule {}
```

> Make sure to replace the migration with your own migration class name and file path.
> 

Then, in `src/main.ts` import the `InviteModule` at the beginning of the file:

```tsx
import { InviteModule } from './modules/invite/invite.module';
```

and add it to the array passed to the `load` function:

```tsx
await new Medusa(__dirname + '/../', expressInstance).load([
    //...
    InviteModule,
]);
```

### Listen to Create Invite

In this section, you’ll listen to the “before insert” event on invites then attach `store_id` of the logged-in user to the invite.

To listen to events on entities such as the “before insert” event, you need to create a subscriber and a middleware that registers the subscriber.

To create the subscriber, create the file `src/modules/invite/invite.subscriber.ts` with the following content:

```tsx
import { Connection, EntitySubscriberInterface, EventSubscriber, InsertEvent } from 'typeorm';
import { Utils as MedusaUtils, OnMedusaEntityEvent, eventEmitter } from 'medusa-extender';

import { Invite } from './invite.entity';

@EventSubscriber()
export default class InviteSubscriber implements EntitySubscriberInterface<Invite> {
	static attachTo(connection: Connection): void {
		MedusaUtils.attachOrReplaceEntitySubscriber(connection, InviteSubscriber);
	}

	public listenTo(): typeof Invite {
		return Invite;
	}

	/**
	 * Relay the event to the handlers.
	 * @param event Event to pass to the event handler
	 */
	public async beforeInsert(event: InsertEvent<Invite>): Promise<void> {
		return await eventEmitter.emitAsync(OnMedusaEntityEvent.Before.InsertEvent(Invite), {
			event,
			transactionalEntityManager: event.manager,
		});
	}
}
```

This subscriber emits the “before insert” event whenever a new invite is created.

Then, create the file `src/modules/invite/inviteSubscriber.middleware.ts` with the following content:

```tsx
import {
  MEDUSA_RESOLVER_KEYS,
  MedusaAuthenticatedRequest,
  MedusaMiddleware,
  Utils as MedusaUtils,
  Middleware
} from 'medusa-extender';
import { NextFunction, Response } from 'express';

import { Connection } from 'typeorm';
import InviteSubscriber from './invite.subscriber';

@Middleware({ requireAuth: true, routes: [{ method: "post", path: '/admin/invites*' }] })
export class AttachInviteSubscriberMiddleware implements MedusaMiddleware {
    public async consume(req: MedusaAuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
      const { connection } = req.scope.resolve(MEDUSA_RESOLVER_KEYS.manager) as { connection: Connection };
      InviteSubscriber.attachTo(connection)
      return next();
    }
}
```

This middleware registers the subscriber on all routes that begin with `/admin/invites` if the request method is `POST`.

Next, in `src/modules/store/services/store.service.ts` import `Invite` at the beginning of the file:

```tsx
import { Invite } from '../../invite/invite.entity';
```

Then, add the following method to listen to the “before insert” event and handle it:

```tsx
@OnMedusaEntityEvent.Before.Insert(Invite, { async: true })
public async addStoreToInvite(
	params: MedusaEventHandlerParams<Invite, 'Insert'>
): Promise<EntityEventType<Invite, 'Insert'>> {
	const { event } = params; //invite to be created is in event.entity
	let store_id = this.container.loggedInUser.store_id
	
	if (!event.entity.store_id && store_id) {
		event.entity.store_id = store_id;
	}

	return event;
}
```

This checks if the invite doesn’t already have a `store_id` and if the current user has a `store_id`, then set the `store_id` of the invite to the logged-in user’s invite.

This implementation also enables the super admin to send invites to other super admins.

Finally, you need to add the middleware to the invite module before you can test it out.

In `src/modules/invite/invite.module.ts` add the following import at the beginning of the file:

```tsx
import { AttachInviteSubscriberMiddleware } from "./inviteSubscriber.middleware";
```

Then, add the `AttachInviteSubscriberMiddleware` class to the `imports` array passed to the `Module` directive:

```tsx
@Module({
  imports: [
		//...
    AttachInviteSubscriberMiddleware,
  ]
})
```

### Test List and Create Invites

Restart the server and check the Users page again. Then, create an invite. The invite should now appear in the store it’s associated with and not in all stores.


![Invite per store](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5q5ph29tzf5wi9aejkca.png)

### Associate Invited User with Store

When the invite is accepted, a new user is created. However, the current implementation does not associate the new user with any store.

In this section, you’ll override the [Accept an Invite endpoint](https://docs.medusajs.com/api/admin/invites/accept-an-invite) to add this implementation.

Go to `src/modules/user/services/user.service.ts` and add the new method `addUserToStore`:

```tsx
public async addUserToStore (user_id, store_id) {
await this.atomicPhase(async (m) => {
      const userRepo = m.getCustomRepository(this.userRepository);
      const query = this.buildQuery_({ id: user_id });

      const user = await userRepo.findOne(query);
      if (user) {
          user.store_id = store_id;
          await userRepo.save(user);
      }
  })
}
```

Also, add the `withTransaction` method to override the parent’s method and make sure it returns the custom user service:

```jsx
withTransaction(transactionManager: EntityManager): UserService {
    if (!transactionManager) {
        return this
    }

    const cloned = new UserService({
        ...this.container,
        manager: transactionManager
    })

    cloned.transactionManager = transactionManager

    return cloned
}
```

Next, you need to add a new method to `InviteService` that retrieves an invite by ID.

In `src/modules/invite/invite.service.ts` add the following method in `InviteService`:

```tsx
async retrieve (invite_id: string) : Promise<Invite|null> {
	return await this.atomicPhase_(async (m) => {
    const inviteRepo: InviteRepository = m.getCustomRepository(
      this.inviteRepository
    )

    return await inviteRepo.findOne({ where: { id: invite_id } })

  })
}
```

You can now override the original route that handles accepting invites.

Create the file `src/modules/invite/acceptInvite.controller.ts` with the following content:

```tsx
import { AdminPostInvitesInviteAcceptReq } from "@medusajs/medusa"
import { InviteService } from './invite.service';
import { MedusaError } from 'medusa-core-utils';
import UserService from '../user/services/user.service';
import { validator } from "@medusajs/medusa/dist/utils/validator"

export default async (req, res) => {
	const validated = await validator(AdminPostInvitesInviteAcceptReq, req.body)

  const inviteService: InviteService = req.scope.resolve(InviteService.resolutionKey)

  const manager: EntityManager = req.scope.resolve("manager")

  await manager.transaction(async (m) => {
    //retrieve invite
    let decoded
    try {
      decoded = inviteService
      .withTransaction(m)
      .verifyToken(validated.token)
    } catch (err) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Token is not valid"
      )
    }

    const invite = await inviteService
		.withTransaction(m)
		.retrieve(decoded.invite_id);

    let store_id = invite ? invite.store_id : null;

    const user = await inviteService
    .withTransaction(m)
    .accept(validated.token, validated.user);

    if (store_id) {
      const userService: UserService = req.scope.resolve("userService");
      await userService
			.withTransaction(m)
			.addUserToStore(user.id, store_id);
    }

    res.sendStatus(200)
  })await userService
      .withTransaction(m)
      .addUserToStore(user.id, store_id);
    }

    res.sendStatus(200)
  })
}
```

This uses the same implementation as the [original accept invite endpoint](https://github.com/medusajs/medusa/blob/master/packages/medusa/src/api/routes/admin/invites/accept-invite.ts). However, it retrieves the invite using the `retrieve` method you created earlier. Then, it retrieves the `store_id` of the invite.

The `accept` method in `inviteService` creates a new user and returns it. You then check if the `store_id` was set on the invite and add the `store_id` to the user using the `userService.addUserToStore` method.

To make this function the handler of the Accept an Invite endpoint, create the file `src/modules/invite/invite.router.ts` with the following content:

```tsx
import { Router } from 'medusa-extender';
import acceptInvite from './acceptInvite.controller';

@Router({
    routes: [
        {
            requiredAuth: false,
            path: '/admin/invites/accept',
            method: 'post',
            handlers: [acceptInvite],
        },
    ],
})
export class AcceptInviteRouter {}
```

The last step is to add this router to the invite module.

In `src/modules/invite/invite.module.ts` import this class at the beginning of the function:

```tsx
import { AcceptInviteRouter } from "./invite.router";
```

Then, add it to the `imports` array passed to the `Module` directive:

```tsx
@Module({
  imports: [
    //...
    AcceptInviteRouter,
  ]
})
```

### Test Accepting Invites

Restart your server. Then, send a request to the [List all Invites](https://docs.medusajs.com/api/admin/invites/lists-all-invites) endpoint and retrieve the `token` of the invite you want to accept.

After you retrieve the token, send a request to the [Accept an Invite](https://docs.medusajs.com/api/admin/invites/accept-an-invite) endpoint. You can then check if the new user shows up in the store it’s associated with it.

![User accepted invite](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y7k4300dseu35cms84rr.png)

## Implementing Roles and Permissions

This section covers how to implement user roles and permissions in a brief way. You may choose to implement it differently based on your use case.

This entails creating two new entities `Role` and `Permission`. A permission can be associated with multiple roles, and a role can have multiple permissions. Additionally, a role is associated with a store, and a user is associated with a role.

Then, you’ll create a guard middleware that you can add to any route you want to protect based on a certain permission. You’ll see an example of how to use the guard middleware.

> ⚠️ This section does not cover the endpoints necessary to add permissions and roles and associate them with users. You’ll need to implement it yourself.
> 

### Create the Role Module

Start by creating the `src/modules/role` directory that holds all files related to the role module.

Then, create the file `src/modules/role/role.entity.ts` with the following content:

```tsx
import { BeforeInsert, Column, Entity, Index, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany } from "typeorm";

import { BaseEntity } from "@medusajs/medusa";
import { Entity as MedusaEntity } from "medusa-extender";
import { Permission } from '../permission/permission.entity';
import { Store } from "../store/entities/store.entity";
import { User } from "../user/entities/user.entity";
import { generateEntityId } from "@medusajs/medusa/dist/utils";

@MedusaEntity()
@Entity()
export class Role extends BaseEntity {

  @Column({type: "varchar"})
  name: string;

  @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")
  }
}
```

> If you see errors about missing imports or methods, you’ll be resolving them as you proceed with the next steps.
> 

This creates a `Role` entity that has the properties `name` and `store_id` as well as the relations mentioned earlier.

You need to add these relations to the `Store` and `User` entities.

Go to `src/modules/user/entities/user.entity.ts` and add the following import at the beginning of the file:

```tsx
import { Role } from '../../role/role.entity';
```

Then, add the following inside the `User` entity:

```tsx
@Index()
@Column({ nullable: true })
role_id: string;

@ManyToOne(() => Role, (role) => role.users)
@JoinColumn({ name: 'role_id' })
teamRole: Role;
```

Next, go to `src/modules/store/entities/store.entity.ts` and add the following import at the beginning of the file:

```tsx
import { Role } from '../../role/role.entity';
```

Then, add the following inside the `Store` entity:

```tsx
@OneToMany(() => Role, (role) => role.store)
@JoinColumn({ name: 'id', referencedColumnName: 'store_id' })
roles: Role[];
```

Then, create the file `src/modules/role/role.repository.ts` with the following content:

```tsx
import { EntityRepository, Repository } from "typeorm";

import { Repository as MedusaRepository } from "medusa-extender";
import { Role } from './role.entity';

@MedusaRepository()
@EntityRepository(Role)
export class RoleRepository extends Repository<Role> {}
```

Next, you need to create 2 migrations: one to create the `role` table and one to add the `role_id` column to the `user` table.

Run the following command to create the `role` migration:

```bash
./node_modules/.bin/medex g -mi role
```

Then, go to `src/modules/role/<TIMESTAMP>-role.migration.ts` and add the following import at the beginning of the file:

```tsx
import { TableForeignKey } from 'typeorm';
```

Then, replace the `up` and `down` methods with the following:

```tsx
public async up(queryRunner: QueryRunner): Promise<void> {
    const query = `
    CREATE TABLE "role" ("id" character varying NOT NULL, 
    "name" character varying NOT NULL, "store_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())
    `;
    await queryRunner.query(query);

    await queryRunner.createPrimaryKey("role", ["id"])
    await queryRunner.createForeignKey("role", new TableForeignKey({
        columnNames: ["store_id"],
        referencedColumnNames: ["id"],
        referencedTableName: "store",
        onDelete: "CASCADE",
        onUpdate: "CASCADE"
    }))
}

public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.dropTable("role", true);
}
```

Next, run the following command:

```bash
./node_modules/.bin/medex g -mi user
```

> Running this command can mess up the file `src/modules/user/user.module.ts` as the files in the user module are based on an old structure that the Medusa Extender used. Please check the file for any errors in the imports and resolve them. If it adds an import for `UserSubscriber` you can safely remove it as well as remove it from the `imports` array passed to `Module`.
> 

Go to `src/modules/user/<TIMESTAMP>-user.migration.ts` and add the following import at the beginning of the file:

```tsx
import { TableForeignKey } from 'typeorm';
```

Then, replace the `up` and `down` methods with the following content:

```tsx
public async up(queryRunner: QueryRunner): Promise<void> {
	const query = `ALTER TABLE public."user" ADD COLUMN IF NOT EXISTS "role_id" text;`;
	await queryRunner.query(query);

  await queryRunner.createForeignKey("user", new TableForeignKey({
      columnNames: ["role_id"],
      referencedColumnNames: ["id"],
      referencedTableName: "role",
      onDelete: "CASCADE",
      onUpdate: "CASCADE"
  }))
}

public async down(queryRunner: QueryRunner): Promise<void> {
	const query = `ALTER TABLE public."user" DROP COLUMN "role_id";`;
	await queryRunner.query(query);
}
```

Next, create the file `src/modules/role/role.module.ts` with the following content:

```tsx
import { Module } from "medusa-extender";
import { Role } from "./role.entity";
import { RoleMigration1655131148363 } from './1655131148363-role.migration';
import { RoleRepository } from "./role.repository";

@Module({
  imports: [
    Role,
    RoleRepository,
    RoleMigration1655131148363
  ]
})
export class RoleModule {}
```

> Make sure to replace the migration with your own migration class name and file path.
> 

Finally, at the beginning of `src/main.ts` import the role module:

```tsx
import { RoleModule } from './modules/role/role.module';
```

and add it in the array passed to the `load` function:

```tsx
await new Medusa(__dirname + '/../', expressInstance).load([
    //...
    RoleModule,
]);
```

### Create the Permission Module

Start by creating the `src/modules/permission` folder that holds all files related to the permission module.

Then, create the file `src/modules/permission/permission.entity.ts` with the following content:

```tsx
import { BeforeInsert, Column, Entity, JoinTable, ManyToMany } from "typeorm";

import { BaseEntity } from "@medusajs/medusa";
import { DbAwareColumn } from "@medusajs/medusa/dist/utils/db-aware-column";
import { Entity as MedusaEntity } from "medusa-extender";
import { Role } from "../role/role.entity";
import { generateEntityId } from "@medusajs/medusa/dist/utils";

@MedusaEntity()
@Entity()
export class Permission extends BaseEntity {

  @Column({type: "varchar"})
  name: string;

  @DbAwareColumn({ type: "jsonb", nullable: true })
  metadata: Record<string, unknown>

  @ManyToMany(() => Role)
  @JoinTable({
    name: "role_permissions",
    joinColumn: {
      name: "permission_id",
      referencedColumnName: "id",
    },
    inverseJoinColumn: {
      name: "role_id",
      referencedColumnName: "id",
    },
  })
  roles: Role[]

  @BeforeInsert()
  private beforeInsert(): void {
    this.id = generateEntityId(this.id, "perm")
  }
}
```

This creates the entity `Permission` which has the attributes `name` and `metadata`. You can use the `metadata` attribute, which acts as an object with key-value pairs, to add whatever condition that these permissions entail. For example, you can add the name of paths that the role this permission is associated with can access.

Then, create the file `src/modules/permission/permission.repository.ts` with the following content:

```tsx
import { EntityRepository, Repository } from "typeorm";

import { Repository as MedusaRepository } from "medusa-extender";
import { Permission } from './permission.entity';

@MedusaRepository()
@EntityRepository(Permission)
export class PermissionRepository extends Repository<Permission> {}
```

Next, you need to create the migration for permission. Run the following command to create the migration:

```bash
./node_modules/.bin/medex g -mi permission
```

Then, open the file `src/modules/permission/<TIMESTAMP>-permission.migration.ts` and add the following import at the beginning of the file:

```tsx
import { TableForeignKey } from 'typeorm';
```

and replace the `up` and `down` methods with the following:

```tsx
public async up(queryRunner: QueryRunner): Promise<void> {
    let query = `
    CREATE TABLE "permission" ("id" character varying NOT NULL, 
    "name" character varying NOT NULL, "metadata" jsonb,
    "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now())`;
    await queryRunner.query(query);

    await queryRunner.createPrimaryKey("permission", ["id"])

    query = `
    CREATE TABLE "role_permissions" ("role_id" character varying NOT NULL, "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())`;

    await queryRunner.query(query);

    await queryRunner.createPrimaryKey("role_permissions", ["role_id", "permission_id"])

    await queryRunner.createForeignKey("role_permissions", new TableForeignKey({
        columnNames: ["role_id"],
        referencedColumnNames: ["id"],
        referencedTableName: "role",
        onDelete: "CASCADE",
        onUpdate: "CASCADE"
    }))

    await queryRunner.createForeignKey("role_permissions", new TableForeignKey({
        columnNames: ["permission_id"],
        referencedColumnNames: ["id"],
        referencedTableName: "permission",
        onDelete: "CASCADE",
        onUpdate: "CASCADE"
    }))
}

public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.dropTable("role_permissions", true);
    await queryRunner.dropTable("permission", true);
}
```

Notice that in this migration you also create the table `role_permissions` which creates the many-to-many relation between the two entities.

The final step is to add the guard middleware that will handle checking whether the user can access an endpoint or not.

Create the file `src/modules/permission/permission.guard.ts` with the following content:

```tsx
import UserService from "../user/services/user.service";
import _ from "lodash";

export default (permissions: Record<string, unknown>[]) => {
  return async (req, res, next) => {
    const userService = req.scope.resolve('userService') as UserService;
    const loggedInUser = await userService.retrieve(req.user.userId, {
        select: ['id', 'store_id'],
        relations: ['teamRole', 'teamRole.permissions']
    });
  
    const isAllowed = permissions.every(permission => 
      loggedInUser.teamRole?.permissions.some((userPermission) => _.isEqual(userPermission.metadata, permission))
    )

    if (isAllowed) {      
      return next()    
    }
  
    //permission denied
    res.sendStatus(401)
  }
}
```

The guard accepts the parameter `permissions` which is an array of type any. Then, it returns a function that accepts the `req`, `res`, and `next` parameters as every [middleware in Express](https://expressjs.com/en/guide/using-middleware.html).

The `permissions` parameter is an array of permissions that the logged in user must have before accessing a route. So, inside the returned middleware function, you check that the logged in user has every item in `permissions` as part of their role. You use the `metadata` field in the `Permission` entity to check for equality between the user’s permissions and the required permissions for this route.

If the user has all permissions, they are admitted to the route by calling `next`. Otherwise, the `401` unauthorized status is returned.

Next, create the file `src/modules/permission/permission.module.ts` with the following content:

```tsx
import { Module } from "medusa-extender";
import { Permission } from "./permission.entity";
import { PermissionMigration1655131601491 } from "./1655131601491-permission.migration";
import { PermissionRepository } from "./permission.repository";

@Module({
  imports: [
    Permission,
    PermissionRepository,
    PermissionMigration1655131601491
  ]
})
export class PermissionModule {} 
```

> Make sure to replace the migration with your own migration class name and file path.
> 

Finally, import this file at the beginning of `src/main.ts`:

```tsx
import { PermissionModule } from './modules/permission/permission.module';
```

And add the class to the array passed to the `load` function:

```tsx
await new Medusa(__dirname + '/../', expressInstance).load([
	  //...
    PermissionModule,
]);
```

### Run Migrations

Run the `build` command to transpile the TypeScript files to JavaScript files:

```tsx
npm run build
```

> If you see an error when you run this command, check the imports in `src/modules/invite/invite.module.ts` . This is because of how the Medusa Extender CLI works when you ran the migrate command earlier. If you see an import for `InviteSubscriber` you can safely remove it as well as remove it from the `imports` array passed to `Module`.
> 

Then, use the `migrate` command provided by the Medusa Extender CLI to run those migrations:

```bash
./node_modules/.bin/medex migrate --run
```

> If you get an error about duplicate migrations because of previous migrations, go ahead and remove the old ones from the `dist` directory and try running the command again.
> 

If you check your database after the migration is run successfully, you can see that 2 new tables `role` and `permission` have been added to the database, and the `user` table has a new column `role_id`.

### Test Roles and Permissions

As mentioned earlier in this section, you’ll need to either implement endpoints to add roles and permissions yourself or add them directly to the database if you want to test it out.

Then, to use the permissions middleware, you can pass it to the `handlers` array of any router. For example, here’s a router that restricts access to the List Products endpoint:

```tsx
import { Router } from 'medusa-extender';
import listProductsHandler from '@medusajs/medusa/dist/api/routes/admin/products/list-products';
import permissionGuard from '../permission/permission.guard';
import wrapHandler from '@medusajs/medusa/dist/api/middlewares/await-middleware';

@Router({
    routes: [
        {
            requiredAuth: true,
            path: '/admin/products',
            method: 'get',
            handlers: [
              permissionGuard([
                {path: "/admin/products"}
              ]),
              wrapHandler(listProductsHandler)
            ],
        },
    ],
})
export class ProductsRouter {}
```

Notice that you pass the argument `[{path: "/admin/products"}]` as the permission to check for. If the user has permission that has `metadata` with the same value as the argument, they’ll be able to access the endpoint. Otherwise, they’ll be unauthorized to access.

You can pass multiple permissions in the array.

> Make sure to pass the router to the module it is associated with and restart the server before testing it out.
> 

![Unauthorized request](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2g4h89f9jy0uq7trhl8t.png)

## What’s Next?

In the next tutorial in this series, you’ll learn how to make customization to endpoints and to the Medusa admin to make sure the super admin can manage the marketplace as a whole.

You can also check out the following resources for additional help while developing your marketplace with Medusa:

- Learn how to [deploy Medusa on Heroku](https://docs.medusajs.com/how-to/deploying-on-heroku)
- Learn how to add [Stripe as a payment provider](https://docs.medusajs.com/add-plugins/stripe).
- Learn how to add a storefront either with [Next.js](https://docs.medusajs.com/starters/nextjs-medusa-starter) or [Gatsby](https://docs.medusajs.com/starters/gatsby-medusa-starter).

> Should you have any issues or questions related to Medusa, then feel free to reach out to the Medusa team via [Discord](https://discord.gg/F87eGuwkTp).