How to Create an Actor Type
In this document, learn how to create an actor type and authenticate its associated data model.
0. Create Module with Data Model#
Before creating an actor type, you must have a module with a data model representing the actor type.
The rest of this guide uses this Manager data model, defined in a company module, as an example:
1. Create Workflow#
Start by creating a workflow that does two things:
- Creates a record of the
Managerdata model. - Sets the
app_metadataproperty of the associatedAuthIdentityrecord based on the new actor type.
For example, create the file src/workflows/create-manager.ts. with the following content:
1import { 2 createWorkflow, 3 createStep,4 StepResponse,5 WorkflowResponse,6} from "@medusajs/framework/workflows-sdk"7import { 8 setAuthAppMetadataStep,9} from "@medusajs/medusa/core-flows"10import CompanyModuleService from "../modules/company/service"11 12type CreateManagerWorkflowInput = {13 manager: {14 first_name: string15 last_name: string16 email: string17 }18 authIdentityId: string19}20 21const createManagerStep = createStep(22 "create-manager-step",23 async ({ 24 manager: managerData,25 }: Pick<CreateManagerWorkflowInput, "manager">, 26 { container }) => {27 const companyModuleService: CompanyModuleService = 28 container.resolve("company")29 30 const manager = await companyModuleService.createManagers(31 managerData32 )33 34 return new StepResponse(manager)35 }36)37 38const createManagerWorkflow = createWorkflow(39 "create-manager",40 function (input: CreateManagerWorkflowInput) {41 const manager = createManagerStep({42 manager: input.manager,43 })44 45 setAuthAppMetadataStep({46 authIdentityId: input.authIdentityId,47 actorType: "manager",48 value: manager.id,49 })50 51 return new WorkflowResponse(manager)52 }53)54 55export default createManagerWorkflow
This workflow accepts the manager’s data and the associated auth identity’s ID as inputs. The next sections explain how the auth identity ID is retrieved.
The workflow has two steps:
- Create the manager using the
createManagerStep. - Set the
app_metadataproperty of the associated auth identity using thesetAuthAppMetadataStepfrom Medusa's core workflows. You specify the actor typemanagerin theactorTypeproperty of the step’s input.
2. Define the Create API Route#
Next, you’ll use the workflow defined in the previous section in an API route that creates a manager.
So, create the file src/api/manager/route.ts with the following content:
1import type { 2 AuthenticatedMedusaRequest,3 MedusaResponse,4} from "@medusajs/framework/http"5import { MedusaError } from "@medusajs/framework/utils"6import createManagerWorkflow from "../../workflows/create-manager"7 8type RequestBody = {9 first_name: string10 last_name: string11 email: string12}13 14export async function POST(15 req: AuthenticatedMedusaRequest<RequestBody>, 16 res: MedusaResponse17) {18 // If `actor_id` is present, the request carries 19 // authentication for an existing manager20 if (req.auth_context.actor_id) {21 throw new MedusaError(22 MedusaError.Types.INVALID_DATA,23 "Request already authenticated as a manager."24 )25 }26 27 const { result } = await createManagerWorkflow(req.scope)28 .run({29 input: {30 manager: req.body,31 authIdentityId: req.auth_context.auth_identity_id,32 },33 })34 35 res.status(200).json({ manager: result })36}
Since the manager must be associated with an AuthIdentity record, the request is expected to be authenticated, even if the manager isn’t created yet. This can be achieved by:
- Obtaining a token using the /auth route.
- Passing the token in the bearer header of the request to this route.
In the API route, you create the manager using the workflow from the previous section and return it in the response.
3. Apply the authenticate Middleware#
The last step is to apply the authenticate middleware on the API routes that require a manager’s authentication.
To do that, create the file src/api/middlewares.ts with the following content:
1import { 2 defineMiddlewares,3 authenticate,4} from "@medusajs/framework/http"5 6export default defineMiddlewares({7 routes: [8 {9 matcher: "/manager",10 method: "POST",11 middlewares: [12 authenticate("manager", ["session", "bearer"], {13 allowUnregistered: true,14 }),15 ],16 },17 {18 matcher: "/manager/me*",19 middlewares: [20 authenticate("manager", ["session", "bearer"]),21 ],22 },23 ],24})
This applies middlewares on two route patterns:
- The
authenticatemiddleware is applied on the/managerAPI route forPOSTrequests while allowing unregistered managers. This requires that a bearer token be passed in the request to access the manager’s auth identity but doesn’t require the manager to be registered. - The
authenticatemiddleware is applied on all routes starting with/manager/me, restricting these routes to authenticated managers only.
Retrieve Manager API Route#
For example, create the file src/api/manager/me/route.ts with the following content:
1import { 2 AuthenticatedMedusaRequest,3 MedusaResponse,4} from "@medusajs/framework/http"5import CompanyModuleService from "../../../modules/company/service"6import {7 ContainerRegistrationKeys,8} from "@medusajs/framework/utils"9 10export async function GET(11 req: AuthenticatedMedusaRequest,12 res: MedusaResponse13): Promise<void> {14 const query = req.scope.resolve(15 ContainerRegistrationKeys.QUERY16 )17 const managerId = req.auth_context?.actor_id18 19 const { data: [manager] } = await query.graph({20 entity: "manager",21 fields: ["*"],22 filters: {23 id: managerId,24 },25 }, {26 throwIfKeyNotFound: true,27 })28 29 res.json({ manager })30}
This route is only accessible by authenticated managers. You access the manager’s ID using req.auth_context.actor_id.
Test Custom Actor Type Authentication Flow#
To authenticate managers:
- Send a
POSTrequest to/auth/manager/emailpass/registerto create an auth identity for the manager:
Copy the returned token to use it in the next request.
- Send a
POSTrequest to/managerto create a manager:
Replace {token} with the token returned in the previous step.
- Send a
POSTrequest to/auth/manager/emailpassagain to retrieve an authenticated token for the manager:
- You can now send authenticated requests as a manager. For example, send a
GETrequest to/manager/meto retrieve the authenticated manager’s details:
Whenever you want to log in as a manager, use the /auth/manager/emailpass API route, as explained in step 3.
Delete User of Actor Type#
When you delete a user of the actor type, you must update its auth identity to remove the association to the user.
If the auth identity is shared with other actor types (for example, the same user is both a manager and a customer), you should only remove the manager association. If the manager is the only actor type associated with the auth identity, you should delete the auth identity entirely using deleteAuthIdentityStep.
For example, create the following workflow that deletes a manager and updates its auth identity, create the file src/workflows/delete-manager.ts with the following content:
5import CompanyModuleService from "../modules/company/service"6 7export type DeleteManagerWorkflow = {8 id: string9}10 11const deleteManagerStep = createStep(12 "delete-manager-step",13 async (14 { id }: DeleteManagerWorkflow, 15 { container }) => {16 const companyModuleService: CompanyModuleService = 17 container.resolve("company")18 19 const manager = await companyModuleService.retrieveManager(id)20 21 await companyModuleService.deleteManagers(id)22 23 return new StepResponse(undefined, { manager })24 },25 async ({ manager }, { container }) => {26 const companyModuleService: CompanyModuleService = 27 container.resolve("company")28 29 await companyModuleService.createManagers(manager)30 }31 )
You add a step that deletes the manager using the deleteManagers method of the module's main service. In the compensation function, you create the manager again.
Next, in the same file, add the workflow that deletes a manager:
14} from "@medusajs/medusa/core-flows"15 16// ...17 18export const deleteManagerWorkflow = createWorkflow(19 "delete-manager",20 (21 input: WorkflowData<DeleteManagerWorkflow>22 ): WorkflowResponse<string> => {23 deleteManagerStep(input)24 25 const { data: authIdentities } = useQueryGraphStep({26 entity: "auth_identity",27 fields: ["id", "app_metadata"],28 filters: {29 app_metadata: {30 // the ID is of the format `{actor_type}_id`.31 manager_id: input.id,32 },33 },34 })35 36 const authIdentity = transform(37 { authIdentities },38 ({ authIdentities }) => {39 return authIdentities[0]40 }41 )42 43 const shouldKeepAuthIdentity = transform(44 { authIdentity },45 ({ authIdentity }) => {46 if (!authIdentity) {47 return undefined48 }49 return Object.entries(authIdentity.app_metadata ?? {})50 .filter(([key]) => key !== "manager_id")51 .some(([, value]) => value !== null)52 }53 )54 55 when({ shouldKeepAuthIdentity }, ({ shouldKeepAuthIdentity }) => {56 return shouldKeepAuthIdentity === true57 }).then(() => {58 setAuthAppMetadataStep({59 authIdentityId: authIdentity.id,60 actorType: "manager",61 value: null,62 })63 })64 65 when({ shouldKeepAuthIdentity }, ({ shouldKeepAuthIdentity }) => {66 return shouldKeepAuthIdentity === false67 }).then(() => {68 deleteAuthIdentityStep({69 id: authIdentity.id,70 })71 })72 73 return new WorkflowResponse(input.id)74 }75)
In the workflow, you:
- Use the
deleteManagerStepdefined earlier to delete the manager. - Retrieve the auth identity of the manager using Query. To do that, you filter the
app_metadataproperty of an auth identity, which holds the user's ID under{actor_type_name}_id. So, in this case, it'smanager_id. - Check whether the auth identity is shared with other actor types by inspecting the remaining non-null entries in
app_metadata. - If the auth identity is shared, call
setAuthAppMetadataStepto remove only the manager association. If it is the sole actor type, calldeleteAuthIdentityStepto delete the auth identity entirely.
You can use this workflow when deleting a manager, such as in an API route.