From 004b7995cb193abb5c38d910957b883b6c35ee29 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Thu, 11 Apr 2024 17:50:23 +0800 Subject: [PATCH 1/3] fix(console): make profile a tenant independent page --- packages/console/src/cloud/AppRoutes.tsx | 17 +-- .../Topbar/UserInfo/index.module.scss | 5 + .../src/components/Topbar/UserInfo/index.tsx | 15 ++- .../console/src/components/Topbar/index.tsx | 10 +- .../src/containers/ConsoleRoutes/index.tsx | 6 +- .../console/src/contexts/TenantsProvider.tsx | 1 + .../Uploader/FileUploader/index.tsx | 17 ++- .../Uploader/ImageUploaderField/index.tsx | 2 +- .../src/hooks/use-console-routes/index.tsx | 2 - .../use-console-routes/routes/profile.tsx | 17 ++- .../src/hooks/use-user-assets-service.ts | 24 +++- .../BasicUserInfoUpdateModal/index.tsx | 8 +- .../src/pages/Profile/index.module.scss | 49 +++++--- packages/console/src/pages/Profile/index.tsx | 107 ++++++++++-------- packages/core/src/routes-me/init.ts | 2 + packages/core/src/routes-me/user-assets.ts | 103 +++++++++++++++++ 16 files changed, 289 insertions(+), 96 deletions(-) create mode 100644 packages/core/src/routes-me/user-assets.ts diff --git a/packages/console/src/cloud/AppRoutes.tsx b/packages/console/src/cloud/AppRoutes.tsx index 3230e90cc91..33b715526a3 100644 --- a/packages/console/src/cloud/AppRoutes.tsx +++ b/packages/console/src/cloud/AppRoutes.tsx @@ -1,8 +1,8 @@ -import { Route, Routes } from 'react-router-dom'; +import { Route, Routes, useRoutes } from 'react-router-dom'; -import { isCloud } from '@/consts/env'; import ProtectedRoutes from '@/containers/ProtectedRoutes'; import { GlobalAnonymousRoute, GlobalRoute } from '@/contexts/TenantsProvider'; +import { profile } from '@/hooks/use-console-routes/routes/profile'; import AcceptInvitation from '@/pages/AcceptInvitation'; import Callback from '@/pages/Callback'; import CheckoutSuccessCallback from '@/pages/CheckoutSuccessCallback'; @@ -13,18 +13,19 @@ import SocialDemoCallback from './pages/SocialDemoCallback'; /** Renders necessary routes when the user is not in a tenant context. */ function AppRoutes() { + const profileRoutes = useRoutes(profile); + return (
} /> } /> }> - {isCloud && ( - } - /> - )} + } + /> + {profileRoutes} } /> } /> diff --git a/packages/console/src/components/Topbar/UserInfo/index.module.scss b/packages/console/src/components/Topbar/UserInfo/index.module.scss index 489df1751f5..2d2582a0149 100644 --- a/packages/console/src/components/Topbar/UserInfo/index.module.scss +++ b/packages/console/src/components/Topbar/UserInfo/index.module.scss @@ -48,6 +48,11 @@ } .icon { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; color: var(--color-text-secondary); } diff --git a/packages/console/src/components/Topbar/UserInfo/index.tsx b/packages/console/src/components/Topbar/UserInfo/index.tsx index 7dd2a1c25a6..67dd709a2a9 100644 --- a/packages/console/src/components/Topbar/UserInfo/index.tsx +++ b/packages/console/src/components/Topbar/UserInfo/index.tsx @@ -5,12 +5,14 @@ import classNames from 'classnames'; import { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import ExternalLinkIcon from '@/assets/icons/external-link.svg'; import Globe from '@/assets/icons/globe.svg'; import Palette from '@/assets/icons/palette.svg'; import Profile from '@/assets/icons/profile.svg'; import SignOut from '@/assets/icons/sign-out.svg'; import UserAvatar from '@/components/UserAvatar'; import UserInfoCard from '@/components/UserInfoCard'; +import { isCloud } from '@/consts/env'; import Divider from '@/ds-components/Divider'; import Dropdown, { DropdownItem } from '@/ds-components/Dropdown'; import Spacer from '@/ds-components/Spacer'; @@ -28,7 +30,7 @@ import * as styles from './index.module.scss'; function UserInfo() { const { signOut } = useLogto(); - const { navigate } = useTenantPathname(); + const { getUrl } = useTenantPathname(); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { user, isLoading: isLoadingUser } = useCurrentUser(); const anchorRef = useRef(null); @@ -77,10 +79,19 @@ function UserInfo() { className={classNames(styles.dropdownItem, isLoading && styles.loading)} icon={} onClick={() => { - navigate('/profile'); + // In OSS version, there will be a `/console` context path in the URL. + const profileRouteWithConsoleContext = getUrl('/profile'); + + // Open the profile page in a new tab. In Logto Cloud, the profile page is not nested in the tenant independent, + // whereas in OSS version, it is under the `/console` context path. + window.open(isCloud ? '/profile' : profileRouteWithConsoleContext, '_blank'); }} > {t('menu.profile')} + +
+ +
- {isCloud && } - {!isCloud && ( + {isCloud && !hideTenantSelector && } + {!isCloud && !hideTitle && ( <>
{t('title')}
diff --git a/packages/console/src/containers/ConsoleRoutes/index.tsx b/packages/console/src/containers/ConsoleRoutes/index.tsx index 9dbb4d7df0c..87aaa43b878 100644 --- a/packages/console/src/containers/ConsoleRoutes/index.tsx +++ b/packages/console/src/containers/ConsoleRoutes/index.tsx @@ -1,5 +1,5 @@ import { ossConsolePath } from '@logto/schemas'; -import { Navigate, Outlet, Route, Routes } from 'react-router-dom'; +import { Navigate, Outlet, Route, Routes, useRoutes } from 'react-router-dom'; import { SWRConfig } from 'swr'; import { isCloud } from '@/consts/env'; @@ -10,6 +10,7 @@ import ProtectedRoutes from '@/containers/ProtectedRoutes'; import TenantAccess from '@/containers/TenantAccess'; import { GlobalRoute } from '@/contexts/TenantsProvider'; import Toast from '@/ds-components/Toast'; +import { profile } from '@/hooks/use-console-routes/routes/profile'; import useSwrOptions from '@/hooks/use-swr-options'; import Callback from '@/pages/Callback'; import CheckoutSuccessCallback from '@/pages/CheckoutSuccessCallback'; @@ -31,6 +32,8 @@ function Layout() { } export function ConsoleRoutes() { + const profileRoutes = useRoutes(profile); + return ( {/** @@ -44,6 +47,7 @@ export function ConsoleRoutes() { } /> }> } /> + {profileRoutes} }> {isCloud && ( = Object.freeze([ diff --git a/packages/console/src/ds-components/Uploader/FileUploader/index.tsx b/packages/console/src/ds-components/Uploader/FileUploader/index.tsx index bab632a0e5f..edafb2d325d 100644 --- a/packages/console/src/ds-components/Uploader/FileUploader/index.tsx +++ b/packages/console/src/ds-components/Uploader/FileUploader/index.tsx @@ -1,6 +1,7 @@ import type { AllowedUploadMimeType, UserAssets } from '@logto/schemas'; import { maxUploadFileSize } from '@logto/schemas'; import classNames from 'classnames'; +import { type KyInstance } from 'ky'; import { useCallback, useEffect, useState } from 'react'; import { type FileRejection, useDropzone } from 'react-dropzone'; import { useTranslation } from 'react-i18next'; @@ -20,6 +21,15 @@ export type Props = { readonly onCompleted: (fileUrl: string) => void; readonly onUploadErrorChange: (errorMessage?: string) => void; readonly className?: string; + /** + * Specify which API instance to use for the upload request. For example, you can use admin tenant API instead. + * Defaults to the return value of `useApi()`. + */ + readonly apiInstance?: KyInstance; + /** + * Specify the URL to upload the file to. Defaults to `api/user-assets`. + */ + readonly uploadUrl?: string; }; function FileUploader({ @@ -29,6 +39,8 @@ function FileUploader({ onCompleted, onUploadErrorChange, className, + apiInstance, + uploadUrl = 'api/user-assets', }: Props) { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const [isUploading, setIsUploading] = useState(false); @@ -91,7 +103,8 @@ function FileUploader({ try { setIsUploading(true); - const { url } = await api.post('api/user-assets', { body: formData }).json(); + const uploadApi = apiInstance ?? api; + const { url } = await uploadApi.post(uploadUrl, { body: formData }).json(); onCompleted(url); } catch { @@ -100,7 +113,7 @@ function FileUploader({ setIsUploading(false); } }, - [allowedMimeTypes, api, maxSize, onCompleted, t] + [api, apiInstance, allowedMimeTypes, maxSize, onCompleted, t, uploadUrl] ); const { getRootProps, getInputProps, isDragActive } = useDropzone({ diff --git a/packages/console/src/ds-components/Uploader/ImageUploaderField/index.tsx b/packages/console/src/ds-components/Uploader/ImageUploaderField/index.tsx index 02d079289f7..faaccc9ce62 100644 --- a/packages/console/src/ds-components/Uploader/ImageUploaderField/index.tsx +++ b/packages/console/src/ds-components/Uploader/ImageUploaderField/index.tsx @@ -9,7 +9,7 @@ import type { Props as ImageUploaderProps } from '../ImageUploader'; import * as styles from './index.module.scss'; -type Props = Pick & { +type Props = Omit & { readonly onChange: (value: string) => void; readonly allowedMimeTypes?: UserAssetsServiceStatus['allowUploadMimeTypes']; }; diff --git a/packages/console/src/hooks/use-console-routes/index.tsx b/packages/console/src/hooks/use-console-routes/index.tsx index 9739b14c4b2..92277b99fb7 100644 --- a/packages/console/src/hooks/use-console-routes/index.tsx +++ b/packages/console/src/hooks/use-console-routes/index.tsx @@ -23,7 +23,6 @@ import { customizeJwt } from './routes/customize-jwt'; import { enterpriseSso } from './routes/enterprise-sso'; import { organizationTemplate } from './routes/organization-template'; import { organizations } from './routes/organizations'; -import { profile } from './routes/profile'; import { roles } from './routes/roles'; import { signInExperience } from './routes/sign-in-experience'; import { useTenantSettings } from './routes/tenant-settings'; @@ -62,7 +61,6 @@ export const useConsoleRoutes = () => { { path: steps.organizationInfo, element: }, ], }, - profile, { path: 'signing-keys', element: }, isCloud && tenantSettings, isCloud && customizeJwt diff --git a/packages/console/src/hooks/use-console-routes/routes/profile.tsx b/packages/console/src/hooks/use-console-routes/routes/profile.tsx index a1644479ecc..ec8f2e6c6f4 100644 --- a/packages/console/src/hooks/use-console-routes/routes/profile.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/profile.tsx @@ -6,13 +6,10 @@ import LinkEmailModal from '@/pages/Profile/containers/LinkEmailModal'; import VerificationCodeModal from '@/pages/Profile/containers/VerificationCodeModal'; import VerifyPasswordModal from '@/pages/Profile/containers/VerifyPasswordModal'; -export const profile: RouteObject = { - path: 'profile', - children: [ - { index: true, element: }, - { path: 'verify-password', element: }, - { path: 'change-password', element: }, - { path: 'link-email', element: }, - { path: 'verification-code', element: }, - ], -}; +export const profile: RouteObject[] = [ + { index: true, element: }, + { path: 'verify-password', element: }, + { path: 'change-password', element: }, + { path: 'link-email', element: }, + { path: 'verification-code', element: }, +]; diff --git a/packages/console/src/hooks/use-user-assets-service.ts b/packages/console/src/hooks/use-user-assets-service.ts index e781ff149d8..4ad92bc47bd 100644 --- a/packages/console/src/hooks/use-user-assets-service.ts +++ b/packages/console/src/hooks/use-user-assets-service.ts @@ -1,11 +1,29 @@ -import type { UserAssetsServiceStatus } from '@logto/schemas'; +import { type UserAssetsServiceStatus } from '@logto/schemas'; +import { useLocation } from 'react-router-dom'; import useSWRImmutable from 'swr/immutable'; -import type { RequestError } from './use-api'; +import { adminTenantEndpoint, meApi } from '@/consts'; +import { isCloud } from '@/consts/env'; +import { GlobalRoute } from '@/contexts/TenantsProvider'; + +import useApi, { useStaticApi, type RequestError } from './use-api'; +import useSwrFetcher from './use-swr-fetcher'; const useUserAssetsService = () => { + const adminApi = useStaticApi({ + prefixUrl: adminTenantEndpoint, + resourceIndicator: meApi.indicator, + }); + const api = useApi(); + const { pathname } = useLocation(); + const isProfilePage = + pathname === GlobalRoute.Profile || pathname.startsWith(GlobalRoute.Profile + '/'); + const shouldUseAdminApi = isCloud && isProfilePage; + + const fetcher = useSwrFetcher(shouldUseAdminApi ? adminApi : api); const { data, error } = useSWRImmutable( - 'api/user-assets/service-status' + `${shouldUseAdminApi ? 'me' : 'api'}/user-assets/service-status`, + fetcher ); return { diff --git a/packages/console/src/pages/Profile/containers/BasicUserInfoUpdateModal/index.tsx b/packages/console/src/pages/Profile/containers/BasicUserInfoUpdateModal/index.tsx index 4f0e5b157c0..74b93ffb424 100644 --- a/packages/console/src/pages/Profile/containers/BasicUserInfoUpdateModal/index.tsx +++ b/packages/console/src/pages/Profile/containers/BasicUserInfoUpdateModal/index.tsx @@ -130,7 +130,13 @@ function BasicUserInfoUpdateModal({ field, value: initialValue, isOpen, onClose name="avatar" control={control} render={({ field: { onChange, value, name } }) => ( - + )} /> ) : ( diff --git a/packages/console/src/pages/Profile/index.module.scss b/packages/console/src/pages/Profile/index.module.scss index 5a963e324d7..26226a6afcd 100644 --- a/packages/console/src/pages/Profile/index.module.scss +++ b/packages/console/src/pages/Profile/index.module.scss @@ -1,24 +1,43 @@ @use '@/scss/underscore' as _; -.content { - margin-top: _.unit(4); - padding-bottom: _.unit(6); +.pageContainer { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + height: 100%; + + .scrollable { + width: 100%; + } + + .wrapper { + @include _.main-content-width; + width: 100%; + padding: _.unit(3) _.unit(6) 0; + } - > div + div { + .content { + width: 100%; margin-top: _.unit(4); + padding-bottom: _.unit(6); + + > div + div { + margin-top: _.unit(4); + } } -} -.deleteAccount { - flex: 1; - display: flex; - align-items: center; - border: 1px solid var(--color-divider); - border-radius: 8px; - padding: _.unit(4); + .deleteAccount { + flex: 1; + display: flex; + align-items: center; + border: 1px solid var(--color-divider); + border-radius: 8px; + padding: _.unit(4); - .description { - font: var(--font-body-2); - margin-right: _.unit(2); + .description { + font: var(--font-body-2); + margin-right: _.unit(2); + } } } diff --git a/packages/console/src/pages/Profile/index.tsx b/packages/console/src/pages/Profile/index.tsx index afc6d451d6d..7ba42539d1b 100644 --- a/packages/console/src/pages/Profile/index.tsx +++ b/packages/console/src/pages/Profile/index.tsx @@ -5,10 +5,12 @@ import useSWRImmutable from 'swr/immutable'; import FormCard from '@/components/FormCard'; import PageMeta from '@/components/PageMeta'; +import Topbar from '@/components/Topbar'; import { adminTenantEndpoint, meApi } from '@/consts'; import { isCloud } from '@/consts/env'; import Button from '@/ds-components/Button'; import CardTitle from '@/ds-components/CardTitle'; +import OverlayScrollbar from '@/ds-components/OverlayScrollbar'; import type { RequestError } from '@/hooks/use-api'; import { useStaticApi } from '@/hooks/use-api'; import useCurrentUser from '@/hooks/use-current-user'; @@ -42,58 +44,67 @@ function Profile() { const showLoadingSkeleton = isLoadingUser || isLoadingConnectors || isUserAssetServiceLoading; return ( -
- -
- -
- {showLoadingSkeleton && } - {user && !showLoadingSkeleton && ( -
- - {isCloud && } - - (value ? ******** : ), - action: { - name: 'profile.change', - handler: () => { - navigate(user.hasPassword ? 'verify-password' : 'change-password', { - state: { email: user.primaryEmail, action: 'changePassword' }, - }); +
+ + +
+ +
+ +
+ {showLoadingSkeleton && } + {user && !showLoadingSkeleton && ( +
+ + {isCloud && ( + + )} + + (value ? ******** : ), + action: { + name: 'profile.change', + handler: () => { + navigate(user.hasPassword ? 'verify-password' : 'change-password', { + state: { email: user.primaryEmail, action: 'changePassword' }, + }); + }, + }, }, - }, - }, - ]} - /> - - {isCloud && ( - -
-
{t('profile.delete_account.description')}
-
- { - setShowDeleteAccountModal(false); - }} - /> -
+ + {isCloud && ( + +
+
+ {t('profile.delete_account.description')} +
+
+ { + setShowDeleteAccountModal(false); + }} + /> +
+ )} +
)}
- )} +
); } diff --git a/packages/core/src/routes-me/init.ts b/packages/core/src/routes-me/init.ts index bff084f08df..a689b69727a 100644 --- a/packages/core/src/routes-me/init.ts +++ b/packages/core/src/routes-me/init.ts @@ -11,6 +11,7 @@ import type TenantContext from '#src/tenants/TenantContext.js'; import assertThat from '#src/utils/assert-that.js'; import socialRoutes from './social.js'; +import userAssetsRoutes from './user-assets.js'; import userRoutes from './user.js'; import verificationCodeRoutes from './verification-code.js'; @@ -36,6 +37,7 @@ export default function initMeApis(tenant: TenantContext): Koa { userRoutes(meRouter, tenant); socialRoutes(meRouter, tenant); verificationCodeRoutes(meRouter, tenant); + userAssetsRoutes(meRouter, tenant); const meApp = new Koa(); meApp.use(koaCors(EnvSet.values.cloudUrlSet)); diff --git a/packages/core/src/routes-me/user-assets.ts b/packages/core/src/routes-me/user-assets.ts new file mode 100644 index 00000000000..92948d47d8f --- /dev/null +++ b/packages/core/src/routes-me/user-assets.ts @@ -0,0 +1,103 @@ +import { readFile } from 'node:fs/promises'; + +import { consoleLog } from '@logto/cli/lib/utils.js'; +import { + userAssetsServiceStatusGuard, + allowUploadMimeTypes, + maxUploadFileSize, + type UserAssets, + userAssetsGuard, + adminTenantId, +} from '@logto/schemas'; +import { generateStandardId } from '@logto/shared'; +import { format } from 'date-fns'; +import { object } from 'zod'; + +import RequestError from '#src/errors/RequestError/index.js'; +import koaGuard from '#src/middleware/koa-guard.js'; +import type { RouterInitArgs } from '#src/routes/types.js'; +import SystemContext from '#src/tenants/SystemContext.js'; +import assertThat from '#src/utils/assert-that.js'; +import { uploadFileGuard } from '#src/utils/storage/consts.js'; +import { buildUploadFile } from '#src/utils/storage/index.js'; + +import type { AuthedMeRouter } from './types.js'; + +/** + * Duplicated from `/user-assets` management API and used specifically for admin tenant. + * E.g. Profile avatar upload. + */ +export default function userAssetsRoutes(...[router]: RouterInitArgs) { + router.get( + '/user-assets/service-status', + koaGuard({ + response: userAssetsServiceStatusGuard, + }), + async (ctx, next) => { + const { storageProviderConfig } = SystemContext.shared; + const status = storageProviderConfig + ? { + status: 'ready', + allowUploadMimeTypes, + maxUploadFileSize, + } + : { + status: 'not_configured', + }; + + ctx.body = status; + + return next(); + } + ); + + router.post( + '/user-assets', + koaGuard({ + files: object({ + file: uploadFileGuard, + }), + response: userAssetsGuard, + }), + async (ctx, next) => { + const { file } = ctx.guard.files; + + assertThat(file.size <= maxUploadFileSize, 'guard.file_size_exceeded'); + assertThat( + allowUploadMimeTypes.map(String).includes(file.mimetype), + 'guard.mime_type_not_allowed' + ); + + const { storageProviderConfig } = SystemContext.shared; + assertThat(storageProviderConfig, 'storage.not_configured'); + + const userId = ctx.auth.id; + const uploadFile = buildUploadFile(storageProviderConfig); + const objectKey = `${adminTenantId}/${userId}/${format( + new Date(), + 'yyyy/MM/dd' + )}/${generateStandardId(8)}/${file.originalFilename}`; + + try { + const { url } = await uploadFile(await readFile(file.filepath), objectKey, { + contentType: file.mimetype, + publicUrl: storageProviderConfig.publicUrl, + }); + + const result: UserAssets = { + url, + }; + + ctx.body = result; + } catch (error: unknown) { + consoleLog.error(error); + throw new RequestError({ + code: 'storage.upload_error', + status: 500, + }); + } + + return next(); + } + ); +} From fe31183333df300664256c887f67aaffdeea9a75 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Thu, 9 May 2024 18:52:10 +0800 Subject: [PATCH 2/3] refactor(console): profile routes --- packages/console/src/cloud/AppRoutes.tsx | 8 +++----- .../console/src/containers/ConsoleContent/index.tsx | 3 ++- .../console/src/containers/ConsoleRoutes/index.tsx | 12 ++++++------ packages/console/src/contexts/TenantsProvider.tsx | 2 +- .../src/hooks/use-console-routes/routes/profile.tsx | 2 -- .../console/src/hooks/use-user-assets-service.ts | 5 +++-- packages/console/src/pages/Profile/index.tsx | 7 +++++++ 7 files changed, 22 insertions(+), 17 deletions(-) diff --git a/packages/console/src/cloud/AppRoutes.tsx b/packages/console/src/cloud/AppRoutes.tsx index 33b715526a3..d97f388c13c 100644 --- a/packages/console/src/cloud/AppRoutes.tsx +++ b/packages/console/src/cloud/AppRoutes.tsx @@ -1,11 +1,11 @@ -import { Route, Routes, useRoutes } from 'react-router-dom'; +import { Route, Routes } from 'react-router-dom'; import ProtectedRoutes from '@/containers/ProtectedRoutes'; import { GlobalAnonymousRoute, GlobalRoute } from '@/contexts/TenantsProvider'; -import { profile } from '@/hooks/use-console-routes/routes/profile'; import AcceptInvitation from '@/pages/AcceptInvitation'; import Callback from '@/pages/Callback'; import CheckoutSuccessCallback from '@/pages/CheckoutSuccessCallback'; +import Profile from '@/pages/Profile'; import * as styles from './AppRoutes.module.scss'; import Main from './pages/Main'; @@ -13,8 +13,6 @@ import SocialDemoCallback from './pages/SocialDemoCallback'; /** Renders necessary routes when the user is not in a tenant context. */ function AppRoutes() { - const profileRoutes = useRoutes(profile); - return (
@@ -25,7 +23,7 @@ function AppRoutes() { path={`${GlobalRoute.AcceptInvitation}/:invitationId`} element={} /> - {profileRoutes} + } /> } /> } /> diff --git a/packages/console/src/containers/ConsoleContent/index.tsx b/packages/console/src/containers/ConsoleContent/index.tsx index 651982374c6..f1ae1cd5003 100644 --- a/packages/console/src/containers/ConsoleContent/index.tsx +++ b/packages/console/src/containers/ConsoleContent/index.tsx @@ -14,9 +14,10 @@ function ConsoleContent() { const { scrollableContent } = useOutletContext(); const routeObjects = useConsoleRoutes(); const routes = useRoutes(routeObjects); + usePlausiblePageview(routeObjects); + // Use this hook here to make sure console listens to user tenant scope changes. useTenantScopeListener(); - usePlausiblePageview(routeObjects); return (
diff --git a/packages/console/src/containers/ConsoleRoutes/index.tsx b/packages/console/src/containers/ConsoleRoutes/index.tsx index 87aaa43b878..45c0da00a05 100644 --- a/packages/console/src/containers/ConsoleRoutes/index.tsx +++ b/packages/console/src/containers/ConsoleRoutes/index.tsx @@ -1,5 +1,5 @@ import { ossConsolePath } from '@logto/schemas'; -import { Navigate, Outlet, Route, Routes, useRoutes } from 'react-router-dom'; +import { Navigate, Outlet, Route, Routes } from 'react-router-dom'; import { SWRConfig } from 'swr'; import { isCloud } from '@/consts/env'; @@ -8,12 +8,12 @@ import AppContent, { RedirectToFirstItem } from '@/containers/AppContent'; import ConsoleContent from '@/containers/ConsoleContent'; import ProtectedRoutes from '@/containers/ProtectedRoutes'; import TenantAccess from '@/containers/TenantAccess'; -import { GlobalRoute } from '@/contexts/TenantsProvider'; +import { GlobalAnonymousRoute, GlobalRoute } from '@/contexts/TenantsProvider'; import Toast from '@/ds-components/Toast'; -import { profile } from '@/hooks/use-console-routes/routes/profile'; import useSwrOptions from '@/hooks/use-swr-options'; import Callback from '@/pages/Callback'; import CheckoutSuccessCallback from '@/pages/CheckoutSuccessCallback'; +import Profile from '@/pages/Profile'; import HandleSocialCallback from '@/pages/Profile/containers/HandleSocialCallback'; import Welcome from '@/pages/Welcome'; import { dropLeadingSlash } from '@/utils/url'; @@ -32,8 +32,6 @@ function Layout() { } export function ConsoleRoutes() { - const profileRoutes = useRoutes(profile); - return ( {/** @@ -42,12 +40,14 @@ export function ConsoleRoutes() { * console path to trigger the console routes. */} {!isCloud && } />} + {!isCloud && ( + } /> + )} }> } /> } /> }> } /> - {profileRoutes} }> {isCloud && ( = Object.freeze([ diff --git a/packages/console/src/hooks/use-console-routes/routes/profile.tsx b/packages/console/src/hooks/use-console-routes/routes/profile.tsx index ec8f2e6c6f4..f4bb0268f08 100644 --- a/packages/console/src/hooks/use-console-routes/routes/profile.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/profile.tsx @@ -1,13 +1,11 @@ import { type RouteObject } from 'react-router-dom'; -import Profile from '@/pages/Profile'; import ChangePasswordModal from '@/pages/Profile/containers/ChangePasswordModal'; import LinkEmailModal from '@/pages/Profile/containers/LinkEmailModal'; import VerificationCodeModal from '@/pages/Profile/containers/VerificationCodeModal'; import VerifyPasswordModal from '@/pages/Profile/containers/VerifyPasswordModal'; export const profile: RouteObject[] = [ - { index: true, element: }, { path: 'verify-password', element: }, { path: 'change-password', element: }, { path: 'link-email', element: }, diff --git a/packages/console/src/hooks/use-user-assets-service.ts b/packages/console/src/hooks/use-user-assets-service.ts index 4ad92bc47bd..ff4a39bad48 100644 --- a/packages/console/src/hooks/use-user-assets-service.ts +++ b/packages/console/src/hooks/use-user-assets-service.ts @@ -4,7 +4,7 @@ import useSWRImmutable from 'swr/immutable'; import { adminTenantEndpoint, meApi } from '@/consts'; import { isCloud } from '@/consts/env'; -import { GlobalRoute } from '@/contexts/TenantsProvider'; +import { GlobalAnonymousRoute } from '@/contexts/TenantsProvider'; import useApi, { useStaticApi, type RequestError } from './use-api'; import useSwrFetcher from './use-swr-fetcher'; @@ -17,7 +17,8 @@ const useUserAssetsService = () => { const api = useApi(); const { pathname } = useLocation(); const isProfilePage = - pathname === GlobalRoute.Profile || pathname.startsWith(GlobalRoute.Profile + '/'); + pathname === GlobalAnonymousRoute.Profile || + pathname.startsWith(GlobalAnonymousRoute.Profile + '/'); const shouldUseAdminApi = isCloud && isProfilePage; const fetcher = useSwrFetcher(shouldUseAdminApi ? adminApi : api); diff --git a/packages/console/src/pages/Profile/index.tsx b/packages/console/src/pages/Profile/index.tsx index 7ba42539d1b..8d3a68116d1 100644 --- a/packages/console/src/pages/Profile/index.tsx +++ b/packages/console/src/pages/Profile/index.tsx @@ -1,6 +1,7 @@ import type { ConnectorResponse } from '@logto/schemas'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useRoutes } from 'react-router-dom'; import useSWRImmutable from 'swr/immutable'; import FormCard from '@/components/FormCard'; @@ -13,7 +14,9 @@ import CardTitle from '@/ds-components/CardTitle'; import OverlayScrollbar from '@/ds-components/OverlayScrollbar'; import type { RequestError } from '@/hooks/use-api'; import { useStaticApi } from '@/hooks/use-api'; +import { profile } from '@/hooks/use-console-routes/routes/profile'; import useCurrentUser from '@/hooks/use-current-user'; +import { usePlausiblePageview } from '@/hooks/use-plausible-pageview'; import useSwrFetcher from '@/hooks/use-swr-fetcher'; import useTenantPathname from '@/hooks/use-tenant-pathname'; import useUserAssetsService from '@/hooks/use-user-assets-service'; @@ -30,6 +33,9 @@ import * as styles from './index.module.scss'; function Profile() { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { navigate } = useTenantPathname(); + const childrenRoutes = useRoutes(profile); + usePlausiblePageview(profile); + const api = useStaticApi({ prefixUrl: adminTenantEndpoint, resourceIndicator: meApi.indicator }); const fetcher = useSwrFetcher(api); const { data: connectors, error: fetchConnectorsError } = useSWRImmutable< @@ -105,6 +111,7 @@ function Profile() { )}
+ {childrenRoutes}
); } From 1aa2fbb898defea01b7e9416183912b839cd27e8 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Thu, 9 May 2024 18:59:13 +0800 Subject: [PATCH 3/3] chore(core): refactor later --- packages/core/src/routes-me/user-assets.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/src/routes-me/user-assets.ts b/packages/core/src/routes-me/user-assets.ts index 92948d47d8f..c0139f51bc6 100644 --- a/packages/core/src/routes-me/user-assets.ts +++ b/packages/core/src/routes-me/user-assets.ts @@ -26,6 +26,8 @@ import type { AuthedMeRouter } from './types.js'; /** * Duplicated from `/user-assets` management API and used specifically for admin tenant. * E.g. Profile avatar upload. + * + * @todo: Refactor to reuse as much code as possible. @Charles */ export default function userAssetsRoutes(...[router]: RouterInitArgs) { router.get(