mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(console): update user access immediately on tenant role updates (#5720)
* feat(console): update user access immediately on tenant role updates * chore: improve comments Co-authored-by: Gao Sun <gao@silverhand.io> --------- Co-authored-by: Gao Sun <gao@silverhand.io>
This commit is contained in:
parent
75deb2db04
commit
59acedeecd
19 changed files with 164 additions and 100 deletions
|
@ -48,6 +48,6 @@
|
|||
"access": "public"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@logto/cloud": "0.2.5-ab8a489"
|
||||
"@logto/cloud": "0.2.5-821690c"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,13 +28,13 @@
|
|||
"@fontsource/roboto-mono": "^5.0.0",
|
||||
"@jest/types": "^29.5.0",
|
||||
"@logto/app-insights": "workspace:^1.4.0",
|
||||
"@logto/cloud": "0.2.5-94f7bcc",
|
||||
"@logto/cloud": "0.2.5-821690c",
|
||||
"@logto/connector-kit": "workspace:^3.0.0",
|
||||
"@logto/core-kit": "workspace:^2.4.0",
|
||||
"@logto/language-kit": "workspace:^1.1.0",
|
||||
"@logto/phrases": "workspace:^1.10.0",
|
||||
"@logto/phrases-experience": "workspace:^1.6.1",
|
||||
"@logto/react": "^3.0.5",
|
||||
"@logto/react": "^3.0.8",
|
||||
"@logto/schemas": "workspace:^1.15.0",
|
||||
"@logto/shared": "workspace:^3.1.0",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
|
|
59
packages/console/src/containers/ConsoleContent/hooks.ts
Normal file
59
packages/console/src/containers/ConsoleContent/hooks.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { Prompt, useLogto } from '@logto/react';
|
||||
import { getTenantOrganizationId } from '@logto/schemas';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import useCurrentTenantScopes from '@/hooks/use-current-tenant-scopes';
|
||||
import useRedirectUri from '@/hooks/use-redirect-uri';
|
||||
import { saveRedirect } from '@/utils/storage';
|
||||
|
||||
/**
|
||||
* Listens to the tenant scope changes for the current signed-in user. This hook will fetch the tenant scopes
|
||||
* for the user, and compare it with the "scope" token claim in access token. After comparing the scopes:
|
||||
*
|
||||
* - If the user has been granted new scopes, it will re-consent to obtain the additional scopes.
|
||||
* - If the user has been revoked scopes, it will clear the cached access token and renew one with shrunk scopes.
|
||||
*
|
||||
* Note: This hook should only be used once in the ConsoleContent component.
|
||||
*/
|
||||
const useTenantScopeListener = () => {
|
||||
const { currentTenantId } = useContext(TenantsContext);
|
||||
const { clearAccessToken, clearAllTokens, getOrganizationTokenClaims, signIn } = useLogto();
|
||||
const [tokenClaims, setTokenClaims] = useState<string[]>();
|
||||
const redirectUri = useRedirectUri();
|
||||
const { scopes = [], isLoading } = useCurrentTenantScopes();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const organizationId = getTenantOrganizationId(currentTenantId);
|
||||
const claims = await getOrganizationTokenClaims(organizationId);
|
||||
setTokenClaims(claims?.scope?.split(' ') ?? []);
|
||||
})();
|
||||
}, [currentTenantId, getOrganizationTokenClaims]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || tokenClaims === undefined) {
|
||||
return;
|
||||
}
|
||||
const hasScopesGranted = scopes.some((scope) => !tokenClaims.includes(scope));
|
||||
const hasScopesRevoked = tokenClaims.some((claim) => !scopes.includes(claim));
|
||||
if (hasScopesGranted) {
|
||||
(async () => {
|
||||
// User has been newly granted scopes. Need to re-consent to obtain the additional scopes.
|
||||
saveRedirect();
|
||||
await clearAllTokens();
|
||||
void signIn({
|
||||
redirectUri: redirectUri.href,
|
||||
prompt: Prompt.Consent,
|
||||
});
|
||||
})();
|
||||
}
|
||||
if (hasScopesRevoked) {
|
||||
// User has been revoked scopes. Need to clear the cached access token and it will be renewed
|
||||
// automatically with shrunk scopes.
|
||||
void clearAccessToken();
|
||||
}
|
||||
}, [clearAccessToken, clearAllTokens, isLoading, redirectUri.href, scopes, signIn, tokenClaims]);
|
||||
};
|
||||
|
||||
export default useTenantScopeListener;
|
|
@ -6,11 +6,14 @@ import { useConsoleRoutes } from '@/hooks/use-console-routes';
|
|||
import type { AppContentOutletContext } from '../AppContent/types';
|
||||
|
||||
import Sidebar from './Sidebar';
|
||||
import useTenantScopeListener from './hooks';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
function ConsoleContent() {
|
||||
const { scrollableContent } = useOutletContext<AppContentOutletContext>();
|
||||
const routes = useConsoleRoutes();
|
||||
// Use this hook here to make sure console listens to user tenant scope changes.
|
||||
useTenantScopeListener();
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
|
|
|
@ -14,7 +14,9 @@ import TenantMembers from '@/pages/TenantSettings/TenantMembers';
|
|||
|
||||
export const useTenantSettings = () => {
|
||||
const { isDevTenant } = useContext(TenantsContext);
|
||||
const { canManageTenant } = useCurrentTenantScopes();
|
||||
const {
|
||||
access: { canManageTenant },
|
||||
} = useCurrentTenantScopes();
|
||||
|
||||
const tenantSettings: RouteObject = useMemo(
|
||||
() => ({
|
||||
|
|
|
@ -1,61 +1,52 @@
|
|||
import { useLogto } from '@logto/react';
|
||||
import { TenantScope, getTenantOrganizationId } from '@logto/schemas';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { TenantScope } from '@logto/schemas';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { useAuthedCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
|
||||
import { type RequestError } from './use-api';
|
||||
import useCurrentUser from './use-current-user';
|
||||
|
||||
const useCurrentTenantScopes = () => {
|
||||
const { currentTenantId, isInitComplete } = useContext(TenantsContext);
|
||||
const { isAuthenticated, getOrganizationTokenClaims } = useLogto();
|
||||
const cloudApi = useAuthedCloudApi();
|
||||
const { user } = useCurrentUser();
|
||||
const userId = user?.id ?? '';
|
||||
|
||||
const [scopes, setScopes] = useState<string[]>([]);
|
||||
const [canInviteMember, setCanInviteMember] = useState(false);
|
||||
const [canRemoveMember, setCanRemoveMember] = useState(false);
|
||||
const [canUpdateMemberRole, setCanUpdateMemberRole] = useState(false);
|
||||
const [canManageTenant, setCanManageTenant] = useState(false);
|
||||
const {
|
||||
data: scopes,
|
||||
isLoading,
|
||||
mutate,
|
||||
} = useSWR<string[], RequestError>(
|
||||
userId && isInitComplete && `api/tenants/${currentTenantId}/members/${userId}/scopes`,
|
||||
async () => {
|
||||
const scopes = await cloudApi.get('/api/tenants/:tenantId/members/:userId/scopes', {
|
||||
params: { tenantId: currentTenantId, userId },
|
||||
});
|
||||
return scopes.map(({ name }) => name);
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (isAuthenticated && isInitComplete) {
|
||||
const organizationId = getTenantOrganizationId(currentTenantId);
|
||||
const claims = await getOrganizationTokenClaims(organizationId);
|
||||
const allScopes = claims?.scope?.split(' ') ?? [];
|
||||
setScopes(allScopes);
|
||||
const access = useMemo(
|
||||
() => ({
|
||||
canInviteMember: Boolean(scopes?.includes(TenantScope.InviteMember)),
|
||||
canRemoveMember: Boolean(scopes?.includes(TenantScope.RemoveMember)),
|
||||
canUpdateMemberRole: Boolean(scopes?.includes(TenantScope.UpdateMemberRole)),
|
||||
canManageTenant: Boolean(scopes?.includes(TenantScope.ManageTenant)),
|
||||
}),
|
||||
[scopes]
|
||||
);
|
||||
|
||||
for (const scope of allScopes) {
|
||||
switch (scope) {
|
||||
case TenantScope.InviteMember: {
|
||||
setCanInviteMember(true);
|
||||
break;
|
||||
}
|
||||
case TenantScope.RemoveMember: {
|
||||
setCanRemoveMember(true);
|
||||
break;
|
||||
}
|
||||
case TenantScope.UpdateMemberRole: {
|
||||
setCanUpdateMemberRole(true);
|
||||
break;
|
||||
}
|
||||
case TenantScope.ManageTenant: {
|
||||
setCanManageTenant(true);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [currentTenantId, getOrganizationTokenClaims, isAuthenticated, isInitComplete]);
|
||||
|
||||
return {
|
||||
canInviteMember,
|
||||
canRemoveMember,
|
||||
canUpdateMemberRole,
|
||||
canManageTenant,
|
||||
scopes,
|
||||
};
|
||||
return useMemo(
|
||||
() => ({
|
||||
isLoading,
|
||||
scopes,
|
||||
access,
|
||||
mutate,
|
||||
}),
|
||||
[isLoading, scopes, access, mutate]
|
||||
);
|
||||
};
|
||||
|
||||
export default useCurrentTenantScopes;
|
||||
|
|
|
@ -16,7 +16,9 @@ type Props = {
|
|||
};
|
||||
|
||||
function ProfileForm({ currentTenantId }: Props) {
|
||||
const { canManageTenant } = useCurrentTenantScopes();
|
||||
const {
|
||||
access: { canManageTenant },
|
||||
} = useCurrentTenantScopes();
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
|
|
|
@ -30,7 +30,9 @@ const tenantProfileToForm = (tenant?: TenantResponse): TenantSettingsForm => {
|
|||
|
||||
function TenantBasicSettings() {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { canManageTenant } = useCurrentTenantScopes();
|
||||
const {
|
||||
access: { canManageTenant },
|
||||
} = useCurrentTenantScopes();
|
||||
const api = useCloudApi();
|
||||
const {
|
||||
currentTenant,
|
||||
|
|
|
@ -23,7 +23,9 @@ function TenantDomainSettings() {
|
|||
const { data: customDomain, isLoading: isLoadingCustomDomain, mutate } = useCustomDomain(true);
|
||||
const { getDocumentationUrl } = useDocumentationUrl();
|
||||
const api = useApi();
|
||||
const { canManageTenant } = useCurrentTenantScopes();
|
||||
const {
|
||||
access: { canManageTenant },
|
||||
} = useCurrentTenantScopes();
|
||||
|
||||
if (isLoadingCustomDomain) {
|
||||
return <Skeleton />;
|
||||
|
|
|
@ -12,6 +12,7 @@ import FormField from '@/ds-components/FormField';
|
|||
import ModalLayout from '@/ds-components/ModalLayout';
|
||||
import Select, { type Option } from '@/ds-components/Select';
|
||||
import { useConfirmModal } from '@/hooks/use-confirm-modal';
|
||||
import useCurrentTenantScopes from '@/hooks/use-current-tenant-scopes';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
type Props = {
|
||||
|
@ -23,6 +24,7 @@ type Props = {
|
|||
function EditMemberModal({ user, isOpen, onClose }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.tenant_members' });
|
||||
const { currentTenantId } = useContext(TenantsContext);
|
||||
const { mutate: mutateUserTenantScopes } = useCurrentTenantScopes();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [role, setRole] = useState(TenantRole.Collaborator);
|
||||
|
@ -57,6 +59,7 @@ function EditMemberModal({ user, isOpen, onClose }: Props) {
|
|||
params: { tenantId: currentTenantId, userId: user.id },
|
||||
body: { roleName: role },
|
||||
});
|
||||
void mutateUserTenantScopes();
|
||||
onClose();
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
|
|
|
@ -53,7 +53,9 @@ function Invitations() {
|
|||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.tenant_members' });
|
||||
const cloudApi = useAuthedCloudApi();
|
||||
const { currentTenantId } = useContext(TenantsContext);
|
||||
const { canInviteMember, canRemoveMember } = useCurrentTenantScopes();
|
||||
const {
|
||||
access: { canInviteMember, canRemoveMember },
|
||||
} = useCurrentTenantScopes();
|
||||
|
||||
const { data, error, isLoading, mutate } = useSWR<TenantInvitationResponse[], RequestError>(
|
||||
`api/tenants/${currentTenantId}/invitations`,
|
||||
|
|
|
@ -24,7 +24,9 @@ function Members() {
|
|||
const cloudApi = useAuthedCloudApi();
|
||||
const { currentTenantId } = useContext(TenantsContext);
|
||||
const { user: currentUser } = useCurrentUser();
|
||||
const { canRemoveMember, canUpdateMemberRole } = useCurrentTenantScopes();
|
||||
const {
|
||||
access: { canRemoveMember, canUpdateMemberRole },
|
||||
} = useCurrentTenantScopes();
|
||||
|
||||
const { data, error, isLoading, mutate } = useSWR<TenantMemberResponse[], RequestError>(
|
||||
`api/tenants/${currentTenantId}/members`,
|
||||
|
|
|
@ -13,7 +13,9 @@ import { hasReachedQuotaLimit, hasSurpassedQuotaLimit } from '@/utils/quota';
|
|||
const useTenantMembersUsage = () => {
|
||||
const { currentPlan } = useContext(SubscriptionDataContext);
|
||||
const { currentTenantId } = useContext(TenantsContext);
|
||||
const { canInviteMember } = useCurrentTenantScopes();
|
||||
const {
|
||||
access: { canInviteMember },
|
||||
} = useCurrentTenantScopes();
|
||||
|
||||
const cloudApi = useAuthedCloudApi();
|
||||
|
||||
|
|
|
@ -30,7 +30,9 @@ function TenantMembers() {
|
|||
const { hasTenantMembersSurpassedLimit } = useTenantMembersUsage();
|
||||
const { navigate, match } = useTenantPathname();
|
||||
const [showInviteModal, setShowInviteModal] = useState(false);
|
||||
const { canInviteMember } = useCurrentTenantScopes();
|
||||
const {
|
||||
access: { canInviteMember },
|
||||
} = useCurrentTenantScopes();
|
||||
|
||||
const isInvitationTab = match(
|
||||
`/tenant-settings/${TenantSettingsTabs.Members}/${invitationsRoute}`
|
||||
|
|
|
@ -12,7 +12,9 @@ import * as styles from './index.module.scss';
|
|||
|
||||
function TenantSettings() {
|
||||
const { isDevTenant } = useContext(TenantsContext);
|
||||
const { canManageTenant } = useCurrentTenantScopes();
|
||||
const {
|
||||
access: { canManageTenant },
|
||||
} = useCurrentTenantScopes();
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
|
|
|
@ -92,7 +92,7 @@
|
|||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@logto/cloud": "0.2.5-94f7bcc",
|
||||
"@logto/cloud": "0.2.5-821690c",
|
||||
"@silverhand/eslint-config": "5.0.0",
|
||||
"@silverhand/ts-config": "5.0.0",
|
||||
"@types/debug": "^4.1.7",
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
"@logto/core-kit": "workspace:^2.4.0",
|
||||
"@logto/language-kit": "workspace:^1.1.0",
|
||||
"@logto/phrases": "workspace:^1.10.0",
|
||||
"@logto/react": "^3.0.5",
|
||||
"@logto/react": "^3.0.8",
|
||||
"@logto/schemas": "workspace:^1.15.0",
|
||||
"@parcel/core": "2.9.3",
|
||||
"@parcel/transformer-sass": "2.9.3",
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
"@logto/connector-kit": "workspace:^3.0.0",
|
||||
"@logto/core-kit": "workspace:^",
|
||||
"@logto/js": "^4.1.1",
|
||||
"@logto/node": "^2.4.4",
|
||||
"@logto/node": "^2.4.7",
|
||||
"@logto/schemas": "workspace:^1.15.0",
|
||||
"@logto/shared": "workspace:^3.1.0",
|
||||
"@silverhand/eslint-config": "5.0.0",
|
||||
|
|
|
@ -1235,8 +1235,8 @@ importers:
|
|||
version: 3.22.4
|
||||
devDependencies:
|
||||
'@logto/cloud':
|
||||
specifier: 0.2.5-ab8a489
|
||||
version: 0.2.5-ab8a489(zod@3.22.4)
|
||||
specifier: 0.2.5-821690c
|
||||
version: 0.2.5-821690c(zod@3.22.4)
|
||||
'@rollup/plugin-commonjs':
|
||||
specifier: ^25.0.0
|
||||
version: 25.0.7(rollup@4.12.0)
|
||||
|
@ -2715,8 +2715,8 @@ importers:
|
|||
specifier: workspace:^1.4.0
|
||||
version: link:../app-insights
|
||||
'@logto/cloud':
|
||||
specifier: 0.2.5-94f7bcc
|
||||
version: 0.2.5-94f7bcc(zod@3.22.4)
|
||||
specifier: 0.2.5-821690c
|
||||
version: 0.2.5-821690c(zod@3.22.4)
|
||||
'@logto/connector-kit':
|
||||
specifier: workspace:^3.0.0
|
||||
version: link:../toolkit/connector-kit
|
||||
|
@ -2733,8 +2733,8 @@ importers:
|
|||
specifier: workspace:^1.6.1
|
||||
version: link:../phrases-experience
|
||||
'@logto/react':
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5(react@18.2.0)
|
||||
specifier: ^3.0.8
|
||||
version: 3.0.8(react@18.2.0)
|
||||
'@logto/schemas':
|
||||
specifier: workspace:^1.15.0
|
||||
version: link:../schemas
|
||||
|
@ -3205,8 +3205,8 @@ importers:
|
|||
version: 3.22.4
|
||||
devDependencies:
|
||||
'@logto/cloud':
|
||||
specifier: 0.2.5-94f7bcc
|
||||
version: 0.2.5-94f7bcc(zod@3.22.4)
|
||||
specifier: 0.2.5-821690c
|
||||
version: 0.2.5-821690c(zod@3.22.4)
|
||||
'@silverhand/eslint-config':
|
||||
specifier: 5.0.0
|
||||
version: 5.0.0(eslint@8.44.0)(prettier@3.0.0)(typescript@5.3.3)
|
||||
|
@ -3319,8 +3319,8 @@ importers:
|
|||
specifier: workspace:^1.10.0
|
||||
version: link:../phrases
|
||||
'@logto/react':
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5(react@18.2.0)
|
||||
specifier: ^3.0.8
|
||||
version: 3.0.8(react@18.2.0)
|
||||
'@logto/schemas':
|
||||
specifier: workspace:^1.15.0
|
||||
version: link:../schemas
|
||||
|
@ -3638,8 +3638,8 @@ importers:
|
|||
specifier: ^4.1.1
|
||||
version: 4.1.1
|
||||
'@logto/node':
|
||||
specifier: ^2.4.4
|
||||
version: 2.4.4
|
||||
specifier: ^2.4.7
|
||||
version: 2.4.7
|
||||
'@logto/schemas':
|
||||
specifier: workspace:^1.15.0
|
||||
version: link:../schemas
|
||||
|
@ -7630,16 +7630,16 @@ packages:
|
|||
tiny-cookie: 2.4.1
|
||||
dev: false
|
||||
|
||||
/@logto/browser@2.2.7:
|
||||
resolution: {integrity: sha512-+tB4QWB4/JSO5pXItX491mRR4Id5dsYlEJchI0gPC8JNX7cl4968/oDXhqQ42XWqFnqX3W5Wx7RgKeV6JtTMhg==}
|
||||
/@logto/browser@2.2.10:
|
||||
resolution: {integrity: sha512-y6NauaxctqpfApccP6uFVmpg/vG1OhsDVLD4Pdpzbmj3whl63Nb17yxSTQHt4eYNKmSZJ2SzudAnMnVEYD91iQ==}
|
||||
dependencies:
|
||||
'@logto/client': 2.6.3
|
||||
'@logto/client': 2.6.6
|
||||
'@silverhand/essentials': 2.9.0
|
||||
js-base64: 3.7.5
|
||||
dev: true
|
||||
|
||||
/@logto/client@2.6.3:
|
||||
resolution: {integrity: sha512-uZphb17TZD2rXTiYfhPaIpiavMbUec+WwznIWIm2wJ9x4th8UO05egw9eTPiSaoEOZSuoPs6oWBROP1SQ00iBg==}
|
||||
/@logto/client@2.6.6:
|
||||
resolution: {integrity: sha512-QT7jMnzEIWHBNrf9/M8p1OErRBbbNZjoekXGji5aZCyUh975hh8+GEBL21HV71FT3H/5Cq4Gf1GzUbAIW3izMA==}
|
||||
dependencies:
|
||||
'@logto/js': 4.1.1
|
||||
'@silverhand/essentials': 2.9.0
|
||||
|
@ -7647,18 +7647,8 @@ packages:
|
|||
jose: 5.2.2
|
||||
dev: true
|
||||
|
||||
/@logto/cloud@0.2.5-94f7bcc(zod@3.22.4):
|
||||
resolution: {integrity: sha512-1nY3o1/gXgEIqgvjel2no0X3rR+BGnfozB7Vev+FY2qTkDyQIWRtHAnx+kkv4iEIIFcZW86LRNlvfjDUqR2yIg==}
|
||||
engines: {node: ^20.9.0}
|
||||
dependencies:
|
||||
'@silverhand/essentials': 2.9.0
|
||||
'@withtyped/server': 0.13.3(zod@3.22.4)
|
||||
transitivePeerDependencies:
|
||||
- zod
|
||||
dev: true
|
||||
|
||||
/@logto/cloud@0.2.5-ab8a489(zod@3.22.4):
|
||||
resolution: {integrity: sha512-nUD1n2CDe/nu6x4cOhXfJ5VyKKDqkKv+a/u9zSfbIMxIF0nShybd2LiCYJDO0SPuMqLnmlYFg+79KrdPCNvjIQ==}
|
||||
/@logto/cloud@0.2.5-821690c(zod@3.22.4):
|
||||
resolution: {integrity: sha512-eVTlJxknWbvmaeaitKzPPMTx6C4GK4TLTb97hFr91E2u6SwKP+csE3oMBgL7ZdoDLOGG+nY+j08JpVMQ8QdOWw==}
|
||||
engines: {node: ^20.9.0}
|
||||
dependencies:
|
||||
'@silverhand/essentials': 2.9.0
|
||||
|
@ -7674,20 +7664,20 @@ packages:
|
|||
camelcase-keys: 7.0.2
|
||||
dev: true
|
||||
|
||||
/@logto/node@2.4.4:
|
||||
resolution: {integrity: sha512-3qkhXQKGZX5cVBfWT6n2l0kN9ln3fPShXngHaY5LTBBRd0b2e20h1XIrXCdoGoMmdSp1zntEo2PMv0+fBodzcw==}
|
||||
/@logto/node@2.4.7:
|
||||
resolution: {integrity: sha512-AlANeqY1NIt93EBcRzrTmyAVHXOHpszTJK+qe1ok50rmZlTmX2p7yQvrg0/Ehwf/+4Rla5vooAR+HIFMaOmPpQ==}
|
||||
dependencies:
|
||||
'@logto/client': 2.6.3
|
||||
'@logto/client': 2.6.6
|
||||
'@silverhand/essentials': 2.9.0
|
||||
js-base64: 3.7.5
|
||||
dev: true
|
||||
|
||||
/@logto/react@3.0.5(react@18.2.0):
|
||||
resolution: {integrity: sha512-oCwKBGRf79QRo/MixPi8C8myZwHOx7eMon3/05nho0iiwBPllI2zSUJ7jUOnlFFnKTOLYV03l8pEMFnF+ODKyw==}
|
||||
/@logto/react@3.0.8(react@18.2.0):
|
||||
resolution: {integrity: sha512-p3pV4rX4g8ZwHQ159mxI+pP3Bwome47dNEmP1hI8/10WqdIPXGYTnfYn5c2l4Y2DyslYyK3ur2Sy4i4K6ept9A==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0 || ^18.0.0'
|
||||
dependencies:
|
||||
'@logto/browser': 2.2.7
|
||||
'@logto/browser': 2.2.10
|
||||
'@silverhand/essentials': 2.9.0
|
||||
react: 18.2.0
|
||||
dev: true
|
||||
|
|
Loading…
Reference in a new issue