diff --git a/packages/console/src/contexts/TenantsProvider.tsx b/packages/console/src/contexts/TenantsProvider.tsx index 9d6d4a625..8d25cae96 100644 --- a/packages/console/src/contexts/TenantsProvider.tsx +++ b/packages/console/src/contexts/TenantsProvider.tsx @@ -17,6 +17,7 @@ export type Tenants = { setTenants: (tenants: TenantInfo[]) => void; setIsSettle: (isSettle: boolean) => void; currentTenantId: string; + setCurrentTenantId: (tenantId: string) => void; navigate: (tenantId: string, options?: NavigateOptions) => void; }; @@ -33,6 +34,7 @@ export const TenantsContext = createContext({ isSettle: false, setIsSettle: noop, currentTenantId: '', + setCurrentTenantId: noop, navigate: noop, }); @@ -52,7 +54,15 @@ function TenantsProvider({ children }: Props) { }, []); const memorizedContext = useMemo( - () => ({ tenants, setTenants, isSettle, setIsSettle, currentTenantId, navigate }), + () => ({ + tenants, + setTenants, + isSettle, + setIsSettle, + currentTenantId, + setCurrentTenantId, + navigate, + }), [currentTenantId, isSettle, navigate, tenants] ); diff --git a/packages/console/src/hooks/use-tenants.ts b/packages/console/src/hooks/use-tenants.ts new file mode 100644 index 000000000..7abdd5a4d --- /dev/null +++ b/packages/console/src/hooks/use-tenants.ts @@ -0,0 +1,117 @@ +import { useLogto } from '@logto/react'; +import { type PatchTenant, type CreateTenant, type TenantInfo } from '@logto/schemas'; +import { trySafe } from '@silverhand/essentials'; +import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; + +import { useCloudApi } from '@/cloud/hooks/use-cloud-api'; +import { TenantsContext } from '@/contexts/TenantsProvider'; + +const useTenants = () => { + const { signIn, getAccessToken } = useLogto(); + const cloudApi = useCloudApi(); + const { tenants, setTenants, currentTenantId, setCurrentTenantId, setIsSettle, navigate } = + useContext(TenantsContext); + const [error, setError] = useState(); + + const tryCatch = async (exec: Parameters[0]) => + trySafe(exec, (error) => { + setError(error instanceof Error ? error : new Error(String(error))); + }); + + const validate = useCallback( + async (tenant: TenantInfo) => { + const { id, indicator } = tenant; + if (await trySafe(getAccessToken(indicator))) { + setIsSettle(true); + } else { + void signIn(new URL(`/${id}/callback`, window.location.origin).toString()); + } + }, + [getAccessToken, setIsSettle, signIn] + ); + + const loadTenants = useCallback(async () => { + await tryCatch(async () => { + const availableTenants = await cloudApi.get('/api/tenants').json(); + setTenants(availableTenants); + }); + }, [cloudApi, setTenants]); + + const create = useCallback( + async (payload: Required>) => { + await tryCatch(async () => { + const createdTenant = await cloudApi + .post('/api/tenants', { json: payload }) + .json(); + const newTenants = [createdTenant, ...(tenants ?? [])]; + setTenants(newTenants); + }); + }, + [cloudApi, setTenants, tenants] + ); + + const update = useCallback( + async (payload: Required) => { + await tryCatch(async () => { + const updatedTenant = await cloudApi + .patch(`/api/tenants/${currentTenantId}`, { json: payload }) + .json(); + const index = tenants?.findIndex(({ id }) => id === currentTenantId); + if (index !== undefined && index !== -1) { + const updatedTenants = [ + ...(tenants ?? []).slice(0, index), + updatedTenant, + ...(tenants ?? []).slice(index + 1), + ]; + setTenants(updatedTenants); + } + }); + }, + [cloudApi, currentTenantId, setTenants, tenants] + ); + + /** `delete` is built-in property. */ + const remove = useCallback(async () => { + await tryCatch(async () => { + await cloudApi.delete(`/api/tenants/${currentTenantId}`); + const tenantsAfterDeletion = (tenants ?? []).filter(({ id }) => id !== currentTenantId); + setTenants(tenantsAfterDeletion); + setCurrentTenantId(''); + setIsSettle(false); + }); + }, [cloudApi, currentTenantId, setCurrentTenantId, setIsSettle, setTenants, tenants]); + + useEffect(() => { + if (!tenants) { + void loadTenants(); + } + }, [loadTenants, tenants]); + + const currentTenant = useMemo(() => { + return tenants?.find(({ id }) => id === currentTenantId); + }, [currentTenantId, tenants]); + + useEffect(() => { + if (currentTenant) { + void validate(currentTenant); + } + /** Fallback to the first available tenant. */ + if (tenants?.[0]) { + setCurrentTenantId(tenants[0].id); + navigate(tenants[0].id); + } + }, [currentTenant, navigate, setCurrentTenantId, tenants, validate]); + + return { + tenants, + currentTenantId, + currentTenant, + create, + update, + remove, + isLoaded: Boolean(tenants && !error), + error, + }; +}; + +export default useTenants;