Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/admin/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"lodash.isequal": "^4.5.0",
"match-sorter": "^6.3.4",
"motion": "^11.15.0",
"qrcode": "^1.5.4",
"qs": "^6.12.1",
"radix-ui": "1.1.2",
"react": "^18.3.1",
Expand All @@ -85,6 +86,7 @@
"@medusajs/admin-vite-plugin": "2.15.4",
"@medusajs/types": "2.15.4",
"@medusajs/ui-preset": "2.15.4",
"@types/qrcode": "^1.5.5",
"@typescript/native-preview": "^7.0.0-dev.20260423.1",
"eslint": "8",
"typescript": "5.9.3"
Expand Down
26 changes: 7 additions & 19 deletions packages/admin/dashboard/src/hooks/api/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,22 @@ import { FetchError } from "@medusajs/js-sdk"
import { HttpTypes } from "@medusajs/types"
import { UseMutationOptions, useMutation } from "@tanstack/react-query"
import { sdk } from "../../lib/client"

const ensureAuthTokenOrRedirect = async (
authResponse: ReturnType<typeof sdk.auth.login>
) => {
const result = await authResponse

if (typeof result === "string" || "location" in result) {
return result
}

throw new Error(
"Multi-factor authentication is not supported in the dashboard"
)
}
import type { AuthLoginResponse } from "./mfa"

export const useSignInWithEmailPass = (
options?: UseMutationOptions<
| string
| {
location: string
},
AuthLoginResponse,
FetchError,
HttpTypes.AdminSignUpWithEmailPassword
>
) => {
return useMutation({
mutationFn: (payload) =>
ensureAuthTokenOrRedirect(sdk.auth.login("user", "emailpass", payload)),
sdk.auth.login(
"user",
"emailpass",
payload
) as Promise<AuthLoginResponse>,
onSuccess: async (data, variables, context) => {
options?.onSuccess?.(data, variables, context)
},
Expand Down
1 change: 1 addition & 0 deletions packages/admin/dashboard/src/hooks/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export * from "./fulfillment-sets"
export * from "./inventory"
export * from "./invites"
export * from "./locales"
export * from "./mfa"
export * from "./notification"
export * from "./orders"
export * from "./payment-collections"
Expand Down
142 changes: 142 additions & 0 deletions packages/admin/dashboard/src/hooks/api/mfa.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import type {
AuthMfaFactorResponse,
AuthMfaListResponse,
AuthMfaRecoveryCodesResponse,
AuthMfaSetupResponse,
FetchError,
} from "@medusajs/js-sdk"
import type { AuthTypes } from "@medusajs/types"
import {
QueryKey,
UseMutationOptions,
UseQueryOptions,
useMutation,
useQuery,
} from "@tanstack/react-query"
import { sdk } from "../../lib/client"
import { queryClient } from "../../lib/query-client"
import { queryKeysFactory } from "../../lib/query-key-factory"

const MFA_QUERY_KEY = "mfa" as const
export const mfaQueryKeys = queryKeysFactory(MFA_QUERY_KEY)

export type {
AuthLoginResponse,
AuthMfaRequiredResponse,
AuthMfaSetupResponse,
} from "@medusajs/js-sdk"

export type AuthMfa = AuthTypes.AuthMfaDTO
Comment thread
christiananese marked this conversation as resolved.
Outdated
export type AuthMfaChallenge = AuthTypes.AuthMfaChallengeDTO
export type AuthMfaChallengeMethod = AuthTypes.AuthMfaChallengeMethod
export type AuthMfaProvider = AuthTypes.AuthMfaProvider

export const callbackWithCloudAuth = async (
query: Record<string, unknown>
): ReturnType<typeof sdk.auth.callback> =>
sdk.auth.callback("user", "cloud", query)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this works just fine when doing auth with Cloud? How does it work, does it require MFA after the oauth or it's skipped entirely?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cloud OAuth can return an MFA challenge after the OAuth callback. Admin then shows the same MFA challenge screen and only continues after challenge verification. So admin MFA is not skipped


export const useAuthMfa = (
options?: Omit<
UseQueryOptions<
AuthMfaListResponse,
FetchError,
AuthMfaListResponse,
QueryKey
>,
"queryFn" | "queryKey"
>
) => {
const { data, ...rest } = useQuery({
queryFn: () => sdk.auth.mfa.list(),
queryKey: mfaQueryKeys.lists(),
...options,
})

return { ...data, ...rest }
}

export const useStartAuthMfa = (
options?: UseMutationOptions<
AuthMfaSetupResponse,
FetchError,
{
provider: AuthTypes.AuthMfaProvider
label?: string | null
issuer?: string
metadata?: Record<string, unknown> | null
}
>
) => {
return useMutation({
mutationFn: (payload) => sdk.auth.mfa.start(payload),
...options,
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({ queryKey: mfaQueryKeys.lists() })
options?.onSuccess?.(data, variables, context)
},
})
}

export const useVerifyAuthMfa = (
id: string,
options?: UseMutationOptions<
AuthMfaFactorResponse,
FetchError,
{ code: string }
>
) => {
return useMutation({
mutationFn: (payload) => sdk.auth.mfa.verify(id, payload),
...options,
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({ queryKey: mfaQueryKeys.lists() })
options?.onSuccess?.(data, variables, context)
},
})
}

export const useDisableAuthMfa = (
id: string,
options?: UseMutationOptions<
AuthMfaFactorResponse,
FetchError,
{ method?: AuthTypes.AuthMfaChallengeMethod; code?: string } | void
>
) => {
return useMutation({
mutationFn: (payload) => sdk.auth.mfa.disable(id, payload ?? {}),
...options,
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({ queryKey: mfaQueryKeys.lists() })
options?.onSuccess?.(data, variables, context)
},
})
}

