diff --git a/packages/console/src/assets/docs/guides/types.ts b/packages/console/src/assets/docs/guides/types.ts index dcc99e60374..36bd4628a1a 100644 --- a/packages/console/src/assets/docs/guides/types.ts +++ b/packages/console/src/assets/docs/guides/types.ts @@ -1,7 +1,8 @@ -import { type ApplicationType } from '@logto/schemas'; import { type MDXProps } from 'mdx/types'; import { type LazyExoticComponent, type ComponentType, type SVGProps } from 'react'; +import { type ApplicationTypeWithoutSaml } from '@/types/applications'; + /** * The guide metadata type. The directory name that the metadata is in will be the * unique identifier of the guide. @@ -17,7 +18,7 @@ export type GuideMetadata = { * For example, if the guide is for application creation, the target should be `ApplicationType`, * and an application of the target type should be created. */ - target: ApplicationType | 'API'; + target: ApplicationTypeWithoutSaml | 'API'; /** The related sample information of the guide. */ sample?: { /** The GitHub repository of the `logto-io` organization that the sample is in. */ diff --git a/packages/console/src/components/ApplicationCreation/CreateForm/Footer/index.tsx b/packages/console/src/components/ApplicationCreation/CreateForm/Footer/index.tsx index 0ae1d908d07..7852be6c85b 100644 --- a/packages/console/src/components/ApplicationCreation/CreateForm/Footer/index.tsx +++ b/packages/console/src/components/ApplicationCreation/CreateForm/Footer/index.tsx @@ -13,12 +13,13 @@ import Button from '@/ds-components/Button'; import TextLink from '@/ds-components/TextLink'; import useApplicationsUsage from '@/hooks/use-applications-usage'; import useUserPreferences from '@/hooks/use-user-preferences'; +import { type ApplicationTypeWithoutSaml } from '@/types/applications'; import { isPaidPlan } from '@/utils/subscription'; import styles from './index.module.scss'; type Props = { - readonly selectedType?: ApplicationType; + readonly selectedType?: ApplicationTypeWithoutSaml; readonly isLoading: boolean; readonly isThirdParty?: boolean; readonly onClickCreate: () => void; diff --git a/packages/console/src/components/ApplicationCreation/CreateForm/index.tsx b/packages/console/src/components/ApplicationCreation/CreateForm/index.tsx index 8bf01d668c9..ae9ea7e76e4 100644 --- a/packages/console/src/components/ApplicationCreation/CreateForm/index.tsx +++ b/packages/console/src/components/ApplicationCreation/CreateForm/index.tsx @@ -1,5 +1,4 @@ import { type AdminConsoleKey } from '@logto/phrases'; -import type { Application } from '@logto/schemas'; import { ApplicationType, ReservedPlanId } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; import { type ReactElement, useContext, useMemo } from 'react'; @@ -21,7 +20,11 @@ import useApplicationsUsage from '@/hooks/use-applications-usage'; import useCurrentUser from '@/hooks/use-current-user'; import TypeDescription from '@/pages/Applications/components/TypeDescription'; import modalStyles from '@/scss/modal.module.scss'; -import { applicationTypeI18nKey } from '@/types/applications'; +import { + applicationTypeI18nKey, + type ApplicationTypeWithoutSaml, + type Application, +} from '@/types/applications'; import { trySubmitSafe } from '@/utils/form'; import { isPaidPlan } from '@/utils/subscription'; @@ -29,7 +32,7 @@ import Footer from './Footer'; import styles from './index.module.scss'; type FormData = { - type: ApplicationType; + type: ApplicationTypeWithoutSaml; name: string; description?: string; isThirdParty?: boolean; @@ -37,7 +40,7 @@ type FormData = { export type Props = { readonly isDefaultCreateThirdParty?: boolean; - readonly defaultCreateType?: ApplicationType; + readonly defaultCreateType?: ApplicationTypeWithoutSaml; readonly defaultCreateFrameworkName?: string; readonly onClose?: (createdApp?: Application) => void; }; @@ -161,7 +164,7 @@ function CreateForm({ > {Object.values(ApplicationType) // Other application types (e.g. "Protected") should not show up in the creation modal - .filter((value): value is Exclude => + .filter((value): value is ApplicationTypeWithoutSaml => [ ApplicationType.Native, ApplicationType.SPA, diff --git a/packages/console/src/components/ApplicationCreation/index.tsx b/packages/console/src/components/ApplicationCreation/index.tsx index a95ed9665f8..dbd4c937778 100644 --- a/packages/console/src/components/ApplicationCreation/index.tsx +++ b/packages/console/src/components/ApplicationCreation/index.tsx @@ -1,7 +1,8 @@ -import { ApplicationType, RoleType, type Application } from '@logto/schemas'; +import { ApplicationType, RoleType } from '@logto/schemas'; import { useCallback, useState } from 'react'; import RoleAssignmentModal from '@/components/RoleAssignmentModal'; +import { type Application } from '@/types/applications'; import CreateForm, { type Props as CreateApplicationFormProps } from './CreateForm'; diff --git a/packages/console/src/components/ApplicationIcon/index.tsx b/packages/console/src/components/ApplicationIcon/index.tsx index 5a61d0ab1ea..5fe7c458f51 100644 --- a/packages/console/src/components/ApplicationIcon/index.tsx +++ b/packages/console/src/components/ApplicationIcon/index.tsx @@ -1,4 +1,4 @@ -import { ApplicationType, Theme } from '@logto/schemas'; +import { Theme } from '@logto/schemas'; import { darkModeApplicationIconMap, @@ -7,16 +7,20 @@ import { thirdPartyApplicationIconDark, } from '@/consts'; import useTheme from '@/hooks/use-theme'; +import { type ApplicationTypeWithoutSaml } from '@/types/applications'; type Props = { - readonly type: ApplicationType; + readonly type: ApplicationTypeWithoutSaml; readonly className?: string; readonly isThirdParty?: boolean; }; -const getIcon = (type: ApplicationType, isLightMode: boolean, isThirdParty?: boolean) => { - // We have ensured that SAML applications are always third party in DB schema, we use `??` here to make TypeScript happy. - if (isThirdParty ?? type === ApplicationType.SAML) { +const getIcon = ( + type: ApplicationTypeWithoutSaml, + isLightMode: boolean, + isThirdParty?: boolean +) => { + if (isThirdParty) { return isLightMode ? thirdPartyApplicationIcon : thirdPartyApplicationIconDark; } diff --git a/packages/console/src/components/AuditLogTable/index.tsx b/packages/console/src/components/AuditLogTable/index.tsx index 17f74028fd1..447e4d3820b 100644 --- a/packages/console/src/components/AuditLogTable/index.tsx +++ b/packages/console/src/components/AuditLogTable/index.tsx @@ -1,4 +1,4 @@ -import type { Log, ApplicationResponse } from '@logto/schemas'; +import type { Log } from '@logto/schemas'; import { LogResult, ApplicationType } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; import { useTranslation } from 'react-i18next'; @@ -12,6 +12,7 @@ import type { Column } from '@/ds-components/Table/types'; import type { RequestError } from '@/hooks/use-api'; import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher'; import useTenantPathname from '@/hooks/use-tenant-pathname'; +import { type ApplicationResponse } from '@/types/applications'; import { buildUrl } from '@/utils/url'; import EmptyDataPlaceholder from '../EmptyDataPlaceholder'; diff --git a/packages/console/src/components/EntitiesTransfer/components/EntityItem/index.tsx b/packages/console/src/components/EntitiesTransfer/components/EntityItem/index.tsx index 7646f9f66ee..4a4c9a50d0a 100644 --- a/packages/console/src/components/EntitiesTransfer/components/EntityItem/index.tsx +++ b/packages/console/src/components/EntitiesTransfer/components/EntityItem/index.tsx @@ -1,8 +1,9 @@ -import { type Application, type User } from '@logto/schemas'; +import { type User } from '@logto/schemas'; import ApplicationIcon from '@/components/ApplicationIcon'; import UserAvatar from '@/components/UserAvatar'; import SuspendedTag from '@/pages/Users/components/SuspendedTag'; +import { type Application } from '@/types/applications'; import { getUserTitle } from '@/utils/user'; import styles from './index.module.scss'; diff --git a/packages/console/src/components/Guide/hooks.ts b/packages/console/src/components/Guide/hooks.ts index 3eceb14d2c4..e73c5e1438e 100644 --- a/packages/console/src/components/Guide/hooks.ts +++ b/packages/console/src/components/Guide/hooks.ts @@ -1,4 +1,3 @@ -import { ApplicationType } from '@logto/schemas'; import { useCallback, useMemo } from 'react'; import { guides } from '@/assets/docs/guides'; @@ -99,8 +98,7 @@ export const useAppGuideMetadata = (): { return accumulated; } - // We have ensured that SAML applications are always third party in DB schema, we use `||` here to make TypeScript happy. - if (target === ApplicationType.SAML || isThirdParty) { + if (isThirdParty) { return { ...accumulated, [thirdPartyAppCategory]: [...accumulated[thirdPartyAppCategory], guide], diff --git a/packages/console/src/components/Guide/index.tsx b/packages/console/src/components/Guide/index.tsx index 7bad21cab04..3222a3d452d 100644 --- a/packages/console/src/components/Guide/index.tsx +++ b/packages/console/src/components/Guide/index.tsx @@ -1,4 +1,3 @@ -import { type ApplicationResponse } from '@logto/schemas'; import classNames from 'classnames'; import { type ComponentType, @@ -16,6 +15,7 @@ import OverlayScrollbar from '@/ds-components/OverlayScrollbar'; import MdxProvider from '@/mdx-components/MdxProvider'; import { type ApplicationSecretRow } from '@/pages/ApplicationDetails/ApplicationDetailsContent/EndpointsAndCredentials'; import NotFound from '@/pages/NotFound'; +import type { ApplicationResponse } from '@/types/applications'; import StepsSkeleton from './StepsSkeleton'; import styles from './index.module.scss'; diff --git a/packages/console/src/components/ItemPreview/ApplicationPreview.tsx b/packages/console/src/components/ItemPreview/ApplicationPreview.tsx index 0639df6eeb8..ae2527d9eb6 100644 --- a/packages/console/src/components/ItemPreview/ApplicationPreview.tsx +++ b/packages/console/src/components/ItemPreview/ApplicationPreview.tsx @@ -1,7 +1,7 @@ -import { type Application, ApplicationType } from '@logto/schemas'; import { useTranslation } from 'react-i18next'; import ApplicationIcon from '@/components/ApplicationIcon'; +import { type Application } from '@/types/applications'; import { applicationTypeI18nKey } from '@/types/applications'; import ItemPreview from '.'; @@ -21,8 +21,7 @@ function ApplicationPreview({ data: { id, name, isThirdParty, type } }: Props) { ]: SvgComponent; + [key in ApplicationTypeWithoutSaml]: SvgComponent; }; export const lightModeApplicationIconMap: ApplicationIconMap = Object.freeze({ diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/GuideDrawer/index.tsx b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/GuideDrawer/index.tsx index dbaf731d8b4..44001e552db 100644 --- a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/GuideDrawer/index.tsx +++ b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/GuideDrawer/index.tsx @@ -1,4 +1,3 @@ -import { ApplicationType, type ApplicationResponse } from '@logto/schemas'; import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,6 +8,7 @@ import GuideCardGroup from '@/components/Guide/GuideCardGroup'; import { useAppGuideMetadata } from '@/components/Guide/hooks'; import IconButton from '@/ds-components/IconButton'; import Spacer from '@/ds-components/Spacer'; +import { type ApplicationResponse } from '@/types/applications'; import AppGuide from '../../components/AppGuide'; import { type ApplicationSecretRow } from '../EndpointsAndCredentials'; @@ -26,30 +26,24 @@ function GuideDrawer({ app, secrets, onClose }: Props) { const { getStructuredAppGuideMetadata } = useAppGuideMetadata(); const [selectedGuide, setSelectedGuide] = useState(); - const appType = useMemo( - // SAML application is actually a Traditional application, the same as OIDC applications. - () => (app.type === ApplicationType.SAML ? ApplicationType.Traditional : app.type), - [app.type] - ); - const structuredMetadata = useMemo( - () => getStructuredAppGuideMetadata({ categories: [appType] }), - [getStructuredAppGuideMetadata, appType] + () => getStructuredAppGuideMetadata({ categories: [app.type] }), + [getStructuredAppGuideMetadata, app.type] ); const hasSingleGuide = useMemo(() => { - return structuredMetadata[appType].length === 1; - }, [appType, structuredMetadata]); + return structuredMetadata[app.type].length === 1; + }, [app.type, structuredMetadata]); useEffect(() => { if (hasSingleGuide) { - const guide = structuredMetadata[appType][0]; + const guide = structuredMetadata[app.type][0]; if (guide) { const { id, metadata } = guide; setSelectedGuide({ id, metadata }); } } - }, [hasSingleGuide, appType, structuredMetadata]); + }, [hasSingleGuide, app.type, structuredMetadata]); return (
@@ -81,8 +75,8 @@ function GuideDrawer({ app, secrets, onClose }: Props) { {!selectedGuide && ( { setSelectedGuide(guide); }} diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/RefreshTokenSettings.tsx b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/RefreshTokenSettings.tsx index 6d19687b21d..467551423fc 100644 --- a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/RefreshTokenSettings.tsx +++ b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/RefreshTokenSettings.tsx @@ -1,4 +1,4 @@ -import { type Application, ApplicationType, customClientMetadataGuard } from '@logto/schemas'; +import { ApplicationType, customClientMetadataGuard } from '@logto/schemas'; import { useFormContext } from 'react-hook-form'; import { Trans, useTranslation } from 'react-i18next'; @@ -7,6 +7,7 @@ import FormField from '@/ds-components/FormField'; import Switch from '@/ds-components/Switch'; import TextInput from '@/ds-components/TextInput'; import TextLink from '@/ds-components/TextLink'; +import { type Application } from '@/types/applications'; type Props = { readonly data: Application; diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Settings.tsx b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Settings.tsx index 880d74a03b8..1480f4db4e2 100644 --- a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Settings.tsx +++ b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/Settings.tsx @@ -1,5 +1,4 @@ import { validateRedirectUrl } from '@logto/core-kit'; -import type { Application } from '@logto/schemas'; import { ApplicationType } from '@logto/schemas'; import { Controller, useFormContext } from 'react-hook-form'; import { Trans, useTranslation } from 'react-i18next'; @@ -16,6 +15,7 @@ import { import TextInput from '@/ds-components/TextInput'; import TextLink from '@/ds-components/TextLink'; import useDocumentationUrl from '@/hooks/use-documentation-url'; +import type { Application } from '@/types/applications'; import { isJsonObject } from '@/utils/json'; import ProtectedAppSettings from './ProtectedAppSettings'; diff --git a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/index.tsx b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/index.tsx index 8804b452d4d..4d1c9291fa3 100644 --- a/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/index.tsx +++ b/packages/console/src/pages/ApplicationDetails/ApplicationDetailsContent/index.tsx @@ -1,8 +1,4 @@ -import { - ApplicationType, - type ApplicationResponse, - type SnakeCaseOidcConfig, -} from '@logto/schemas'; +import { ApplicationType, type SnakeCaseOidcConfig } from '@logto/schemas'; import { useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { toast } from 'react-hot-toast'; @@ -28,6 +24,7 @@ import { organizations } from '@/hooks/use-console-routes/routes/organizations'; import useDocumentationUrl from '@/hooks/use-documentation-url'; import useTenantPathname from '@/hooks/use-tenant-pathname'; import { applicationTypeI18nKey } from '@/types/applications'; +import { type ApplicationResponse } from '@/types/applications'; import { trySubmitSafe } from '@/utils/form'; import BackchannelLogout from './BackchannelLogout'; @@ -123,8 +120,7 @@ function ApplicationDetailsContent({ data, secrets, oidcConfig, onApplicationUpd icon={} title={data.name} primaryTag={ - // We have ensured that SAML applications are always third party in DB schema, we use `||` here to make TypeScript happy. - data.isThirdParty || data.type === ApplicationType.SAML + data.isThirdParty ? t(`${applicationTypeI18nKey.thirdParty}.title`) : t(`${applicationTypeI18nKey[data.type]}.title`) } diff --git a/packages/console/src/pages/ApplicationDetails/GuideModal/index.tsx b/packages/console/src/pages/ApplicationDetails/GuideModal/index.tsx index 89bda4252fa..83142dc2861 100644 --- a/packages/console/src/pages/ApplicationDetails/GuideModal/index.tsx +++ b/packages/console/src/pages/ApplicationDetails/GuideModal/index.tsx @@ -1,8 +1,8 @@ -import type { ApplicationResponse } from '@logto/schemas'; import Modal from 'react-modal'; import ModalHeader from '@/components/Guide/ModalHeader'; import modalStyles from '@/scss/modal.module.scss'; +import { type ApplicationResponse } from '@/types/applications'; import { type ApplicationSecretRow } from '../ApplicationDetailsContent/EndpointsAndCredentials'; import AppGuide from '../components/AppGuide'; diff --git a/packages/console/src/pages/ApplicationDetails/components/AppGuide/index.tsx b/packages/console/src/pages/ApplicationDetails/components/AppGuide/index.tsx index 12ac43f7aec..c5bb8a41b1f 100644 --- a/packages/console/src/pages/ApplicationDetails/components/AppGuide/index.tsx +++ b/packages/console/src/pages/ApplicationDetails/components/AppGuide/index.tsx @@ -1,4 +1,3 @@ -import { type ApplicationResponse } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; import { useContext, useMemo } from 'react'; @@ -6,6 +5,7 @@ import { guides } from '@/assets/docs/guides'; import Guide, { GuideContext, type GuideContextType } from '@/components/Guide'; import { AppDataContext } from '@/contexts/AppDataProvider'; import useCustomDomain from '@/hooks/use-custom-domain'; +import { type ApplicationResponse } from '@/types/applications'; import { type ApplicationSecretRow } from '../../ApplicationDetailsContent/EndpointsAndCredentials'; diff --git a/packages/console/src/pages/ApplicationDetails/index.tsx b/packages/console/src/pages/ApplicationDetails/index.tsx index f0840a1c688..84dd4a5b9d0 100644 --- a/packages/console/src/pages/ApplicationDetails/index.tsx +++ b/packages/console/src/pages/ApplicationDetails/index.tsx @@ -1,4 +1,4 @@ -import { type ApplicationResponse, type SnakeCaseOidcConfig } from '@logto/schemas'; +import { type SnakeCaseOidcConfig } from '@logto/schemas'; import { useParams } from 'react-router-dom'; import useSWR from 'swr'; @@ -8,6 +8,7 @@ import { openIdProviderConfigPath } from '@/consts/oidc'; import { Daisy } from '@/ds-components/Spinner'; import type { RequestError } from '@/hooks/use-api'; import useTenantPathname from '@/hooks/use-tenant-pathname'; +import { type ApplicationResponse } from '@/types/applications'; import ApplicationDetailsContent from './ApplicationDetailsContent'; import { type ApplicationSecretRow } from './ApplicationDetailsContent/EndpointsAndCredentials'; diff --git a/packages/console/src/pages/Applications/components/ProtectedAppForm/index.tsx b/packages/console/src/pages/Applications/components/ProtectedAppForm/index.tsx index a19333a6afc..66b05d3da3a 100644 --- a/packages/console/src/pages/Applications/components/ProtectedAppForm/index.tsx +++ b/packages/console/src/pages/Applications/components/ProtectedAppForm/index.tsx @@ -1,5 +1,5 @@ import { isLocalhost, validateUriOrigin } from '@logto/core-kit'; -import { ApplicationType, type Application, type RequestErrorBody } from '@logto/schemas'; +import { ApplicationType, type RequestErrorBody } from '@logto/schemas'; import { isValidSubdomain } from '@logto/shared/universal'; import { condString, conditional } from '@silverhand/essentials'; import classNames from 'classnames'; @@ -22,6 +22,7 @@ import TextLink from '@/ds-components/TextLink'; import useApi from '@/hooks/use-api'; import useApplicationsUsage from '@/hooks/use-applications-usage'; import useTenantPathname from '@/hooks/use-tenant-pathname'; +import type { Application } from '@/types/applications'; import styles from './index.module.scss'; diff --git a/packages/console/src/pages/Applications/components/TypeDescription/index.tsx b/packages/console/src/pages/Applications/components/TypeDescription/index.tsx index dfecf63eb07..8685ffba952 100644 --- a/packages/console/src/pages/Applications/components/TypeDescription/index.tsx +++ b/packages/console/src/pages/Applications/components/TypeDescription/index.tsx @@ -1,7 +1,7 @@ -import { type ApplicationType } from '@logto/schemas'; import classNames from 'classnames'; import ApplicationIcon from '@/components/ApplicationIcon'; +import { type ApplicationTypeWithoutSaml } from '@/types/applications'; import styles from './index.module.scss'; @@ -9,7 +9,7 @@ type Props = { readonly title: string; readonly subtitle: string; readonly description: string; - readonly type: ApplicationType; + readonly type: ApplicationTypeWithoutSaml; readonly size?: 'large' | 'small'; }; diff --git a/packages/console/src/pages/Applications/hooks/use-application-data.ts b/packages/console/src/pages/Applications/hooks/use-application-data.ts index 584eb1bbae4..60e326ad58b 100644 --- a/packages/console/src/pages/Applications/hooks/use-application-data.ts +++ b/packages/console/src/pages/Applications/hooks/use-application-data.ts @@ -1,10 +1,10 @@ -import { type Application } from '@logto/schemas'; import { useCallback, useEffect, useMemo, useState } from 'react'; import useSWR from 'swr'; import { defaultPageSize } from '@/consts'; import { type RequestError } from '@/hooks/use-api'; import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher'; +import { type Application } from '@/types/applications'; import { buildUrl } from '@/utils/url'; const pageSize = defaultPageSize; diff --git a/packages/console/src/pages/EnterpriseSsoDetails/IdpInitiatedAuth/ConfigForm.tsx b/packages/console/src/pages/EnterpriseSsoDetails/IdpInitiatedAuth/ConfigForm.tsx index 0beac2f2028..e5bd9d22e43 100644 --- a/packages/console/src/pages/EnterpriseSsoDetails/IdpInitiatedAuth/ConfigForm.tsx +++ b/packages/console/src/pages/EnterpriseSsoDetails/IdpInitiatedAuth/ConfigForm.tsx @@ -1,6 +1,5 @@ /* eslint-disable max-lines */ import { - type Application, type SsoConnectorWithProviderConfig, ApplicationType, type SsoConnectorIdpInitiatedAuthConfig, @@ -22,6 +21,7 @@ import Switch from '@/ds-components/Switch'; import TextInput from '@/ds-components/TextInput'; import useApi from '@/hooks/use-api'; import useTenantPathname from '@/hooks/use-tenant-pathname'; +import { type Application } from '@/types/applications'; import { trySubmitSafe } from '@/utils/form'; import { uriValidator } from '@/utils/validator'; diff --git a/packages/console/src/pages/EnterpriseSsoDetails/IdpInitiatedAuth/index.tsx b/packages/console/src/pages/EnterpriseSsoDetails/IdpInitiatedAuth/index.tsx index 893efc67598..60db1d00282 100644 --- a/packages/console/src/pages/EnterpriseSsoDetails/IdpInitiatedAuth/index.tsx +++ b/packages/console/src/pages/EnterpriseSsoDetails/IdpInitiatedAuth/index.tsx @@ -1,9 +1,10 @@ -import { type Application, type SsoConnectorWithProviderConfig } from '@logto/schemas'; +import { type SsoConnectorWithProviderConfig } from '@logto/schemas'; import { useMemo } from 'react'; import useSWR from 'swr'; import FormCard, { FormCardSkeleton } from '@/components/FormCard'; import { type RequestError } from '@/hooks/use-api'; +import { type Application } from '@/types/applications'; import ConfigForm from './ConfigForm'; import useIdpInitiatedAuthConfigSWR from './use-idp-initiated-auth-config-swr'; diff --git a/packages/console/src/pages/EnterpriseSsoDetails/IdpInitiatedAuth/utils.ts b/packages/console/src/pages/EnterpriseSsoDetails/IdpInitiatedAuth/utils.ts index 6974193a7c6..d0cd09ce547 100644 --- a/packages/console/src/pages/EnterpriseSsoDetails/IdpInitiatedAuth/utils.ts +++ b/packages/console/src/pages/EnterpriseSsoDetails/IdpInitiatedAuth/utils.ts @@ -1,5 +1,4 @@ import { - type Application, ApplicationType, SsoConnectorIdpInitiatedAuthConfigs, type CreateSsoConnectorIdpInitiatedAuthConfig, @@ -9,6 +8,8 @@ import { conditional, type DeepPartial } from '@silverhand/essentials'; import { t } from 'i18next'; import { toast } from 'react-hot-toast'; +import { type Application } from '@/types/applications'; + const applicationsSearchParams = new URLSearchParams([ ['types', ApplicationType.Traditional], ['types', ApplicationType.SPA], diff --git a/packages/console/src/pages/GetStarted/index.tsx b/packages/console/src/pages/GetStarted/index.tsx index 26557b6eca9..0fe54f4a5c9 100644 --- a/packages/console/src/pages/GetStarted/index.tsx +++ b/packages/console/src/pages/GetStarted/index.tsx @@ -1,4 +1,4 @@ -import { ApplicationType, Theme, type Application, type Resource } from '@logto/schemas'; +import { ApplicationType, Theme, type Resource } from '@logto/schemas'; import classNames from 'classnames'; import { useCallback, useContext, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -24,6 +24,7 @@ import TextLink from '@/ds-components/TextLink'; import useTenantPathname from '@/hooks/use-tenant-pathname'; import useTheme from '@/hooks/use-theme'; import useWindowResize from '@/hooks/use-window-resize'; +import { type Application } from '@/types/applications'; import CreateApiForm from '../ApiResources/components/CreateForm'; import ProtectedAppModal from '../Applications/components/ProtectedAppModal'; diff --git a/packages/console/src/pages/OrganizationDetails/MachineToMachine/AddAppsToOrganization.tsx b/packages/console/src/pages/OrganizationDetails/MachineToMachine/AddAppsToOrganization.tsx index bcaf531674c..02c8a201475 100644 --- a/packages/console/src/pages/OrganizationDetails/MachineToMachine/AddAppsToOrganization.tsx +++ b/packages/console/src/pages/OrganizationDetails/MachineToMachine/AddAppsToOrganization.tsx @@ -1,4 +1,4 @@ -import { type Organization, type Application, ApplicationType, RoleType } from '@logto/schemas'; +import { type Organization, ApplicationType, RoleType } from '@logto/schemas'; import { useEffect, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -15,6 +15,7 @@ import { type Option } from '@/ds-components/Select/MultiSelect'; import useActionTranslation from '@/hooks/use-action-translation'; import useApi from '@/hooks/use-api'; import modalStyles from '@/scss/modal.module.scss'; +import { type Application } from '@/types/applications'; import { trySubmitSafe } from '@/utils/form'; type Props = { diff --git a/packages/console/src/pages/OrganizationDetails/MachineToMachine/index.tsx b/packages/console/src/pages/OrganizationDetails/MachineToMachine/index.tsx index 117102807aa..e8ab7703b91 100644 --- a/packages/console/src/pages/OrganizationDetails/MachineToMachine/index.tsx +++ b/packages/console/src/pages/OrganizationDetails/MachineToMachine/index.tsx @@ -1,4 +1,4 @@ -import { type ApplicationWithOrganizationRoles } from '@logto/schemas'; +import { type ApplicationWithOrganizationRoles as ApplicationWithOrganizationRolesSchema } from '@logto/schemas'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useOutletContext } from 'react-router-dom'; @@ -17,6 +17,7 @@ import Table from '@/ds-components/Table'; import Tag from '@/ds-components/Tag'; import useActionTranslation from '@/hooks/use-action-translation'; import useApi, { type RequestError } from '@/hooks/use-api'; +import { type ApplicationTypeWithoutSaml } from '@/types/applications'; import { buildUrl } from '@/utils/url'; import EditOrganizationRolesModal from '../EditOrganizationRolesModal'; @@ -27,6 +28,11 @@ import styles from './index.module.scss'; const pageSize = defaultPageSize; +// The type definition of `ApplicationWithOrganizationRoles` defined in @logto/schemas uses `Application` type from @logto/schemas directly, but per our design, the Application here should ONLY be MachineToMachine type and not include SAML type for sure. So it is safe to exclude value `SAML` from the `type` field here. +type ApplicationWithOrganizationRoles = Omit & { + type: ApplicationTypeWithoutSaml; +}; + function MachineToMachine() { const { data: organization } = useOutletContext(); const api = useApi(); diff --git a/packages/console/src/pages/RoleDetails/RoleApplications/index.tsx b/packages/console/src/pages/RoleDetails/RoleApplications/index.tsx index 2cc9dacf8ba..16358a52e65 100644 --- a/packages/console/src/pages/RoleDetails/RoleApplications/index.tsx +++ b/packages/console/src/pages/RoleDetails/RoleApplications/index.tsx @@ -1,4 +1,4 @@ -import { type Application, ApplicationType } from '@logto/schemas'; +import { ApplicationType } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; import { useState } from 'react'; import { toast } from 'react-hot-toast'; @@ -22,6 +22,7 @@ import type { RequestError } from '@/hooks/use-api'; import useApi from '@/hooks/use-api'; import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher'; import AssignRoleModal from '@/pages/Roles/components/AssignRoleModal'; +import { type Application } from '@/types/applications'; import { buildUrl, formatSearchKeyword } from '@/utils/url'; import type { RoleDetailsOutletContext } from '../types'; diff --git a/packages/console/src/pages/Roles/components/AssignRoleModal/index.tsx b/packages/console/src/pages/Roles/components/AssignRoleModal/index.tsx index caaf8ba0456..6c2ac98101b 100644 --- a/packages/console/src/pages/Roles/components/AssignRoleModal/index.tsx +++ b/packages/console/src/pages/Roles/components/AssignRoleModal/index.tsx @@ -1,4 +1,4 @@ -import { type Application, RoleType, type User, ApplicationType, type Role } from '@logto/schemas'; +import { RoleType, type User, ApplicationType, type Role } from '@logto/schemas'; import { useState } from 'react'; import { toast } from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; @@ -13,6 +13,7 @@ import FormField from '@/ds-components/FormField'; import ModalLayout from '@/ds-components/ModalLayout'; import useApi from '@/hooks/use-api'; import modalStyles from '@/scss/modal.module.scss'; +import { type Application } from '@/types/applications'; const isUserEntity = (entity: User | Application): entity is User => 'customData' in entity || 'identities' in entity; diff --git a/packages/console/src/types/applications.ts b/packages/console/src/types/applications.ts index 8678b7107e5..ed32832f5b1 100644 --- a/packages/console/src/types/applications.ts +++ b/packages/console/src/types/applications.ts @@ -1,7 +1,20 @@ import { ApplicationType } from '@logto/schemas'; +import type { + ApplicationResponse as ApplicationResponseSchema, + Application as ApplicationSchema, +} from '@logto/schemas'; import { type Guide } from '@/assets/docs/guides/types'; +// We filter out SAML-typed applications from the list of application types for now until the console is ready. +export type ApplicationTypeWithoutSaml = Exclude; +export type ApplicationResponse = Omit & { + type: ApplicationTypeWithoutSaml; +}; +export type Application = Omit & { + type: ApplicationTypeWithoutSaml; +}; + export const thirdPartyAppCategory = 'ThirdParty'; export const applicationTypeI18nKey = Object.freeze({ diff --git a/packages/core/src/routes/applications/application.ts b/packages/core/src/routes/applications/application.ts index 14b49aec715..af1e2305886 100644 --- a/packages/core/src/routes/applications/application.ts +++ b/packages/core/src/routes/applications/application.ts @@ -2,7 +2,6 @@ /* eslint-disable max-lines */ import type { Role } from '@logto/schemas'; import { - Applications, ApplicationType, buildDemoAppDataForTenant, demoAppApplicationId, @@ -24,7 +23,13 @@ import type { ManagementApiRouter, RouterInitArgs } from '../types.js'; import applicationCustomDataRoutes from './application-custom-data.js'; import { generateInternalSecret } from './application-secret.js'; -import { applicationCreateGuard, applicationPatchGuard } from './types.js'; +import { + applicationCreateGuard, + applicationPatchGuard, + applicationTypeValuesWithoutSaml, + applicationTypeGuardWithoutSaml, + applicationResponseGuard, +} from './types.js'; const includesInternalAdminRole = (roles: Readonly>) => roles.some(({ role: { name } }) => name === InternalRole.Admin); @@ -37,8 +42,6 @@ const parseIsThirdPartQueryParam = (isThirdPartyQuery: 'true' | 'false' | undefi return isThirdPartyQuery === 'true'; }; -const applicationTypeGuard = z.nativeEnum(ApplicationType); - export default function applicationRoutes( ...[router, tenant]: RouterInitArgs ) { @@ -57,27 +60,31 @@ export default function applicationRoutes( * We treat the `types` query param as an array, but it will be parsed as string-typed * value if only one type is specified, manually convert to ApplicationType array. */ - types: applicationTypeGuard + types: applicationTypeGuardWithoutSaml .array() - .or(applicationTypeGuard.transform((type) => [type])) + .or(applicationTypeGuardWithoutSaml.transform((type) => [type])) .optional(), excludeRoleId: string().optional(), excludeOrganizationId: string().optional(), isThirdParty: z.union([z.literal('true'), z.literal('false')]).optional(), }), - response: z.array(Applications.guard), + response: z.array(applicationResponseGuard), status: 200, }), async (ctx, next) => { const { limit, offset, disabled: paginationDisabled } = ctx.pagination; const { searchParams } = ctx.URL; const { - types, + types: rawTypes, excludeRoleId, excludeOrganizationId, isThirdParty: isThirdPartyParam, } = ctx.guard.query; + // We filter out SAML-typed applications from the list of application types for now until the console is ready. + // When declaring `types`, this API will retrieve all applications whose `type` are declared in `types`; if no `types` are declared, it will retrieve all applications. + const types: ApplicationType[] = rawTypes ?? Object.values(applicationTypeValuesWithoutSaml); + if (excludeRoleId && excludeOrganizationId) { throw new RequestError({ code: 'request.invalid_input', @@ -144,17 +151,12 @@ export default function applicationRoutes( '/applications', koaGuard({ body: applicationCreateGuard, - response: Applications.guard, + response: applicationResponseGuard, status: [200, 400, 422, 500], }), - // eslint-disable-next-line complexity async (ctx, next) => { const { oidcClientMetadata, protectedAppMetadata, ...rest } = ctx.guard.body; - if (rest.type === ApplicationType.SAML) { - throw new RequestError('application.use_saml_app_api'); - } - await Promise.all([ rest.type === ApplicationType.MachineToMachine && quota.guardTenantUsageByKey('machineToMachineLimit'), @@ -218,7 +220,7 @@ export default function applicationRoutes( '/applications/:id', koaGuard({ params: object({ id: string().min(1) }), - response: Applications.guard.merge(z.object({ isAdmin: z.boolean() })), + response: applicationResponseGuard.merge(z.object({ isAdmin: z.boolean() })), status: [200, 404], }), async (ctx, next) => { @@ -234,6 +236,11 @@ export default function applicationRoutes( } const application = await queries.applications.findApplicationById(id); + + if (application.type === ApplicationType.SAML) { + throw new RequestError('application.use_saml_app_api'); + } + const applicationsRoles = await queries.applicationsRoles.findApplicationsRolesByApplicationId(id); @@ -255,7 +262,7 @@ export default function applicationRoutes( isAdmin: boolean().optional(), }) ), - response: Applications.guard, + response: applicationResponseGuard, status: [200, 404, 422, 500], }), async (ctx, next) => { @@ -346,6 +353,11 @@ export default function applicationRoutes( async (ctx, next) => { const { id } = ctx.guard.params; const { type, protectedAppMetadata } = await queries.applications.findApplicationById(id); + + if (type === ApplicationType.SAML) { + throw new RequestError('application.use_saml_app_api'); + } + if (type === ApplicationType.Protected && protectedAppMetadata) { assertThat( !protectedAppMetadata.customDomains || protectedAppMetadata.customDomains.length === 0, diff --git a/packages/core/src/routes/applications/types.ts b/packages/core/src/routes/applications/types.ts index 33ca10fc99b..f113ab6ee4a 100644 --- a/packages/core/src/routes/applications/types.ts +++ b/packages/core/src/routes/applications/types.ts @@ -1,10 +1,31 @@ import { applicationCreateGuard as originalApplicationCreateGuard, applicationPatchGuard as originalApplicationPatchGuard, + ApplicationType, + Applications, } from '@logto/schemas'; import { z } from 'zod'; +type ApplicationTypeWithoutSaml = Exclude; + +export const applicationTypeValuesWithoutSaml = Object.freeze([ + ApplicationType.Native, + ApplicationType.SPA, + ApplicationType.Traditional, + ApplicationType.MachineToMachine, + ApplicationType.Protected, +] as const) satisfies Readonly; + +export const applicationTypeGuardWithoutSaml = z.enum( + applicationTypeValuesWithoutSaml +) satisfies z.ZodType; + export const applicationCreateGuard = originalApplicationCreateGuard + .merge( + z.object({ + type: applicationTypeGuardWithoutSaml, + }) + ) .omit({ protectedAppMetadata: true, }) @@ -37,3 +58,9 @@ export const applicationPatchGuard = originalApplicationPatchGuard }) .optional(), }); + +export const applicationResponseGuard = Applications.guard.merge( + z.object({ + type: applicationTypeGuardWithoutSaml, + }) +); diff --git a/packages/integration-tests/src/tests/api/application/application.test.ts b/packages/integration-tests/src/tests/api/application/application.test.ts index bb6324ee722..66ff35fc22c 100644 --- a/packages/integration-tests/src/tests/api/application/application.test.ts +++ b/packages/integration-tests/src/tests/api/application/application.test.ts @@ -37,12 +37,19 @@ describe('application APIs', () => { it('should throw error when creating a non-third party SAML application', async () => { await expectRejects(createApplication('test-create-saml-app', ApplicationType.SAML), { - code: 'application.use_saml_app_api', + code: 'guard.invalid_input', status: 400, }); }); - // TODO: add tests for blocking updating SAML application with `PATCH /applications/:id` API, we can not do it before we implement the `POST /saml-applications` API + it('should throw error when trying to GET SAML applications', async () => { + await expectRejects(getApplications([ApplicationType.SAML]), { + code: 'guard.invalid_input', + status: 400, + }); + }); + + // TODO: add tests for blocking updating SAML application with `PATCH/DELETE/GET /applications/:id` API, we can not do it before we implement the `POST /saml-applications` API it('should create OIDC third party application successfully', async () => { const applicationName = 'test-third-party-app'; diff --git a/packages/schemas/src/utils/application.ts b/packages/schemas/src/utils/application.ts index c42d6510696..bb63a6e6fa3 100644 --- a/packages/schemas/src/utils/application.ts +++ b/packages/schemas/src/utils/application.ts @@ -6,4 +6,6 @@ export const hasSecrets = (type: ApplicationType) => ApplicationType.MachineToMachine, ApplicationType.Protected, ApplicationType.Traditional, + // SAML applications are used as traditional web applications. + ApplicationType.SAML, ].includes(type);