Skip to content

Commit

Permalink
refactor(console): update tenant settings access per user tenant scopes
Browse files Browse the repository at this point in the history
  • Loading branch information
charIeszhao committed Mar 28, 2024
1 parent 2ccc54a commit ac1da23
Show file tree
Hide file tree
Showing 11 changed files with 288 additions and 166 deletions.
60 changes: 35 additions & 25 deletions packages/console/src/components/ActionsButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import useActionTranslation from '@/hooks/use-action-translation';
import * as styles from './index.module.scss';

type Props = {
/** A function that will be called when the user confirms the deletion. */
onDelete: () => void | Promise<void>;
/** A function that will be called when the user confirms the deletion. If not provided,
* the delete button will not be displayed.
*/
onDelete?: () => void | Promise<void>;
/**
* A function that will be called when the user clicks the edit button. If not provided,
* the edit button will not be displayed.
Expand Down Expand Up @@ -48,6 +50,10 @@ function ActionsButton({ onDelete, onEdit, deleteConfirmation, fieldName, textOv
const [isDeleting, setIsDeleting] = useState(false);

const handleDelete = useCallback(async () => {
if (!onDelete) {
return;
}

setIsDeleting(true);
try {
await onDelete();
Expand All @@ -69,31 +75,35 @@ function ActionsButton({ onDelete, onEdit, deleteConfirmation, fieldName, textOv
)}
</ActionMenuItem>
)}
<ActionMenuItem
icon={<Delete />}
type="danger"
onClick={() => {
setIsModalOpen(true);
{onDelete && (
<ActionMenuItem
icon={<Delete />}
type="danger"
onClick={() => {
setIsModalOpen(true);
}}
>
{textOverrides?.delete ? (
<DynamicT forKey={textOverrides.delete} />
) : (
tAction('delete', fieldName)
)}
</ActionMenuItem>
)}
</ActionMenu>
{onDelete && (
<ConfirmModal
isOpen={isModalOpen}
confirmButtonText={textOverrides?.deleteConfirmation ?? 'general.delete'}
isLoading={isDeleting}
onCancel={() => {
setIsModalOpen(false);
}}
onConfirm={handleDelete}
>
{textOverrides?.delete ? (
<DynamicT forKey={textOverrides.delete} />
) : (
tAction('delete', fieldName)
)}
</ActionMenuItem>
</ActionMenu>
<ConfirmModal
isOpen={isModalOpen}
confirmButtonText={textOverrides?.deleteConfirmation ?? 'general.delete'}
isLoading={isDeleting}
onCancel={() => {
setIsModalOpen(false);
}}
onConfirm={handleDelete}
>
<DynamicT forKey={deleteConfirmation} />
</ConfirmModal>
<DynamicT forKey={deleteConfirmation} />
</ConfirmModal>
)}
</>
);
}
Expand Down
20 changes: 17 additions & 3 deletions packages/console/src/containers/ConsoleContent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { isCloud, isDevFeaturesEnabled } from '@/consts/env';
import { TenantsContext } from '@/contexts/TenantsProvider';
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
import useCurrentTenantScopes from '@/hooks/use-current-tenant-scopes';
import ApiResourceDetails from '@/pages/ApiResourceDetails';
import ApiResourcePermissions from '@/pages/ApiResourceDetails/ApiResourcePermissions';
import ApiResourceSettings from '@/pages/ApiResourceDetails/ApiResourceSettings';
Expand Down Expand Up @@ -74,6 +75,7 @@ import * as styles from './index.module.scss';
function ConsoleContent() {
const { scrollableContent } = useOutletContext<AppContentOutletContext>();
const { isDevTenant } = useContext(TenantsContext);
const { canManageTenant } = useCurrentTenantScopes();

return (
<div className={styles.content}>
Expand Down Expand Up @@ -197,13 +199,25 @@ function ConsoleContent() {
<Route path="signing-keys" element={<SigningKeys />} />
{isCloud && (
<Route path="tenant-settings" element={<TenantSettings />}>
<Route index element={<Navigate replace to={TenantSettingsTabs.Settings} />} />
<Route
index
element={
<Navigate
replace
to={
canManageTenant ? TenantSettingsTabs.Settings : TenantSettingsTabs.Members
}
/>
}
/>
<Route path={TenantSettingsTabs.Settings} element={<TenantBasicSettings />} />
{isDevFeaturesEnabled && (
<Route path={`${TenantSettingsTabs.Members}/*`} element={<TenantMembers />} />
)}
<Route path={TenantSettingsTabs.Domains} element={<TenantDomainSettings />} />
{!isDevTenant && (
{canManageTenant && (
<Route path={TenantSettingsTabs.Domains} element={<TenantDomainSettings />} />
)}
{!isDevTenant && canManageTenant && (
<>
<Route path={TenantSettingsTabs.Subscription} element={<Subscription />} />
<Route path={TenantSettingsTabs.BillingHistory} element={<BillingHistory />} />
Expand Down
61 changes: 61 additions & 0 deletions packages/console/src/hooks/use-current-tenant-scopes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useLogto } from '@logto/react';
import { TenantScope, getTenantOrganizationId } from '@logto/schemas';
import { useContext, useEffect, useState } from 'react';

import { TenantsContext } from '@/contexts/TenantsProvider';

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

useEffect(() => {
(async () => {
if (isAuthenticated && isInitComplete) {
const organizationId = getTenantOrganizationId(currentTenantId);
const claims = await getOrganizationTokenClaims(organizationId);
const allScopes = claims?.scope?.split(' ') ?? [];
setScopes(allScopes);

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,
};
};

export default useCurrentTenantScopes;
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import FormCard from '@/components/FormCard';
import CopyToClipboard from '@/ds-components/CopyToClipboard';
import FormField from '@/ds-components/FormField';
import TextInput from '@/ds-components/TextInput';
import useCurrentTenantScopes from '@/hooks/use-current-tenant-scopes';

import { type TenantSettingsForm } from '../types.js';

Expand All @@ -15,6 +16,7 @@ type Props = {
};

function ProfileForm({ currentTenantId }: Props) {
const { canManageTenant } = useCurrentTenantScopes();
const {
register,
formState: { errors },
Expand All @@ -29,6 +31,7 @@ function ProfileForm({ currentTenantId }: Props) {
<FormField isRequired title="tenants.settings.tenant_name">
<TextInput
{...register('profile.name', { required: true })}
readOnly={!canManageTenant}
error={Boolean(errors.profile?.name)}
/>
</FormField>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import SubmitFormChangesActionBar from '@/components/SubmitFormChangesActionBar'
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
import { TenantsContext } from '@/contexts/TenantsProvider';
import { useConfirmModal } from '@/hooks/use-confirm-modal';
import useCurrentTenantScopes from '@/hooks/use-current-tenant-scopes';
import { trySubmitSafe } from '@/utils/form';

import DeleteCard from './DeleteCard';
Expand All @@ -28,6 +29,7 @@ const tenantProfileToForm = (tenant?: TenantResponse): TenantSettingsForm => {

function TenantBasicSettings() {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { canManageTenant } = useCurrentTenantScopes();
const api = useCloudApi();
const {
currentTenant,
Expand Down Expand Up @@ -120,26 +122,34 @@ function TenantBasicSettings() {
<FormProvider {...methods}>
<div className={styles.fields}>
<ProfileForm currentTenantId={currentTenantId} />
<DeleteCard currentTenantId={currentTenantId} onClick={onClickDeletionButton} />
{canManageTenant && (
<DeleteCard currentTenantId={currentTenantId} onClick={onClickDeletionButton} />
)}
</div>
</FormProvider>
<SubmitFormChangesActionBar
isOpen={isDirty}
isSubmitting={isSubmitting}
onDiscard={reset}
onSubmit={onSubmit}
/>
{canManageTenant && (
<SubmitFormChangesActionBar
isOpen={isDirty}
isSubmitting={isSubmitting}
onDiscard={reset}
onSubmit={onSubmit}
/>
)}
</form>
<UnsavedChangesAlertModal hasUnsavedChanges={isDirty} />
<DeleteModal
isOpen={isDeletionModalOpen}
isLoading={isDeleting}
tenant={watch('profile')}
onClose={() => {
setIsDeletionModalOpen(false);
}}
onDelete={onDelete}
/>
{canManageTenant && (
<>
<UnsavedChangesAlertModal hasUnsavedChanges={isDirty} />
<DeleteModal
isOpen={isDeletionModalOpen}
isLoading={isDeleting}
tenant={watch('profile')}
onClose={() => {
setIsDeletionModalOpen(false);
}}
onDelete={onDelete}
/>
</>
)}
</>
);
}
Expand Down
Loading

0 comments on commit ac1da23

Please sign in to comment.