feat: Reset password (#8962)
* wip * more work * wip * more work * wrap up first iteration * work on new approach * more work * move middleware func to route * cleanup * more work * wrap up * more work * fix workflow * minor tweaks * finalize * Use JWT secret instead
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { generateResetPasswordTokenWorkflow } from "@medusajs/core-flows"
|
||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||
import {
|
||||
adminHeaders,
|
||||
@@ -8,120 +9,250 @@ jest.setTimeout(30000)
|
||||
|
||||
medusaIntegrationTestRunner({
|
||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||
let container
|
||||
beforeEach(async () => {
|
||||
await createAdminUser(dbConnection, adminHeaders, getContainer())
|
||||
container = getContainer()
|
||||
await createAdminUser(dbConnection, adminHeaders, container)
|
||||
})
|
||||
|
||||
it("Invite + registration + authentication flow", async () => {
|
||||
// Create invite
|
||||
const { token: inviteToken } = (
|
||||
await api.post(
|
||||
"/admin/invites",
|
||||
{ email: "newadmin@medusa.js" },
|
||||
adminHeaders
|
||||
)
|
||||
).data.invite
|
||||
afterEach(() => {
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
// Register identity
|
||||
const signup = await api.post("/auth/user/emailpass/register", {
|
||||
email: "newadmin@medusa.js",
|
||||
password: "secret_password",
|
||||
})
|
||||
describe("Full authentication lifecycle", () => {
|
||||
it("Invite + registration + authentication flow", async () => {
|
||||
// Create invite
|
||||
const { token: inviteToken } = (
|
||||
await api.post(
|
||||
"/admin/invites",
|
||||
{ email: "newadmin@medusa.js" },
|
||||
adminHeaders
|
||||
)
|
||||
).data.invite
|
||||
|
||||
expect(signup.status).toEqual(200)
|
||||
expect(signup.data).toEqual({ token: expect.any(String) })
|
||||
|
||||
// Accept invite
|
||||
const response = await api.post(
|
||||
`/admin/invites/accept?token=${inviteToken}`,
|
||||
{
|
||||
// Register identity
|
||||
const signup = await api.post("/auth/user/emailpass/register", {
|
||||
email: "newadmin@medusa.js",
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${signup.data.token}`,
|
||||
password: "secret_password",
|
||||
})
|
||||
|
||||
expect(signup.status).toEqual(200)
|
||||
expect(signup.data).toEqual({ token: expect.any(String) })
|
||||
|
||||
// Accept invite
|
||||
const response = await api.post(
|
||||
`/admin/invites/accept?token=${inviteToken}`,
|
||||
{
|
||||
email: "newadmin@medusa.js",
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
},
|
||||
}
|
||||
)
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${signup.data.token}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data).toEqual({
|
||||
user: expect.objectContaining({
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data).toEqual({
|
||||
user: expect.objectContaining({
|
||||
email: "newadmin@medusa.js",
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
}),
|
||||
})
|
||||
|
||||
// Sign in
|
||||
const login = await api.post("/auth/user/emailpass", {
|
||||
email: "newadmin@medusa.js",
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
}),
|
||||
password: "secret_password",
|
||||
})
|
||||
expect(login.status).toEqual(200)
|
||||
expect(login.data).toEqual({ token: expect.any(String) })
|
||||
|
||||
// Convert token to session
|
||||
const createSession = await api.post(
|
||||
"/auth/session",
|
||||
{},
|
||||
{ headers: { authorization: `Bearer ${login.data.token}` } }
|
||||
)
|
||||
expect(createSession.status).toEqual(200)
|
||||
|
||||
// Extract cookie
|
||||
const [cookie] = createSession.headers["set-cookie"][0].split(";")
|
||||
expect(cookie).toEqual(expect.stringContaining("connect.sid"))
|
||||
|
||||
const cookieHeader = {
|
||||
headers: { Cookie: cookie },
|
||||
}
|
||||
|
||||
// Perform cookie authenticated request
|
||||
const authedRequest = await api.get(
|
||||
"/admin/products?limit=1",
|
||||
cookieHeader
|
||||
)
|
||||
expect(authedRequest.status).toEqual(200)
|
||||
|
||||
// Sign out
|
||||
const signOutRequest = await api.delete("/auth/session", cookieHeader)
|
||||
expect(signOutRequest.status).toEqual(200)
|
||||
|
||||
// Attempt to perform authenticated request
|
||||
const unAuthedRequest = await api
|
||||
.get("/admin/products?limit=1", cookieHeader)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(unAuthedRequest.response.status).toEqual(401)
|
||||
})
|
||||
|
||||
// Sign in
|
||||
const login = await api.post("/auth/user/emailpass", {
|
||||
email: "newadmin@medusa.js",
|
||||
password: "secret_password",
|
||||
it("should respond with 401 on register, if email already exists", async () => {
|
||||
const signup = await api
|
||||
.post("/auth/user/emailpass/register", {
|
||||
email: "admin@medusa.js",
|
||||
password: "secret_password",
|
||||
})
|
||||
.catch((e) => e)
|
||||
|
||||
expect(signup.response.status).toEqual(401)
|
||||
expect(signup.response.data.message).toEqual(
|
||||
"Identity with email already exists"
|
||||
)
|
||||
})
|
||||
expect(login.status).toEqual(200)
|
||||
expect(login.data).toEqual({ token: expect.any(String) })
|
||||
|
||||
// Convert token to session
|
||||
const createSession = await api.post(
|
||||
"/auth/session",
|
||||
{},
|
||||
{ headers: { authorization: `Bearer ${login.data.token}` } }
|
||||
)
|
||||
expect(createSession.status).toEqual(200)
|
||||
it("should respond with 401 on sign in, if email does not exist", async () => {
|
||||
const signup = await api
|
||||
.post("/auth/user/emailpass", {
|
||||
email: "john@doe.com",
|
||||
password: "secret_password",
|
||||
})
|
||||
.catch((e) => e)
|
||||
|
||||
// Extract cookie
|
||||
const [cookie] = createSession.headers["set-cookie"][0].split(";")
|
||||
expect(cookie).toEqual(expect.stringContaining("connect.sid"))
|
||||
|
||||
const cookieHeader = {
|
||||
headers: { Cookie: cookie },
|
||||
}
|
||||
|
||||
// Perform cookie authenticated request
|
||||
const authedRequest = await api.get(
|
||||
"/admin/products?limit=1",
|
||||
cookieHeader
|
||||
)
|
||||
expect(authedRequest.status).toEqual(200)
|
||||
|
||||
// Sign out
|
||||
const signOutRequest = await api.delete("/auth/session", cookieHeader)
|
||||
expect(signOutRequest.status).toEqual(200)
|
||||
|
||||
// Attempt to perform authenticated request
|
||||
const unAuthedRequest = await api
|
||||
.get("/admin/products?limit=1", cookieHeader)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(unAuthedRequest.response.status).toEqual(401)
|
||||
expect(signup.response.status).toEqual(401)
|
||||
expect(signup.response.data.message).toEqual(
|
||||
"Invalid email or password"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it("should respond with 401 on register, if email already exists", async () => {
|
||||
const signup = await api
|
||||
.post("/auth/user/emailpass/register", {
|
||||
describe("Reset password flows", () => {
|
||||
it("should generate a reset password token", async () => {
|
||||
const response = await api.post("/auth/user/emailpass/reset-password", {
|
||||
email: "admin@medusa.js",
|
||||
})
|
||||
|
||||
expect(response.status).toEqual(201)
|
||||
})
|
||||
|
||||
it("should fail to generate token for non-existing user, but still respond with 201", async () => {
|
||||
const response = await api.post("/auth/user/emailpass/reset-password", {
|
||||
email: "non-existing-user@medusa.js",
|
||||
})
|
||||
|
||||
expect(response.status).toEqual(201)
|
||||
})
|
||||
|
||||
it("should fail to generate token for existing user but no provider, but still respond with 201", async () => {
|
||||
const response = await api.post(
|
||||
"/auth/user/non-existing-provider/reset-password",
|
||||
{ email: "admin@medusa.js" }
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(201)
|
||||
})
|
||||
|
||||
it("should fail to generate token for existing user but no provider, but still respond with 201", async () => {
|
||||
const response = await api.post(
|
||||
"/auth/user/non-existing-provider/reset-password",
|
||||
{ email: "admin@medusa.js" }
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(201)
|
||||
})
|
||||
|
||||
it("should successfully reset password", async () => {
|
||||
// Register user
|
||||
await api.post("/auth/user/emailpass/register", {
|
||||
email: "test@medusa-commerce.com",
|
||||
password: "secret_password",
|
||||
})
|
||||
.catch((e) => e)
|
||||
|
||||
expect(signup.response.status).toEqual(401)
|
||||
expect(signup.response.data.message).toEqual(
|
||||
"Identity with email already exists"
|
||||
)
|
||||
})
|
||||
// The token won't be part of the Rest API response, so we need to generate it manually
|
||||
const { result } = await generateResetPasswordTokenWorkflow(
|
||||
container
|
||||
).run({
|
||||
input: {
|
||||
entityId: "test@medusa-commerce.com",
|
||||
provider: "emailpass",
|
||||
secret: "test",
|
||||
},
|
||||
})
|
||||
|
||||
it("should respond with 401 on sign in, if email does not exist", async () => {
|
||||
const signup = await api
|
||||
.post("/auth/user/emailpass", {
|
||||
email: "john@doe.com",
|
||||
const response = await api.post(
|
||||
`/auth/user/emailpass/update?token=${result}`,
|
||||
{
|
||||
email: "test@medusa-commerce.com",
|
||||
password: "new_password",
|
||||
}
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data).toEqual({ success: true })
|
||||
|
||||
const failedLogin = await api
|
||||
.post("/auth/user/emailpass", {
|
||||
email: "test@medusa-commerce.com",
|
||||
password: "secret_password",
|
||||
})
|
||||
.catch((e) => e)
|
||||
|
||||
expect(failedLogin.response.status).toEqual(401)
|
||||
expect(failedLogin.response.data.message).toEqual(
|
||||
"Invalid email or password"
|
||||
)
|
||||
|
||||
const login = await api.post("/auth/user/emailpass", {
|
||||
email: "test@medusa-commerce.com",
|
||||
password: "new_password",
|
||||
})
|
||||
|
||||
expect(login.status).toEqual(200)
|
||||
expect(login.data).toEqual({ token: expect.any(String) })
|
||||
})
|
||||
|
||||
it("should fail if token has expired", async () => {
|
||||
jest.useFakeTimers()
|
||||
|
||||
// Register user
|
||||
await api.post("/auth/user/emailpass/register", {
|
||||
email: "test@medusa-commerce.com",
|
||||
password: "secret_password",
|
||||
})
|
||||
.catch((e) => e)
|
||||
|
||||
expect(signup.response.status).toEqual(401)
|
||||
expect(signup.response.data.message).toEqual("Invalid email or password")
|
||||
// The token won't be part of the Rest API response, so we need to generate it manually
|
||||
const { result } = await generateResetPasswordTokenWorkflow(
|
||||
container
|
||||
).run({
|
||||
input: {
|
||||
entityId: "test@medusa-commerce.com",
|
||||
provider: "emailpass",
|
||||
secret: "test",
|
||||
},
|
||||
})
|
||||
|
||||
// Advance time by 15 minutes
|
||||
jest.advanceTimersByTime(15 * 60 * 1000)
|
||||
|
||||
const response = await api
|
||||
.post(`/auth/user/emailpass/update?token=${result}`, {
|
||||
email: "test@medusa-commerce.com",
|
||||
password: "new_password",
|
||||
})
|
||||
.catch((e) => e)
|
||||
|
||||
expect(response.response.status).toEqual(401)
|
||||
expect(response.response.data.message).toEqual("Invalid token")
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export * from "./steps"
|
||||
export * from "./workflows"
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export const setAuthAppMetadataStep = createStep(
|
||||
|
||||
appMetadata[key] = data.value
|
||||
|
||||
await service.updateAuthIdentites({
|
||||
await service.updateAuthIdentities({
|
||||
id: authIdentity.id,
|
||||
app_metadata: appMetadata,
|
||||
})
|
||||
@@ -58,7 +58,7 @@ export const setAuthAppMetadataStep = createStep(
|
||||
delete appMetadata[key]
|
||||
}
|
||||
|
||||
await service.updateAuthIdentites({
|
||||
await service.updateAuthIdentities({
|
||||
id: authIdentity.id,
|
||||
app_metadata: appMetadata,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
AuthWorkflowEvents,
|
||||
generateJwtToken,
|
||||
MedusaError,
|
||||
} from "@medusajs/utils";
|
||||
import {
|
||||
createWorkflow,
|
||||
transform,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/workflows-sdk";
|
||||
import { emitEventStep, useRemoteQueryStep } from "../../common";
|
||||
|
||||
export const generateResetPasswordTokenWorkflow = createWorkflow(
|
||||
"generate-reset-password-token",
|
||||
(input: { entityId: string; provider: string; secret: string }) => {
|
||||
const providerIdentities = useRemoteQueryStep({
|
||||
entry_point: "provider_identity",
|
||||
fields: ["auth_identity_id", "provider_metadata"],
|
||||
variables: {
|
||||
filters: {
|
||||
entity_id: input.entityId,
|
||||
provider: input.provider,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const token = transform(
|
||||
{ input, providerIdentities },
|
||||
({ input, providerIdentities }) => {
|
||||
const providerIdentity = providerIdentities?.[0]
|
||||
|
||||
if (!providerIdentity) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Provider identity with entity_id ${input.entityId} and provider ${input.provider} not found`
|
||||
)
|
||||
}
|
||||
|
||||
const token = generateJwtToken(
|
||||
{
|
||||
entity_id: input.entityId,
|
||||
provider: input.provider,
|
||||
},
|
||||
{
|
||||
secret: input.secret,
|
||||
expiresIn: "15m",
|
||||
}
|
||||
)
|
||||
|
||||
return token
|
||||
}
|
||||
)
|
||||
|
||||
emitEventStep({
|
||||
eventName: AuthWorkflowEvents.PASSWORD_RESET,
|
||||
data: { entity_id: input.entityId, token },
|
||||
})
|
||||
|
||||
return new WorkflowResponse(token)
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./generate-reset-password-token"
|
||||
@@ -83,6 +83,11 @@ export type ProviderIdentityDTO = {
|
||||
*/
|
||||
entity_id: string
|
||||
|
||||
/**
|
||||
* The ID of the auth identity linked to the provider identity.
|
||||
*/
|
||||
auth_identity_id?: string
|
||||
|
||||
/**
|
||||
* The auth identity linked to the provider identity.
|
||||
*/
|
||||
@@ -205,6 +210,11 @@ export interface FilterableProviderIdentityProps
|
||||
*/
|
||||
entity_id?: string
|
||||
|
||||
/**
|
||||
* Filter the provider identities by the ID of the auth identity they are linked to.
|
||||
*/
|
||||
auth_identity_id?: string
|
||||
|
||||
/**
|
||||
* The provider handle to filter the provider identity by.
|
||||
*/
|
||||
|
||||
@@ -41,4 +41,8 @@ export interface IAuthProvider {
|
||||
data: AuthenticationInput,
|
||||
authIdentityProviderService: AuthIdentityProviderService
|
||||
): Promise<AuthenticationResponse>
|
||||
update: (
|
||||
data: Record<string, unknown>,
|
||||
authIdentityProviderService: AuthIdentityProviderService
|
||||
) => Promise<AuthenticationResponse>
|
||||
}
|
||||
|
||||
@@ -54,6 +54,11 @@ export interface IAuthModuleService extends IModuleService {
|
||||
providerData: AuthenticationInput
|
||||
): Promise<AuthenticationResponse>
|
||||
|
||||
updateProvider(
|
||||
provider: string,
|
||||
providerData: Record<string, unknown>
|
||||
): Promise<AuthenticationResponse>
|
||||
|
||||
/**
|
||||
* When authenticating users with a third-party provider, such as Google, the user performs an
|
||||
* action to finish the authentication, such as enter their credentials in Google's sign-in
|
||||
@@ -244,13 +249,13 @@ export interface IAuthModuleService extends IModuleService {
|
||||
* @returns {Promise<AuthIdentityDTO[]>} The updated auths.
|
||||
*
|
||||
* @example
|
||||
* const authIdentities = await authModuleService.updateAuthIdentites([
|
||||
* const authIdentities = await authModuleService.updateAuthIdentities([
|
||||
* {
|
||||
* id: "authusr_123",
|
||||
* },
|
||||
* ])
|
||||
*/
|
||||
updateAuthIdentites(
|
||||
updateAuthIdentities(
|
||||
data: UpdateAuthIdentityDTO[],
|
||||
sharedContext?: Context
|
||||
): Promise<AuthIdentityDTO[]>
|
||||
@@ -263,11 +268,11 @@ export interface IAuthModuleService extends IModuleService {
|
||||
* @returns {Promise<AuthIdentityDTO>} The updated auth.
|
||||
*
|
||||
* @example
|
||||
* const authIdentity = await authModuleService.updateAuthIdentites({
|
||||
* const authIdentity = await authModuleService.updateAuthIdentities({
|
||||
* id: "authusr_123",
|
||||
* })
|
||||
*/
|
||||
updateAuthIdentites(
|
||||
updateAuthIdentities(
|
||||
data: UpdateAuthIdentityDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<AuthIdentityDTO>
|
||||
@@ -399,7 +404,7 @@ export interface IAuthModuleService extends IModuleService {
|
||||
* },
|
||||
* ])
|
||||
*/
|
||||
updateProviderIdentites(
|
||||
updateProviderIdentities(
|
||||
data: UpdateProviderIdentityDTO[],
|
||||
sharedContext?: Context
|
||||
): Promise<ProviderIdentityDTO[]>
|
||||
@@ -412,11 +417,11 @@ export interface IAuthModuleService extends IModuleService {
|
||||
* @returns {Promise<ProviderIdentityDTO>} The updated provider identity.
|
||||
*
|
||||
* @example
|
||||
* const providerIdentity = await authModuleService.updateProviderIdentites({
|
||||
* const providerIdentity = await authModuleService.updateProviderIdentities({
|
||||
* id: "provider_123",
|
||||
* })
|
||||
*/
|
||||
updateProviderIdentites(
|
||||
updateProviderIdentities(
|
||||
data: UpdateProviderIdentityDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<ProviderIdentityDTO>
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
AuthenticationInput,
|
||||
AuthenticationResponse,
|
||||
AuthIdentityProviderService,
|
||||
IAuthProvider,
|
||||
IAuthProvider
|
||||
} from "@medusajs/types"
|
||||
|
||||
/**
|
||||
@@ -238,6 +238,15 @@ export abstract class AbstractAuthModuleProvider implements IAuthProvider {
|
||||
)
|
||||
}
|
||||
|
||||
update(
|
||||
data: Record<string, unknown>,
|
||||
authIdentityProviderService: AuthIdentityProviderService
|
||||
): Promise<AuthenticationResponse> {
|
||||
throw new Error(
|
||||
`Method 'update' not implemented for provider ${this.provider}`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method validates the callback of an authentication request.
|
||||
*
|
||||
|
||||
@@ -10,6 +10,16 @@ export const OrderWorkflowEvents = {
|
||||
COMPLETED: "order.completed",
|
||||
}
|
||||
|
||||
export const UserWorkflowEvents = {
|
||||
CREATED: "user.created",
|
||||
UPDATED: "user.updated",
|
||||
DELETED: "user.deleted",
|
||||
}
|
||||
|
||||
export const AuthWorkflowEvents = {
|
||||
PASSWORD_RESET: "auth.password_reset",
|
||||
}
|
||||
|
||||
export const SalesChannelWorkflowEvents = {
|
||||
CREATED: "sales-channel.created",
|
||||
UPDATED: "sales-channel.updated",
|
||||
|
||||
@@ -7,9 +7,3 @@ export const UserEvents = {
|
||||
...buildEventNamesFromEntityName(eventBaseNames, Modules.USER),
|
||||
INVITE_TOKEN_GENERATED: `${Modules.USER}.user.invite.token_generated`,
|
||||
}
|
||||
|
||||
export const UserWorkflowEvents = {
|
||||
CREATED: "user.created",
|
||||
UPDATED: "user.updated",
|
||||
DELETED: "user.deleted",
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { MiddlewareRoute } from "@medusajs/framework"
|
||||
import { authenticate } from "../../../utils/middlewares/authenticate-middleware"
|
||||
import { validateAndTransformBody } from "../../utils/validate-body"
|
||||
import { validateAndTransformQuery } from "../../utils/validate-query"
|
||||
import * as QueryConfig from "./query-config"
|
||||
|
||||
import {
|
||||
AdminGetUserParams,
|
||||
AdminGetUsersParams,
|
||||
AdminUpdateUser,
|
||||
AdminUpdateUser
|
||||
} from "./validators"
|
||||
|
||||
import { MiddlewareRoute } from "@medusajs/framework"
|
||||
import { authenticate } from "../../../utils/middlewares/authenticate-middleware"
|
||||
import { validateAndTransformQuery } from "../../utils/validate-query"
|
||||
import { validateAndTransformBody } from "../../utils/validate-body"
|
||||
|
||||
// TODO: Due to issues with our routing (and using router.use for applying middlewares), we have to opt-out of global auth in all routes, and then reapply it here.
|
||||
// See https://medusacorp.slack.com/archives/C025KMS13SA/p1716455350491879 for details.
|
||||
export const adminUserRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
@@ -31,8 +29,8 @@ export const adminUserRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
middlewares: [
|
||||
authenticate("user", ["bearer", "session"]),
|
||||
validateAndTransformQuery(
|
||||
AdminGetUserParams,
|
||||
QueryConfig.retrieveTransformQueryConfig
|
||||
AdminGetUserParams,
|
||||
QueryConfig.retrieveTransformQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
@@ -42,8 +40,8 @@ export const adminUserRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
middlewares: [
|
||||
authenticate("user", ["bearer", "session"]),
|
||||
validateAndTransformQuery(
|
||||
AdminGetUserParams,
|
||||
QueryConfig.retrieveTransformQueryConfig
|
||||
AdminGetUserParams,
|
||||
QueryConfig.retrieveTransformQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
@@ -54,8 +52,8 @@ export const adminUserRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
authenticate("user", ["bearer", "session"]),
|
||||
validateAndTransformBody(AdminUpdateUser),
|
||||
validateAndTransformQuery(
|
||||
AdminGetUserParams,
|
||||
QueryConfig.retrieveTransformQueryConfig
|
||||
AdminGetUserParams,
|
||||
QueryConfig.retrieveTransformQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -38,3 +38,4 @@ export const AdminUpdateUser = z.object({
|
||||
last_name: z.string().nullish(),
|
||||
avatar_url: z.string().nullish(),
|
||||
})
|
||||
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
AuthenticationInput,
|
||||
ConfigModule,
|
||||
IAuthModuleService,
|
||||
} from "@medusajs/types"
|
||||
import { AuthenticationInput, IAuthModuleService } from "@medusajs/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
MedusaError,
|
||||
@@ -13,21 +9,6 @@ import { MedusaRequest, MedusaResponse } from "../../../../../types/routing"
|
||||
|
||||
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
const { actor_type, auth_provider } = req.params
|
||||
const config: ConfigModule = req.scope.resolve(
|
||||
ContainerRegistrationKeys.CONFIG_MODULE
|
||||
)
|
||||
|
||||
const authMethodsPerActor =
|
||||
config.projectConfig?.http?.authMethodsPerActor ?? {}
|
||||
// Not having the config defined would allow for all auth providers for the particular actor.
|
||||
if (authMethodsPerActor[actor_type]) {
|
||||
if (!authMethodsPerActor[actor_type].includes(auth_provider)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`The actor type ${actor_type} is not allowed to use the auth provider ${auth_provider}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const service: IAuthModuleService = req.scope.resolve(
|
||||
ModuleRegistrationName.AUTH
|
||||
|
||||
@@ -17,18 +17,6 @@ export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
ContainerRegistrationKeys.CONFIG_MODULE
|
||||
)
|
||||
|
||||
const authMethodsPerActor =
|
||||
config.projectConfig?.http?.authMethodsPerActor ?? {}
|
||||
// Not having the config defined would allow for all auth providers for the particular actor.
|
||||
if (authMethodsPerActor[actor_type]) {
|
||||
if (!authMethodsPerActor[actor_type].includes(auth_provider)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`The actor type ${actor_type} is not allowed to use the auth provider ${auth_provider}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const service: IAuthModuleService = req.scope.resolve(
|
||||
ModuleRegistrationName.AUTH
|
||||
)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { generateResetPasswordTokenWorkflow } from "@medusajs/core-flows"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "../../../../../types/routing"
|
||||
|
||||
export const POST = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const { auth_provider } = req.params
|
||||
const { identifier } = req.body
|
||||
|
||||
const { http } = req.scope.resolve(
|
||||
ContainerRegistrationKeys.CONFIG_MODULE
|
||||
).projectConfig
|
||||
|
||||
await generateResetPasswordTokenWorkflow(req.scope).run({
|
||||
input: {
|
||||
entityId: identifier,
|
||||
provider: auth_provider,
|
||||
secret: http.jwtSecret as string,
|
||||
},
|
||||
throwOnError: false, // we don't want to throw on error to avoid leaking information about non-existing identities
|
||||
})
|
||||
|
||||
res.sendStatus(201)
|
||||
}
|
||||
|
||||
export const AUTHENTICATE = false
|
||||
@@ -17,18 +17,6 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
ContainerRegistrationKeys.CONFIG_MODULE
|
||||
)
|
||||
|
||||
const authMethodsPerActor =
|
||||
config.projectConfig?.http?.authMethodsPerActor ?? {}
|
||||
// Not having the config defined would allow for all auth providers for the particular actor.
|
||||
if (authMethodsPerActor[actor_type]) {
|
||||
if (!authMethodsPerActor[actor_type].includes(auth_provider)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`The actor type ${actor_type} is not allowed to use the auth provider ${auth_provider}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const service: IAuthModuleService = req.scope.resolve(
|
||||
ModuleRegistrationName.AUTH
|
||||
)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { AuthenticatedMedusaRequest } from "@medusajs/framework"
|
||||
import { IAuthModuleService } from "@medusajs/types"
|
||||
import { MedusaError, ModuleRegistrationName } from "@medusajs/utils"
|
||||
import { MedusaResponse } from "../../../../../types/routing"
|
||||
|
||||
export const POST = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const { auth_provider } = req.params
|
||||
|
||||
const authService = req.scope.resolve<IAuthModuleService>(
|
||||
ModuleRegistrationName.AUTH
|
||||
)
|
||||
|
||||
const { authIdentity, success, error } = await authService.updateProvider(
|
||||
auth_provider,
|
||||
req.body as Record<string, unknown>
|
||||
)
|
||||
|
||||
if (success && authIdentity) {
|
||||
return res.status(200).json({ success: true })
|
||||
}
|
||||
|
||||
throw new MedusaError(MedusaError.Types.UNAUTHORIZED, error || "Unauthorized")
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { MiddlewareRoute } from "@medusajs/framework"
|
||||
import { authenticate } from "../../utils/middlewares/authenticate-middleware"
|
||||
import { validateScopeProviderAssociation } from "./utils/validate-scope-provider-association"
|
||||
import { validateToken } from "./utils/validate-token"
|
||||
|
||||
export const authRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
@@ -15,21 +17,31 @@ export const authRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/auth/:actor_type/:auth_provider/callback",
|
||||
middlewares: [],
|
||||
middlewares: [validateScopeProviderAssociation()],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/auth/:actor_type/:auth_provider/register",
|
||||
middlewares: [],
|
||||
middlewares: [validateScopeProviderAssociation()],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/auth/:actor_type/:auth_provider",
|
||||
middlewares: [],
|
||||
middlewares: [validateScopeProviderAssociation()],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
matcher: "/auth/:actor_type/:auth_provider",
|
||||
middlewares: [],
|
||||
middlewares: [validateScopeProviderAssociation()],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/auth/:actor_type/:auth_provider/reset-password",
|
||||
middlewares: [validateScopeProviderAssociation()],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/auth/:actor_type/:auth_provider/update",
|
||||
middlewares: [validateScopeProviderAssociation(), validateToken()],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
MedusaNextFunction,
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework"
|
||||
import { ConfigModule } from "@medusajs/types"
|
||||
import { ContainerRegistrationKeys, MedusaError } from "@medusajs/utils"
|
||||
|
||||
// Middleware to validate that a scope is associated with a provider
|
||||
export const validateScopeProviderAssociation = () => {
|
||||
return async (
|
||||
req: MedusaRequest,
|
||||
_: MedusaResponse,
|
||||
next: MedusaNextFunction
|
||||
) => {
|
||||
const { actor_type, auth_provider } = req.params
|
||||
const config: ConfigModule = req.scope.resolve(
|
||||
ContainerRegistrationKeys.CONFIG_MODULE
|
||||
)
|
||||
|
||||
const authMethodsPerActor =
|
||||
config.projectConfig?.http?.authMethodsPerActor ?? {}
|
||||
// Not having the config defined would allow for all auth providers for the particular actor.
|
||||
if (authMethodsPerActor[actor_type]) {
|
||||
if (!authMethodsPerActor[actor_type].includes(auth_provider)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`The actor type ${actor_type} is not allowed to use the auth provider ${auth_provider}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaNextFunction,
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework"
|
||||
import { ConfigModule, IAuthModuleService } from "@medusajs/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
ModuleRegistrationName,
|
||||
} from "@medusajs/utils"
|
||||
import { decode, JwtPayload, verify } from "jsonwebtoken"
|
||||
|
||||
// Middleware to validate that a token is valid
|
||||
export const validateToken = () => {
|
||||
return async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse,
|
||||
next: MedusaNextFunction
|
||||
) => {
|
||||
const { actor_type, auth_provider } = req.params
|
||||
const { token } = req.query
|
||||
|
||||
const req_ = req as AuthenticatedMedusaRequest
|
||||
|
||||
if (!token) {
|
||||
return next()
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const { http } = req_.scope.resolve<ConfigModule>(
|
||||
ContainerRegistrationKeys.CONFIG_MODULE
|
||||
).projectConfig
|
||||
|
||||
const authModule = req.scope.resolve<IAuthModuleService>(
|
||||
ModuleRegistrationName.AUTH
|
||||
)
|
||||
|
||||
let decoded = decode(token as string) as JwtPayload
|
||||
|
||||
const [providerIdentity] = await authModule.listProviderIdentities(
|
||||
{
|
||||
entity_id: decoded.entity_id,
|
||||
provider: auth_provider,
|
||||
},
|
||||
{
|
||||
select: ["provider_metadata", "auth_identity_id", "entity_id"],
|
||||
}
|
||||
)
|
||||
|
||||
if (!providerIdentity) {
|
||||
return res.status(401).json({ message: "Invalid token" })
|
||||
}
|
||||
|
||||
let verified: JwtPayload | null = null
|
||||
|
||||
try {
|
||||
verified = verify(token as string, http.jwtSecret as string) as JwtPayload
|
||||
} catch (error) {
|
||||
return res.status(401).json({ message: "Invalid token" })
|
||||
}
|
||||
|
||||
req_.auth_context = {
|
||||
actor_type,
|
||||
auth_identity_id: verified.auth_identity_id!,
|
||||
actor_id: providerIdentity.entity_id,
|
||||
app_metadata: {},
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MiddlewareFunction } from "@medusajs/framework"
|
||||
import { NextFunction } from "express"
|
||||
import { MedusaRequest, MedusaResponse } from "../../types/routing"
|
||||
import { MiddlewareFunction } from "@medusajs/framework"
|
||||
|
||||
// Due to how our route loader works, where we load all middlewares before routes, ambiguous routes end up having all middlewares on different routes executed before the route handler is.
|
||||
// This function allows us to skip middlewares for particular routes, so we can temporarily solve this without completely breaking the route loader for everyone.
|
||||
|
||||
@@ -54,7 +54,7 @@ export default async function ({
|
||||
}
|
||||
|
||||
// We know the authIdentity is not undefined
|
||||
await authService.updateAuthIdentites({
|
||||
await authService.updateAuthIdentities({
|
||||
id: authIdentity!.id,
|
||||
app_metadata: {
|
||||
user_id: user.id,
|
||||
|
||||
+6
-6
@@ -215,7 +215,7 @@ moduleIntegrationTestRunner<IAuthModuleService>({
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.updateAuthIdentites([
|
||||
await service.updateAuthIdentities([
|
||||
{
|
||||
id: "does-not-exist",
|
||||
},
|
||||
@@ -230,7 +230,7 @@ moduleIntegrationTestRunner<IAuthModuleService>({
|
||||
})
|
||||
|
||||
it("should update authIdentity", async () => {
|
||||
await service.updateAuthIdentites([
|
||||
await service.updateAuthIdentities([
|
||||
{
|
||||
id,
|
||||
app_metadata: { email: "test@email.com" },
|
||||
@@ -364,7 +364,7 @@ moduleIntegrationTestRunner<IAuthModuleService>({
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.updateProviderIdentites([
|
||||
await service.updateProviderIdentities([
|
||||
{
|
||||
id: "does-not-exist",
|
||||
},
|
||||
@@ -382,18 +382,18 @@ moduleIntegrationTestRunner<IAuthModuleService>({
|
||||
let [providerIdentity] = await service.listProviderIdentities({
|
||||
entity_id,
|
||||
})
|
||||
await service.updateProviderIdentites([
|
||||
await service.updateProviderIdentities([
|
||||
{
|
||||
id: providerIdentity.id,
|
||||
provider_metadata: { email: "test@email.com" },
|
||||
},
|
||||
])
|
||||
|
||||
const providerIdentites = await service.listProviderIdentities({
|
||||
const providerIdentities = await service.listProviderIdentities({
|
||||
id: [providerIdentity.id],
|
||||
})
|
||||
|
||||
expect(providerIdentites[0]).toEqual(
|
||||
expect(providerIdentities[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
provider_metadata: expect.objectContaining({
|
||||
email: "test@email.com",
|
||||
|
||||
+17
-3
@@ -1,8 +1,8 @@
|
||||
import { IAuthModuleService } from "@medusajs/types"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
import { resolve } from "path"
|
||||
import { Module, Modules } from "@medusajs/utils"
|
||||
import { AuthModuleService } from "@services"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
import { resolve } from "path"
|
||||
|
||||
let moduleOptions = {
|
||||
providers: [
|
||||
@@ -42,7 +42,10 @@ moduleIntegrationTestRunner({
|
||||
service: AuthModuleService,
|
||||
}).linkable
|
||||
|
||||
expect(Object.keys(linkable)).toEqual(["authIdentity"])
|
||||
expect(Object.keys(linkable)).toEqual([
|
||||
"authIdentity",
|
||||
"providerIdentity",
|
||||
])
|
||||
|
||||
linkable.authIdentity.toJSON = undefined
|
||||
|
||||
@@ -54,6 +57,17 @@ moduleIntegrationTestRunner({
|
||||
field: "authIdentity",
|
||||
},
|
||||
})
|
||||
|
||||
linkable.providerIdentity.toJSON = undefined
|
||||
|
||||
expect(linkable.providerIdentity).toEqual({
|
||||
id: {
|
||||
linkable: "provider_identity_id",
|
||||
primaryKey: "id",
|
||||
serviceName: "auth",
|
||||
field: "providerIdentity",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("it fails if the provider does not exist", async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AuthIdentity } from "@models"
|
||||
import { defineJoinerConfig, Modules } from "@medusajs/utils"
|
||||
import { AuthIdentity, ProviderIdentity } from "@models"
|
||||
|
||||
export const joinerConfig = defineJoinerConfig(Modules.AUTH, {
|
||||
models: [AuthIdentity],
|
||||
models: [AuthIdentity, ProviderIdentity],
|
||||
})
|
||||
|
||||
@@ -9,17 +9,14 @@ import {
|
||||
ModuleJoinerConfig,
|
||||
ModulesSdkTypes,
|
||||
} from "@medusajs/types"
|
||||
|
||||
import { AuthIdentity, ProviderIdentity } from "@models"
|
||||
|
||||
import { joinerConfig } from "../joiner-config"
|
||||
|
||||
import {
|
||||
InjectManager,
|
||||
MedusaContext,
|
||||
MedusaError,
|
||||
MedusaService,
|
||||
} from "@medusajs/utils"
|
||||
import { AuthIdentity, ProviderIdentity } from "@models"
|
||||
import { joinerConfig } from "../joiner-config"
|
||||
import AuthProviderService from "./auth-provider"
|
||||
|
||||
type InjectedDependencies = {
|
||||
@@ -92,18 +89,19 @@ export default class AuthModuleService
|
||||
}
|
||||
|
||||
// TODO: Update to follow convention
|
||||
updateAuthIdentites(
|
||||
// @ts-expect-error
|
||||
updateAuthIdentities(
|
||||
data: AuthTypes.UpdateAuthIdentityDTO[],
|
||||
sharedContext?: Context
|
||||
): Promise<AuthTypes.AuthIdentityDTO[]>
|
||||
|
||||
updateAuthIdentites(
|
||||
updateAuthIdentities(
|
||||
data: AuthTypes.UpdateAuthIdentityDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<AuthTypes.AuthIdentityDTO>
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async updateAuthIdentites(
|
||||
async updateAuthIdentities(
|
||||
data: AuthTypes.UpdateAuthIdentityDTO | AuthTypes.UpdateAuthIdentityDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<AuthTypes.AuthIdentityDTO | AuthTypes.AuthIdentityDTO[]> {
|
||||
@@ -135,6 +133,7 @@ export default class AuthModuleService
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
createProviderIdentities(
|
||||
data: AuthTypes.CreateProviderIdentityDTO[],
|
||||
@@ -163,18 +162,19 @@ export default class AuthModuleService
|
||||
>(providerIdentities)
|
||||
}
|
||||
|
||||
updateProviderIdentites(
|
||||
// @ts-expect-error
|
||||
updateProviderIdentities(
|
||||
data: AuthTypes.UpdateProviderIdentityDTO[],
|
||||
sharedContext?: Context
|
||||
): Promise<AuthTypes.ProviderIdentityDTO[]>
|
||||
|
||||
updateProviderIdentites(
|
||||
updateProviderIdentities(
|
||||
data: AuthTypes.UpdateProviderIdentityDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<AuthTypes.ProviderIdentityDTO>
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async updateProviderIdentites(
|
||||
async updateProviderIdentities(
|
||||
data:
|
||||
| AuthTypes.UpdateProviderIdentityDTO
|
||||
| AuthTypes.UpdateProviderIdentityDTO[],
|
||||
@@ -192,6 +192,21 @@ export default class AuthModuleService
|
||||
return Array.isArray(data) ? serializedProviders : serializedProviders[0]
|
||||
}
|
||||
|
||||
async updateProvider(
|
||||
provider: string,
|
||||
data: Record<string, unknown>
|
||||
): Promise<AuthenticationResponse> {
|
||||
try {
|
||||
return await this.authProviderService_.update(
|
||||
provider,
|
||||
data,
|
||||
this.getAuthIdentityProviderService(provider)
|
||||
)
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
}
|
||||
|
||||
async authenticate(
|
||||
provider: string,
|
||||
authenticationData: AuthenticationInput
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
AuthIdentityProviderService,
|
||||
AuthTypes,
|
||||
AuthenticationInput,
|
||||
AuthenticationResponse,
|
||||
AuthenticationResponse
|
||||
} from "@medusajs/types"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import { AuthProviderRegistrationPrefix } from "@types"
|
||||
@@ -51,6 +51,15 @@ export default class AuthProviderService {
|
||||
return await providerHandler.register(auth, authIdentityProviderService)
|
||||
}
|
||||
|
||||
async update(
|
||||
provider: string,
|
||||
data: Record<string, unknown>,
|
||||
authIdentityProviderService: AuthIdentityProviderService
|
||||
): Promise<AuthenticationResponse> {
|
||||
const providerHandler = this.retrieveProviderRegistration(provider)
|
||||
return await providerHandler.update(data, authIdentityProviderService)
|
||||
}
|
||||
|
||||
async validateCallback(
|
||||
provider: string,
|
||||
auth: AuthenticationInput,
|
||||
|
||||
@@ -35,14 +35,56 @@ export class EmailPassAuthService extends AbstractAuthModuleProvider {
|
||||
this.logger_ = logger
|
||||
}
|
||||
|
||||
protected async createAuthIdentity({ email, password, authIdentityService }) {
|
||||
protected async hashPassword(password: string) {
|
||||
const hashConfig = this.config_.hashConfig ?? { logN: 15, r: 8, p: 1 }
|
||||
const passwordHash = await Scrypt.kdf(password, hashConfig)
|
||||
return passwordHash.toString("base64")
|
||||
}
|
||||
|
||||
async update(
|
||||
data: { email: string; password: string },
|
||||
authIdentityService: AuthIdentityProviderService
|
||||
) {
|
||||
const { email, password } = data ?? {}
|
||||
|
||||
if (!email || !isString(email)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Cannot update ${this.provider} provider identity without email`,
|
||||
}
|
||||
}
|
||||
|
||||
if (!password || !isString(password)) {
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
let authIdentity
|
||||
|
||||
try {
|
||||
const passwordHash = await this.hashPassword(password)
|
||||
|
||||
authIdentity = await authIdentityService.update(email, {
|
||||
provider_metadata: {
|
||||
password: passwordHash,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
authIdentity,
|
||||
}
|
||||
}
|
||||
|
||||
protected async createAuthIdentity({ email, password, authIdentityService }) {
|
||||
const passwordHash = await this.hashPassword(password)
|
||||
|
||||
const createdAuthIdentity = await authIdentityService.create({
|
||||
entity_id: email,
|
||||
provider_metadata: {
|
||||
password: passwordHash.toString("base64"),
|
||||
password: passwordHash,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user