export const useGenerateAuthMfaRecoveryCodes = (
options?: UseMutationOptions<
AuthMfaRecoveryCodesResponse,
FetchError,
{ count?: number } | void
>
) => {
return useMutation({
mutationFn: (payload) => sdk.auth.mfa.generateRecoveryCodes(payload ?? {}),
...options,
})
}

export const useVerifyAuthMfaChallenge = (
options?: UseMutationOptions<
string,
FetchError,
{ id: string; method: AuthTypes.AuthMfaChallengeMethod; code: string }
>
) => {
return useMutation({
mutationFn: ({ id, method, code }) =>
sdk.auth.mfa.verifyChallenge(id, { method, code }),
...options,
})
}
158 changes: 156 additions & 2 deletions packages/admin/dashboard/src/i18n/translations/$schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -9331,6 +9331,102 @@
"required": ["languageLabel", "usageInsightsLabel"],
"additionalProperties": false
},
"mfa": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"status": {
"type": "string"
},
"method": {
"type": "string"
},
"enabled": {
"type": "string"
},
"disabled": {
"type": "string"
},
"pending": {
"type": "string"
},
"authenticatorApp": {
"type": "string"
},
"noMethod": {
"type": "string"
},
"setupTitle": {
"type": "string"
},
"setupAuthenticatorApp": {
"type": "string"
},
"setupDescription": {
"type": "string"
},
"setupError": {
"type": "string"
},
"verifyError": {
"type": "string"
},
"qrError": {
"type": "string"
},
"recoveryCodesTitle": {
"type": "string"
},
"recoveryCodesDescription": {
"type": "string"
},
"disableTitle": {
"type": "string"
},
"disableDescription": {
"type": "string"
},
"disableChallengeDescription": {
"type": "string"
},
"disableError": {
"type": "string"
},
"disableSuccess": {
"type": "string"
}
},
"required": [
"title",
"description",
"status",
"method",
"enabled",
"disabled",
"pending",
"authenticatorApp",
"noMethod",
"setupTitle",
"setupAuthenticatorApp",
"setupDescription",
"setupError",
"verifyError",
"qrError",
"recoveryCodesTitle",
"recoveryCodesDescription",
"disableTitle",
"disableDescription",
"disableChallengeDescription",
"disableError",
"disableSuccess"
],
"additionalProperties": false
},
"edit": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -9370,6 +9466,7 @@
"domain",
"manageYourProfileDetails",
"fields",
"mfa",
"edit",
"toast"
],
Expand Down Expand Up @@ -10966,9 +11063,61 @@
},
"hint": {
"type": "string"
},
"mfa": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"recoveryDescription": {
"type": "string"
},
"recoveryCodePlaceholder": {
"type": "string"
},
"verify": {
"type": "string"
},
"verifyError": {
"type": "string"
},
"backToLogin": {
"type": "string"
},
"useRecoveryCodePrompt": {
"type": "string"
},
"useRecoveryCode": {
"type": "string"
},
"useAuthenticatorPrompt": {
"type": "string"
},
"useAuthenticator": {
"type": "string"
}
},
"required": [
"title",
"description",
"recoveryDescription",
"recoveryCodePlaceholder",
"verify",
"verifyError",
"backToLogin",
"useRecoveryCodePrompt",
"useRecoveryCode",
"useAuthenticatorPrompt",
"useAuthenticator"
],
"additionalProperties": false
}
},
"required": ["forgotPassword", "title", "hint"],
"required": ["forgotPassword", "title", "hint", "mfa"],
"additionalProperties": false
},
"invite": {
Expand Down Expand Up @@ -12638,7 +12787,12 @@
"additionalProperties": false
}
},
"required": ["accessDenied", "requiredPermissions", "resources", "actions"],
"required": [
"accessDenied",
"requiredPermissions",
"resources",
"actions"
],
"additionalProperties": false
}
},
Expand Down
Loading
Loading