diff --git a/packages/console/src/components/ConnectorTester/index.tsx b/packages/console/src/components/ConnectorTester/index.tsx index 792c345a0..386109857 100644 --- a/packages/console/src/components/ConnectorTester/index.tsx +++ b/packages/console/src/components/ConnectorTester/index.tsx @@ -1,6 +1,7 @@ import { ServiceConnector } from '@logto/connector-kit'; import { emailRegEx, phoneInputRegEx } from '@logto/core-kit'; import { ConnectorType } from '@logto/schemas'; +import { parsePhoneNumber } from '@logto/shared/universal'; import { conditional } from '@silverhand/essentials'; import { useEffect, useState } from 'react'; import { useForm, useFormContext } from 'react-hook-form'; @@ -13,7 +14,6 @@ import { Tooltip } from '@/ds-components/Tip'; import useApi from '@/hooks/use-api'; import { onKeyDownHandler } from '@/utils/a11y'; import { trySubmitSafe } from '@/utils/form'; -import { parsePhoneNumber } from '@/utils/phone'; import * as styles from './index.module.scss'; diff --git a/packages/console/src/components/UserAvatar/index.tsx b/packages/console/src/components/UserAvatar/index.tsx index 67670560a..de0494e25 100644 --- a/packages/console/src/components/UserAvatar/index.tsx +++ b/packages/console/src/components/UserAvatar/index.tsx @@ -1,4 +1,5 @@ import type { User } from '@logto/schemas'; +import { getUserDisplayName, formatToInternationalPhoneNumber } from '@logto/shared/universal'; import { conditional } from '@silverhand/essentials'; import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; @@ -6,7 +7,6 @@ import { useTranslation } from 'react-i18next'; import DefaultAvatar from '@/assets/images/default-avatar.svg'; import ImageWithErrorFallback from '@/ds-components/ImageWithErrorFallback'; import { Tooltip } from '@/ds-components/Tip'; -import { formatToInternationalPhoneNumber } from '@/utils/phone'; import * as styles from './index.module.scss'; @@ -84,7 +84,7 @@ function UserAvatar({ className, size = 'medium', user, hasTooltip = false }: Pr ); } - const nameToDisplay = (name ?? username ?? primaryEmail)?.toLocaleUpperCase(); + const nameToDisplay = getUserDisplayName({ name, username, primaryEmail })?.toLocaleUpperCase(); const color = conditional( nameToDisplay && diff --git a/packages/console/src/pages/UserDetails/UserSettings/index.tsx b/packages/console/src/pages/UserDetails/UserSettings/index.tsx index 74be10d47..32dd07bb8 100644 --- a/packages/console/src/pages/UserDetails/UserSettings/index.tsx +++ b/packages/console/src/pages/UserDetails/UserSettings/index.tsx @@ -1,5 +1,6 @@ import { emailRegEx, usernameRegEx } from '@logto/core-kit'; import type { User } from '@logto/schemas'; +import { parsePhoneNumber } from '@logto/shared/universal'; import { trySafe } from '@silverhand/essentials'; import { parsePhoneNumberWithError } from 'libphonenumber-js'; import { useForm, useController } from 'react-hook-form'; @@ -18,7 +19,6 @@ import { useConfirmModal } from '@/hooks/use-confirm-modal'; import useDocumentationUrl from '@/hooks/use-documentation-url'; import { trySubmitSafe } from '@/utils/form'; import { safeParseJsonObject } from '@/utils/json'; -import { parsePhoneNumber } from '@/utils/phone'; import { uriValidator } from '@/utils/validator'; import { type UserDetailsForm, type UserDetailsOutletContext } from '../types'; diff --git a/packages/console/src/pages/UserDetails/utils.ts b/packages/console/src/pages/UserDetails/utils.ts index 4d024025a..053d6f8d5 100644 --- a/packages/console/src/pages/UserDetails/utils.ts +++ b/packages/console/src/pages/UserDetails/utils.ts @@ -1,8 +1,7 @@ import type { User } from '@logto/schemas'; +import { formatToInternationalPhoneNumber } from '@logto/shared/universal'; import { conditional } from '@silverhand/essentials'; -import { formatToInternationalPhoneNumber } from '@/utils/phone'; - import type { UserDetailsForm } from './types'; export const userDetailsParser = { diff --git a/packages/console/src/pages/Users/components/CreateForm/index.tsx b/packages/console/src/pages/Users/components/CreateForm/index.tsx index f735d41f9..bb64dbb1a 100644 --- a/packages/console/src/pages/Users/components/CreateForm/index.tsx +++ b/packages/console/src/pages/Users/components/CreateForm/index.tsx @@ -1,5 +1,6 @@ import { emailRegEx, phoneInputRegEx, usernameRegEx } from '@logto/core-kit'; import type { CreateUser, User } from '@logto/schemas'; +import { parsePhoneNumber } from '@logto/shared/universal'; import { conditional } from '@silverhand/essentials'; import { useState } from 'react'; import { useForm } from 'react-hook-form'; @@ -17,7 +18,6 @@ import useTenantPathname from '@/hooks/use-tenant-pathname'; import * as modalStyles from '@/scss/modal.module.scss'; import { trySubmitSafe } from '@/utils/form'; import { generateRandomPassword } from '@/utils/password'; -import { parsePhoneNumber } from '@/utils/phone'; import * as styles from './index.module.scss'; diff --git a/packages/console/src/utils/user.ts b/packages/console/src/utils/user.ts index cd30b291d..6e81cc191 100644 --- a/packages/console/src/utils/user.ts +++ b/packages/console/src/utils/user.ts @@ -1,19 +1,16 @@ import type { User } from '@logto/schemas'; -import { conditional } from '@silverhand/essentials'; +import { getUserDisplayName } from '@logto/shared/universal'; import { t } from 'i18next'; -import { formatToInternationalPhoneNumber } from './phone'; +export const getUserTitle = (user?: User): string => + (user ? getUserDisplayName(user) : undefined) ?? t('admin_console.users.unnamed'); -const getSecondaryUserInfo = (user?: User) => { - const { primaryEmail, primaryPhone, username } = user ?? {}; - const formattedPhoneNumber = conditional( - primaryPhone && formatToInternationalPhoneNumber(primaryPhone) - ); - return primaryEmail ?? formattedPhoneNumber ?? username; +export const getUserSubtitle = (user?: User) => { + if (!user?.name) { + return; + } + + const { username, primaryEmail, primaryPhone } = user; + + return getUserDisplayName({ username, primaryEmail, primaryPhone }); }; - -export const getUserTitle = (user?: User) => - user?.name ?? getSecondaryUserInfo(user) ?? t('admin_console.users.unnamed'); - -export const getUserSubtitle = (user?: User) => - conditional(user?.name && getSecondaryUserInfo(user)); diff --git a/packages/core/src/routes/admin-user/mfa-verifications.ts b/packages/core/src/routes/admin-user/mfa-verifications.ts index ebd6d25a8..ffc2d4f63 100644 --- a/packages/core/src/routes/admin-user/mfa-verifications.ts +++ b/packages/core/src/routes/admin-user/mfa-verifications.ts @@ -82,7 +82,7 @@ export default function adminUserMfaVerificationsRoutes( const secret = generateTotpSecret(); const service = ctx.URL.hostname; const user = getUserDisplayName({ username, primaryEmail, primaryPhone, name }); - const keyUri = authenticator.keyuri(user, service, secret); + const keyUri = authenticator.keyuri(user ?? 'Unnamed User', service, secret); await addUserMfaVerification(id, { type: MfaFactor.TOTP, secret }); ctx.body = { type: MfaFactor.TOTP, diff --git a/packages/core/src/routes/interaction/additional.ts b/packages/core/src/routes/interaction/additional.ts index 9ae9afcfb..7e2f1d8a3 100644 --- a/packages/core/src/routes/interaction/additional.ts +++ b/packages/core/src/routes/interaction/additional.ts @@ -152,7 +152,7 @@ export default function additionalRoutes( name = null, } = await parseUserProfile(tenant, profileVerifiedInteraction); const user = getUserDisplayName({ username, primaryEmail, primaryPhone, name }); - const keyUri = authenticator.keyuri(user, service, secret); + const keyUri = authenticator.keyuri(user ?? 'Unnamed User', service, secret); ctx.body = { secret, @@ -166,7 +166,7 @@ export default function additionalRoutes( const { accountId } = profileVerifiedInteraction; const { username, primaryEmail, primaryPhone, name } = await findUserById(accountId); const user = getUserDisplayName({ username, primaryEmail, primaryPhone, name }); - const keyUri = authenticator.keyuri(user, service, secret); + const keyUri = authenticator.keyuri(user ?? 'Unnamed User', service, secret); ctx.body = { secret, diff --git a/packages/core/src/routes/interaction/utils/webauthn.ts b/packages/core/src/routes/interaction/utils/webauthn.ts index 57845515a..ced78779a 100644 --- a/packages/core/src/routes/interaction/utils/webauthn.ts +++ b/packages/core/src/routes/interaction/utils/webauthn.ts @@ -8,6 +8,7 @@ import { type WebAuthnVerificationPayload, type VerifyMfaResult, } from '@logto/schemas'; +import { getUserDisplayName } from '@logto/shared'; import { type GenerateRegistrationOptionsOpts, generateRegistrationOptions, @@ -31,14 +32,16 @@ export const generateWebAuthnRegistrationOptions = async ({ rpId, user, }: GenerateWebAuthnRegistrationOptionsParameters): Promise => { + const { username, primaryEmail, primaryPhone, id, mfaVerifications } = user; + const options: GenerateRegistrationOptionsOpts = { rpName: rpId, rpID: rpId, - userID: user.id, - userName: user.username ?? user.primaryEmail ?? user.primaryPhone ?? user.id, + userID: id, + userName: getUserDisplayName({ username, primaryEmail, primaryPhone }) ?? 'Unnamed User', timeout: 60_000, attestationType: 'none', - excludeCredentials: user.mfaVerifications + excludeCredentials: mfaVerifications .filter( (verification): verification is MfaVerificationWebAuthn => verification.type === MfaFactor.WebAuthn diff --git a/packages/shared/package.json b/packages/shared/package.json index b6b100bd9..046fdaffe 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -63,6 +63,7 @@ "@silverhand/essentials": "^2.8.4", "chalk": "^5.0.0", "find-up": "^6.3.0", + "libphonenumber-js": "^1.9.49", "nanoid": "^5.0.1", "slonik": "^30.0.0" } diff --git a/packages/shared/src/utils/index.ts b/packages/shared/src/utils/index.ts index fa00dbd58..47a6cae3e 100644 --- a/packages/shared/src/utils/index.ts +++ b/packages/shared/src/utils/index.ts @@ -2,3 +2,4 @@ export * from './object.js'; export * from './ttl-cache.js'; export * from './id.js'; export * from './user-display-name.js'; +export * from './phone.js'; diff --git a/packages/console/src/utils/phone.ts b/packages/shared/src/utils/phone.ts similarity index 100% rename from packages/console/src/utils/phone.ts rename to packages/shared/src/utils/phone.ts diff --git a/packages/shared/src/utils/user-display-name.ts b/packages/shared/src/utils/user-display-name.ts index 9c40f8f4e..38f9aa08f 100644 --- a/packages/shared/src/utils/user-display-name.ts +++ b/packages/shared/src/utils/user-display-name.ts @@ -1,3 +1,7 @@ +import { conditional } from '@silverhand/essentials'; + +import { formatToInternationalPhoneNumber } from './phone.js'; + /** * Get user display name from multiple fields */ @@ -7,10 +11,14 @@ export const getUserDisplayName = ({ primaryEmail, primaryPhone, }: { - name: string | null; - username: string | null; - primaryEmail: string | null; - primaryPhone: string | null; -}): string => { - return name ?? username ?? primaryEmail ?? primaryPhone ?? 'Unnamed User'; + name?: string | null; + username?: string | null; + primaryEmail?: string | null; + primaryPhone?: string | null; +}): string | undefined => { + const formattedPhoneNumber = conditional( + primaryPhone && formatToInternationalPhoneNumber(primaryPhone) + ); + + return name ?? primaryEmail ?? formattedPhoneNumber ?? username ?? undefined; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5166190ee..be37feb11 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3981,6 +3981,9 @@ importers: find-up: specifier: ^6.3.0 version: 6.3.0 + libphonenumber-js: + specifier: ^1.9.49 + version: 1.10.51 nanoid: specifier: ^5.0.1 version: 5.0.1 @@ -15604,7 +15607,6 @@ packages: /libphonenumber-js@1.10.51: resolution: {integrity: sha512-vY2I+rQwrDQzoPds0JeTEpeWzbUJgqoV0O4v31PauHBb/e+1KCXKylHcDnBMgJZ9fH9mErsEbROJY3Z3JtqEmg==} - dev: true /lightningcss-darwin-arm64@1.16.1: resolution: {integrity: sha512-/J898YSAiGVqdybHdIF3Ao0Hbh2vyVVj5YNm3NznVzTSvkOi3qQCAtO97sfmNz+bSRHXga7ZPLm+89PpOM5gAg==}