Skip to content

Commit

Permalink
feat(console): update user access immediately on tenant role updates (#…
Browse files Browse the repository at this point in the history
…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>
  • Loading branch information
charIeszhao and gao-sun committed Apr 16, 2024
1 parent 75deb2d commit 59acede
Show file tree
Hide file tree
Showing 19 changed files with 164 additions and 100 deletions.
2 changes: 1 addition & 1 deletion packages/connectors/connector-logto-email/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,6 @@
"access": "public"
},
"devDependencies": {
"@logto/cloud": "0.2.5-ab8a489"
"@logto/cloud": "0.2.5-821690c"
}
}
4 changes: 2 additions & 2 deletions packages/console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
59 changes: 59 additions & 0 deletions packages/console/src/containers/ConsoleContent/hooks.ts
Original file line number Diff line number Diff line change
@@ -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;
3 changes: 3 additions & 0 deletions packages/console/src/containers/ConsoleContent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
() => ({
Expand Down
91 changes: 41 additions & 50 deletions packages/console/src/hooks/use-current-tenant-scopes.ts
Original file line number Diff line number Diff line change
@@ -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 [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 cloudApi = useAuthedCloudApi();
const { user } = useCurrentUser();
const userId = user?.id ?? '';

useEffect(() => {
(async () => {
if (isAuthenticated && isInitComplete) {
const organizationId = getTenantOrganizationId(currentTenantId);
const claims = await getOrganizationTokenClaims(organizationId);
const allScopes = claims?.scope?.split(' ') ?? [];
setScopes(allScopes);
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);
}
);

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]);
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]
);

return {
canInviteMember,
canRemoveMember,
canUpdateMemberRole,
canManageTenant,
scopes,
};
return useMemo(
() => ({
isLoading,
scopes,
access,
mutate,
}),
[isLoading, scopes, access, mutate]
);
};

export default useCurrentTenantScopes;
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ type Props = {
};

function ProfileForm({ currentTenantId }: Props) {
const { canManageTenant } = useCurrentTenantScopes();
const {
access: { canManageTenant },
} = useCurrentTenantScopes();
const {
register,
formState: { errors },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}`
Expand Down
4 changes: 3 additions & 1 deletion packages/console/src/pages/TenantSettings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}>
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/demo-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading

0 comments on commit 59acede

Please sign in to comment.