Implement custom roles and improve permissions logic

The commit refactors the handling of account roles and enhances permissions checks. The account role has been shifted to use a string type, providing the ability to define custom roles. It also introduces the RolesDataProvider component, which stipulates role-related data for different forms and tables. The modification goes further to consider user role hierarchy in permissions checks, offering a more granular access control.
This commit is contained in:
giancarlo
2024-03-29 14:48:45 +08:00
parent f1967a686c
commit 99db8f4ca4
41 changed files with 498 additions and 228 deletions

View File

@@ -5,7 +5,7 @@ import { useState, useTransition } from 'react';
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
import { EmbeddedCheckout, PlanPicker } from '@kit/billing-gateway/components';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { Alert, AlertTitle } from '@kit/ui/alert';
import {
Card,
CardContent,
@@ -14,6 +14,7 @@ import {
CardTitle,
} from '@kit/ui/card';
import { If } from '@kit/ui/if';
import { Trans } from '@kit/ui/trans';
import billingConfig from '~/config/billing.config';
@@ -79,13 +80,11 @@ export function PersonalAccountCheckoutForm() {
function ErrorAlert() {
return (
<Alert variant={'destructive'}>
<ExclamationTriangleIcon />
<ExclamationTriangleIcon className={'h-4'} />
<AlertTitle>Sorry, we encountered an error.</AlertTitle>
<AlertDescription>
We couldn't process your request. Please try again.
</AlertDescription>
<AlertTitle>
<Trans i18nKey={'common:genericError'} />
</AlertTitle>
</Alert>
);
}

View File

@@ -12,6 +12,7 @@ import {
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { Trans } from '@kit/ui/trans';
import billingConfig from '~/config/billing.config';
@@ -36,9 +37,13 @@ export function TeamAccountCheckoutForm(params: { accountId: string }) {
return (
<Card>
<CardHeader>
<CardTitle>Manage your Team Plan</CardTitle>
<CardTitle>
<Trans i18nKey={'billing.manageTeamPlan'} />
</CardTitle>
<CardDescription>You can change your plan at any time.</CardDescription>
<CardDescription>
<Trans i18nKey={'billing.manageTeamPlanDescription'} />
</CardDescription>
</CardHeader>
<CardContent>

View File

@@ -3,6 +3,7 @@ import {
CurrentPlanCard,
} from '@kit/billing-gateway/components';
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { If } from '@kit/ui/if';
import { PageBody, PageHeader } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
@@ -34,6 +35,8 @@ async function TeamAccountBillingPage({ params }: Params) {
const workspace = await loadTeamWorkspace(params.account);
const accountId = workspace.account.id;
const [subscription, customerId] = await loadAccountData(accountId);
const canManageBilling =
workspace.account.permissions.includes('billing.manage');
return (
<>
@@ -44,17 +47,25 @@ async function TeamAccountBillingPage({ params }: Params) {
<PageBody>
<div className={'mx-auto w-full max-w-2xl'}>
<If condition={!canManageBilling}>
<CannotManageBillingAlert />
</If>
<div className={'flex flex-col space-y-4'}>
<If
condition={subscription}
fallback={<TeamAccountCheckoutForm accountId={accountId} />}
fallback={
<If condition={canManageBilling}>
<TeamAccountCheckoutForm accountId={accountId} />
</If>
}
>
{(data) => (
<CurrentPlanCard subscription={data} config={billingConfig} />
)}
</If>
<If condition={customerId}>
<If condition={customerId && canManageBilling}>
<form action={createBillingPortalSession}>
<input type="hidden" name={'accountId'} value={accountId} />
<input type="hidden" name={'slug'} value={params.account} />
@@ -71,6 +82,19 @@ async function TeamAccountBillingPage({ params }: Params) {
export default withI18n(TeamAccountBillingPage);
function CannotManageBillingAlert() {
return (
<Alert>
<AlertTitle>
<Trans i18nKey={'billing:cannotManageBillingAlertTitle'} />
</AlertTitle>
<AlertDescription>
<Trans i18nKey={'billing:cannotManageBillingAlertDescription'} />
</AlertDescription>
</Alert>
);
}
async function loadAccountData(accountId: string) {
const client = getSupabaseServerComponentClient();

View File

@@ -17,6 +17,7 @@ import {
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { If } from '@kit/ui/if';
import { PageBody, PageHeader } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
@@ -99,13 +100,10 @@ async function TeamAccountMembersPage({ params }: Params) {
);
const canManageRoles = account.permissions.includes('roles.manage');
const isPrimaryOwner = account.primary_owner_user_id === user.id;
const canManageInvitations = account.permissions.includes('invites.manage');
const permissions = {
canUpdateRole: canManageRoles,
canRemoveFromAccount: canManageRoles,
canTransferOwnership: isPrimaryOwner,
};
const isPrimaryOwner = account.primary_owner_user_id === user.id;
const currentUserRoleHierarchy = account.role_hierarchy_level;
return (
<>
@@ -126,23 +124,32 @@ async function TeamAccountMembersPage({ params }: Params) {
</CardTitle>
<CardDescription>
Here you can manage the members of your organization.
<Trans i18nKey={'common:membersTabDescription'} />
</CardDescription>
</div>
<InviteMembersDialogContainer account={account.slug}>
<Button size={'sm'}>
<PlusCircle className={'mr-2 w-4'} />
<span>Add Member</span>
</Button>
</InviteMembersDialogContainer>
<If condition={canManageInvitations}>
<InviteMembersDialogContainer
userRoleHierarchy={currentUserRoleHierarchy}
account={account.slug}
>
<Button size={'sm'}>
<PlusCircle className={'mr-2 w-4'} />
<span>
<Trans i18nKey={'teams:inviteMembersButton'} />
</span>
</Button>
</InviteMembersDialogContainer>
</If>
</CardHeader>
<CardContent>
<AccountMembersTable
userRoleHierarchy={currentUserRoleHierarchy}
currentUserId={user.id}
permissions={permissions}
members={members}
isPrimaryOwner={isPrimaryOwner}
canManageRoles={canManageRoles}
/>
</CardContent>
</Card>
@@ -150,11 +157,12 @@ async function TeamAccountMembersPage({ params }: Params) {
<Card>
<CardHeader className={'flex flex-row justify-between'}>
<div className={'flex flex-col space-y-1.5'}>
<CardTitle>Pending Invitations</CardTitle>
<CardTitle>
<Trans i18nKey={'teams:pendingInvitesHeading'} />
</CardTitle>
<CardDescription>
Here you can manage the pending invitations to your
organization.
<Trans i18nKey={'teams:pendingInvitesDescription'} />
</CardDescription>
</div>
</CardHeader>
@@ -164,6 +172,7 @@ async function TeamAccountMembersPage({ params }: Params) {
permissions={{
canUpdateInvitation: canManageRoles,
canRemoveInvitation: canManageRoles,
currentUserRoleHierarchy,
}}
invitations={invitations}
/>