diff --git a/packages/cloud/package.json b/packages/cloud/package.json index de3a69cba..a2a0fee69 100644 --- a/packages/cloud/package.json +++ b/packages/cloud/package.json @@ -7,6 +7,9 @@ "license": "Elastic-2.0", "type": "module", "private": true, + "exports": { + "./routes": "./build/routes/index.js" + }, "imports": { "#src/*": "./build/*" }, @@ -18,6 +21,7 @@ "lint:report": "pnpm lint --format json --output-file report.json", "dev": "rm -rf build/ && nodemon", "start": "NODE_ENV=production node .", + "prepack": "pnpm build", "test:only": "NODE_OPTIONS=\"--experimental-vm-modules --max_old_space_size=4096\" jest --logHeapUsage", "test": "pnpm build:test && pnpm test:only", "test:ci": "pnpm test:only --coverage --silent" diff --git a/packages/cloud/tsconfig.json b/packages/cloud/tsconfig.json index 83e8d729c..423ae2c02 100644 --- a/packages/cloud/tsconfig.json +++ b/packages/cloud/tsconfig.json @@ -2,7 +2,7 @@ "extends": "@silverhand/ts-config/tsconfig.base", "compilerOptions": { "types": ["node", "jest"], - "declaration": false, + "declaration": true, "outDir": "build", "baseUrl": ".", "paths": { diff --git a/packages/console/package.json b/packages/console/package.json index b3d4fc604..7dbdeb95e 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -26,6 +26,7 @@ "@fontsource/roboto-mono": "^5.0.0", "@jest/types": "^29.5.0", "@logto/app-insights": "workspace:^1.2.0", + "@logto/cloud": "workspace:^", "@logto/connector-kit": "workspace:^1.1.1", "@logto/core-kit": "workspace:^2.0.0", "@logto/language-kit": "workspace:^1.0.0", @@ -60,6 +61,7 @@ "@types/react-helmet": "^6.1.6", "@types/react-modal": "^3.13.1", "@types/react-syntax-highlighter": "^15.5.1", + "@withtyped/client": "^0.7.10", "buffer": "^5.7.1", "classnames": "^2.3.1", "clean-deep": "^3.4.0", @@ -145,13 +147,16 @@ ], "import/no-unused-modules": [ "error", - { "unusedExports": true } + { + "unusedExports": true + } ] }, "overrides": [ { "files": [ - "*.d.ts", "**/mdx-components/*/index.tsx" + "*.d.ts", + "**/mdx-components/*/index.tsx" ], "rules": { "import/no-unused-modules": "off" diff --git a/packages/console/src/cloud/hooks/use-cloud-api.ts b/packages/console/src/cloud/hooks/use-cloud-api.ts index 4725b653b..7478b08c4 100644 --- a/packages/console/src/cloud/hooks/use-cloud-api.ts +++ b/packages/console/src/cloud/hooks/use-cloud-api.ts @@ -1,23 +1,20 @@ +import type router from '@logto/cloud/routes'; import { useLogto } from '@logto/react'; -import ky from 'ky'; +import Client from '@withtyped/client'; import { useMemo } from 'react'; import { cloudApi } from '@/consts'; -export const useCloudApi = () => { +export const useCloudApi = (): Client => { const { isAuthenticated, getAccessToken } = useLogto(); const api = useMemo( () => - ky.create({ - hooks: { - beforeRequest: [ - async (request) => { - if (isAuthenticated) { - const accessToken = await getAccessToken(cloudApi.indicator); - request.headers.set('Authorization', `Bearer ${accessToken ?? ''}`); - } - }, - ], + new Client({ + baseUrl: window.location.origin, + headers: async () => { + if (isAuthenticated) { + return { Authorization: `Bearer ${(await getAccessToken(cloudApi.indicator)) ?? ''}` }; + } }, }), [getAccessToken, isAuthenticated] diff --git a/packages/console/src/cloud/hooks/use-cloud-swr.ts b/packages/console/src/cloud/hooks/use-cloud-swr.ts new file mode 100644 index 000000000..456094db0 --- /dev/null +++ b/packages/console/src/cloud/hooks/use-cloud-swr.ts @@ -0,0 +1,23 @@ +import type router from '@logto/cloud/routes'; +import { type RouterRoutes } from '@withtyped/client'; +import useSWR from 'swr'; + +import { useCloudApi } from './use-cloud-api'; + +type GetRoutes = RouterRoutes['get']; + +const normalizeError = (error: unknown) => { + if (error === undefined || error === null) { + return; + } + + return error instanceof Error ? error : new Error(String(error)); +}; + +export const useCloudSwr = (key: Key) => { + const cloudApi = useCloudApi(); + const response = useSWR(key, async () => cloudApi.get(key)); + + // By default, `useSWR()` uses `any` for the error type which is unexpected under our lint rule set. + return { ...response, error: normalizeError(response.error) }; +}; diff --git a/packages/console/src/cloud/pages/Main/CreateTenantModal/index.tsx b/packages/console/src/cloud/pages/Main/CreateTenantModal/index.tsx index d1a69b7db..3ad6f1bd8 100644 --- a/packages/console/src/cloud/pages/Main/CreateTenantModal/index.tsx +++ b/packages/console/src/cloud/pages/Main/CreateTenantModal/index.tsx @@ -57,9 +57,8 @@ function CreateTenantModal({ isOpen, onClose }: Props) { const onSubmit = handleSubmit(async (data) => { try { const { name, tag } = data; - const newTenant = await cloudApi - .post('/api/tenants', { json: { name, tag } }) - .json(); + const newTenant = await cloudApi.post('/api/tenants', { body: { name, tag } }); + onClose(newTenant); } catch (error: unknown) { toast.error(error instanceof Error ? error.message : String(error)); diff --git a/packages/console/src/cloud/pages/Main/Tenants.tsx b/packages/console/src/cloud/pages/Main/Tenants.tsx index e821f93c7..5efb79722 100644 --- a/packages/console/src/cloud/pages/Main/Tenants.tsx +++ b/packages/console/src/cloud/pages/Main/Tenants.tsx @@ -24,9 +24,7 @@ function Tenants({ data, onAdd }: Props) { * `name` and `tag` are required for POST /tenants API, add fixed value to avoid throwing error. * This page page will be removed in upcoming changes on multi-tenancy cloud console. */ - await api - .post('api/tenants', { json: { name: 'My Project', tag: TenantTag.Development } }) - .json() + await api.post('/api/tenants', { body: { name: 'My Project', tag: TenantTag.Development } }) ); }, [api, onAdd]); diff --git a/packages/console/src/cloud/pages/Main/index.tsx b/packages/console/src/cloud/pages/Main/index.tsx index 03a594fdb..e3045b4e4 100644 --- a/packages/console/src/cloud/pages/Main/index.tsx +++ b/packages/console/src/cloud/pages/Main/index.tsx @@ -1,5 +1,4 @@ import { useLogto } from '@logto/react'; -import type { TenantInfo } from '@logto/schemas/models'; import { conditional, yes } from '@silverhand/essentials'; import { HTTPError } from 'ky'; import { useContext, useEffect, useState } from 'react'; @@ -25,7 +24,7 @@ function Protected() { setError(undefined); try { - const data = await api.get('/api/tenants').json(); + const data = await api.get('/api/tenants'); setTenants(data); } catch (error: unknown) { setError(error instanceof Error ? error : new Error(String(error))); diff --git a/packages/console/src/hooks/use-swr-fetcher.ts b/packages/console/src/hooks/use-swr-fetcher.ts index 4b9933ff0..9c278cad3 100644 --- a/packages/console/src/hooks/use-swr-fetcher.ts +++ b/packages/console/src/hooks/use-swr-fetcher.ts @@ -1,8 +1,8 @@ -import { HTTPError, type Options } from 'ky'; +import { HTTPError } from 'ky'; import type ky from 'ky'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import type { BareFetcher } from 'swr'; +import type { Fetcher } from 'swr'; import { RequestError } from './use-api'; @@ -10,18 +10,18 @@ type KyInstance = typeof ky; type WithTotalNumber = Array | number>; -type useSwrFetcherHook = { - (api: KyInstance): BareFetcher; - (api: KyInstance): BareFetcher>; +type UseSwrFetcherHook = { + (api: KyInstance): Fetcher; + (api: KyInstance): Fetcher>; }; -const useSwrFetcher: useSwrFetcherHook = (api: KyInstance) => { +const useSwrFetcher: UseSwrFetcherHook = (api: KyInstance) => { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const fetcher = useCallback>>( - async (resource: string | URL, init: Options) => { + const fetcher = useCallback>>( + async (resource: string) => { try { - const response = await api.get(resource, init); + const response = await api.get(resource); const data = await response.json(); if (typeof resource === 'string' && resource.includes('?')) { diff --git a/packages/console/src/hooks/use-tenants.ts b/packages/console/src/hooks/use-tenants.ts index 0d04463b4..9dd57abf1 100644 --- a/packages/console/src/hooks/use-tenants.ts +++ b/packages/console/src/hooks/use-tenants.ts @@ -1,19 +1,13 @@ import { useLogto } from '@logto/react'; import { type TenantInfo } from '@logto/schemas/models'; import { type Optional, trySafe } from '@silverhand/essentials'; -import type ky from 'ky'; import { useCallback, useContext, useEffect, useMemo } from 'react'; -import useSWR, { type KeyedMutator } from 'swr'; +import { type KeyedMutator } from 'swr'; -import { useCloudApi } from '@/cloud/hooks/use-cloud-api'; +import { useCloudSwr } from '@/cloud/hooks/use-cloud-swr'; import { TenantsContext } from '@/contexts/TenantsProvider'; -import useSwrFetcher from './use-swr-fetcher'; - -type KyInstance = typeof ky; - type TenantsHook = { - api: KyInstance; currentTenant?: TenantInfo; currentTenantId: string; error?: Error; @@ -26,16 +20,9 @@ type TenantsHook = { }; const useTenants = (): TenantsHook => { - const cloudApi = useCloudApi(); const { signIn, getAccessToken } = useLogto(); const { currentTenantId, setCurrentTenantId, isSettle, setIsSettle } = useContext(TenantsContext); - - const fetcher = useSwrFetcher(cloudApi); - const { - data: availableTenants, - error, - mutate, - } = useSWR('/api/tenants', fetcher); + const { data: availableTenants, error, mutate } = useCloudSwr('/api/tenants'); const isLoading = !availableTenants && !error; const isLoaded = Boolean(availableTenants && !error); @@ -62,7 +49,6 @@ const useTenants = (): TenantsHook => { }, [currentTenant, validate]); return { - api: cloudApi, currentTenant, currentTenantId, error, diff --git a/packages/console/src/pages/TenantSettings/TenantBasicSettings/index.tsx b/packages/console/src/pages/TenantSettings/TenantBasicSettings/index.tsx index 7c38219c8..42457c762 100644 --- a/packages/console/src/pages/TenantSettings/TenantBasicSettings/index.tsx +++ b/packages/console/src/pages/TenantSettings/TenantBasicSettings/index.tsx @@ -3,6 +3,7 @@ import classNames from 'classnames'; import { useEffect, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; +import { useCloudApi } from '@/cloud/hooks/use-cloud-api'; import AppError from '@/components/AppError'; import AppLoading from '@/components/AppLoading'; import PageMeta from '@/components/PageMeta'; @@ -23,8 +24,8 @@ const tenantProfileToForm = (tenant?: TenantInfo): TenantSettingsForm => { }; function TenantBasicSettings() { + const api = useCloudApi(); const { - api: cloudApi, currentTenant, currentTenantId, error: requestError, @@ -58,11 +59,10 @@ function TenantBasicSettings() { const saveData = async (data: { name?: string; tag?: TenantTag }) => { try { - const { name, tag } = await cloudApi - .patch(`/api/tenants/${currentTenantId}`, { - json: data, - }) - .json(); + const { name, tag } = await api.patch(`/api/tenants/:tenantId`, { + params: { tenantId: currentTenantId }, + body: data, + }); reset({ profile: { name, tag } }); void mutate(); } catch (error: unknown) { @@ -96,7 +96,7 @@ function TenantBasicSettings() { setIsDeleting(true); try { - await cloudApi.delete(`/api/tenants/${currentTenantId}`); + await api.delete(`/api/tenants/:tenantId`, { params: { tenantId: currentTenantId } }); setIsDeletionModalOpen(false); await mutate(); if (tenants?.[0]?.id) { diff --git a/packages/integration-tests/src/tests/ui-cloud/smoke.test.ts b/packages/integration-tests/src/tests/ui-cloud/smoke.test.ts index 02e8be852..2dd8b3967 100644 --- a/packages/integration-tests/src/tests/ui-cloud/smoke.test.ts +++ b/packages/integration-tests/src/tests/ui-cloud/smoke.test.ts @@ -171,6 +171,8 @@ describe('smoke testing for cloud', () => { await expect(page).toClick('button[name=submit]'); await page.waitForNavigation({ waitUntil: 'networkidle0' }); + console.log('???', page.url()); + expect(page.url().startsWith(logtoCloudUrl.href)).toBeTruthy(); expect(new URL(page.url()).pathname.endsWith('/onboarding/welcome')).toBeTruthy(); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40abb9ad8..adf36693f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2775,6 +2775,9 @@ importers: '@logto/app-insights': specifier: workspace:^1.2.0 version: link:../app-insights + '@logto/cloud': + specifier: workspace:^ + version: link:../cloud '@logto/connector-kit': specifier: workspace:^1.1.1 version: link:../toolkit/connector-kit @@ -2877,6 +2880,9 @@ importers: '@types/react-syntax-highlighter': specifier: ^15.5.1 version: 15.5.1 + '@withtyped/client': + specifier: ^0.7.10 + version: 0.7.10(zod@3.20.2) buffer: specifier: ^5.7.1 version: 5.7.1 @@ -9808,6 +9814,15 @@ packages: eslint-visitor-keys: 3.3.0 dev: true + /@withtyped/client@0.7.10(zod@3.20.2): + resolution: {integrity: sha512-8EndTFr1+V6WbAW1kldxPZztk4uIbmoBnfWKG1zvVC6TTPwEOiB3SP3BsjiW8si7wUmO8GWjazKqnq5CYlSjGg==} + dependencies: + '@withtyped/server': 0.11.1(zod@3.20.2) + '@withtyped/shared': 0.2.0 + transitivePeerDependencies: + - zod + dev: true + /@withtyped/postgres@0.11.0(@withtyped/server@0.11.1): resolution: {integrity: sha512-PHnx6ake/MDdyy4sZXS/7l5XNBtjqlPSqSHrlmCYUXYxUV0sHSrXECKxX7deAvWZtcHVh9VaWEpiQBhFS06Vig==} peerDependencies: @@ -9829,11 +9844,9 @@ packages: '@silverhand/essentials': 2.6.2 '@withtyped/shared': 0.2.0 zod: 3.20.2 - dev: false /@withtyped/shared@0.2.0: resolution: {integrity: sha512-SADIVEospfIWAVK0LxX7F1T04hsWMZ0NkfR3lNfvJqOktJ52GglI3FOTVYOM1NJYReDT6pR0XFlCfaF8TVPt8w==} - dev: false /@xmldom/xmldom@0.8.7: resolution: {integrity: sha512-sI1Ly2cODlWStkINzqGrZ8K6n+MTSbAeQnAipGyL+KZCXuHaRlj2gyyy8B/9MvsFFqN7XHryQnB2QwhzvJXovg==}