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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Session, User } from '@supabase/supabase-js';
|
||||
import type { User } from '@supabase/supabase-js';
|
||||
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
|
||||
import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown';
|
||||
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
|
||||
import { useUser } from '@kit/supabase/hooks/use-user';
|
||||
import { useUserSession } from '@kit/supabase/hooks/use-user-session';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
@@ -23,7 +23,7 @@ export async function generateMetadata({
|
||||
const { title, date, description, image, slug } = post;
|
||||
const url = [appConfig.url, 'blog', slug].join('/');
|
||||
|
||||
return {
|
||||
return Promise.resolve({
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
@@ -46,7 +46,7 @@ export async function generateMetadata({
|
||||
description,
|
||||
images: image ? [image] : [],
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function BlogPost({ params }: { params: { slug: string } }) {
|
||||
|
||||
@@ -25,6 +25,7 @@ export const buildDocumentationTree = cache(
|
||||
.filter(
|
||||
(_) =>
|
||||
_.pathSegments.length === level + 1 &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
_.pathSegments
|
||||
.map(({ pathName }: { pathName: string }) => pathName)
|
||||
.join('/')
|
||||
@@ -37,6 +38,7 @@ export const buildDocumentationTree = cache(
|
||||
return pages.map((doc, index) => {
|
||||
const children = buildDocumentationTree(
|
||||
docs,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
doc.pathSegments.map(({ pathName }: { pathName: string }) => pathName),
|
||||
);
|
||||
|
||||
@@ -44,7 +46,7 @@ export const buildDocumentationTree = cache(
|
||||
...doc,
|
||||
pathSegments: doc.pathSegments || ([] as string[]),
|
||||
collapsible: children.length > 0,
|
||||
nextPage: children[0] || pages[index + 1],
|
||||
nextPage: children[0] ?? pages[index + 1],
|
||||
previousPage: pages[index - 1],
|
||||
children,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { DocumentationPage } from 'contentlayer/generated';
|
||||
import { allDocumentationPages } from 'contentlayer/generated';
|
||||
|
||||
import DocsNavigation from '~/(marketing)/docs/_components/docs-navigation';
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"build": "pnpm with-env next build",
|
||||
"clean": "git clean -xdf .next .turbo node_modules",
|
||||
"dev": "pnpm with-env next dev --turbo",
|
||||
"lint": "next lint",
|
||||
"next:lint": "next lint",
|
||||
"format": "prettier --check \"**/*.{js,cjs,mjs,ts,tsx,md,json}\"",
|
||||
"start": "pnpm with-env next start",
|
||||
"typecheck": "tsc --noEmit",
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
"checkoutSuccessTitle": "Done! You're all set.",
|
||||
"checkoutSuccessDescription": "Thank you for subscribing, we have successfully processed your subscription! A confirmation email will be sent to {{ customerEmail }}.",
|
||||
"checkoutSuccessBackButton": "Proceed to App",
|
||||
"cannotManageBillingAlertTitle": "You cannot manage billing",
|
||||
"cannotManageBillingAlertDescription": "You do not have permissions to manage billing. Please contact your organization owner.",
|
||||
"manageTeamPlan": "Manage your Team Plan",
|
||||
"manageTeamPlanDescription": "Choose a plan that fits your team's needs. You can upgrade or downgrade your plan at any time.",
|
||||
"status": {
|
||||
"free": {
|
||||
"badge": "Free Plan",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"primaryOwnerLabel": "Primary Owner",
|
||||
"joinedAtLabel": "Joined at",
|
||||
"invitedAtLabel": "Invited at",
|
||||
"membersTabSubheading": "Manage and Invite members",
|
||||
"membersTabDescription": "Here you can manage the members of your team.",
|
||||
"inviteMembersPageSubheading": "Invite members to your Team",
|
||||
"createTeamModalHeading": "Create Team",
|
||||
"createTeamModalDescription": "Create a new Team to manage your projects and members.",
|
||||
@@ -96,10 +96,10 @@
|
||||
"inviteMembersDescription": "Invite members to your team by entering their email and role.",
|
||||
"emailPlaceholder": "member@email.com",
|
||||
"membersPageHeading": "Members",
|
||||
"inviteMembersButtonLabel": "Invite Members",
|
||||
"inviteMembersButton": "Invite Members",
|
||||
"invitingMembers": "Inviting members...",
|
||||
"pendingInvitesHeading": "Pending Invites",
|
||||
"pendingInvitesSubheading": "Manage invites not yet accepted",
|
||||
"pendingInvitesDescription": " Here you can manage the pending invitations to your team.",
|
||||
"noPendingInvites": "No pending invites found",
|
||||
"loadingMembers": "Loading members...",
|
||||
"loadMembersError": "Sorry, we couldn't fetch your team's members.",
|
||||
|
||||
Reference in New Issue
Block a user