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:
@@ -20,11 +20,13 @@ The roadmap for the project is as follows:
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Authentication**: Sign up, sign in, sign out, forgot password, update profile, and more.
|
- **Authentication**: Sign up, sign in, sign out, forgot password, update profile, and more.
|
||||||
- **Billing**: Subscription management, payment methods, invoices, and more.
|
- **Billing**: Subscription management, one-off payments, flat subscriptions, per-seat subscriptions, and more.
|
||||||
- **Personal Account**: Manage your account, profile picture, and more.
|
- **Personal Account**: Manage your account, profile picture, and more.
|
||||||
- **Team Accounts**: Invite members, manage roles, and more.
|
- **Team Accounts**: Invite members, manage roles, and more. Manage resources within a team.
|
||||||
|
- **RBAC**: Simple-to-use role-based access control. Customize roles and permissions (coming soon).
|
||||||
- **Admin Dashboard**: Manage users, subscriptions, and more.
|
- **Admin Dashboard**: Manage users, subscriptions, and more.
|
||||||
- **Pluggable**: Easily add new features and packages to your SaaS application.
|
- **Pluggable**: Easily add new features and packages to your SaaS application.
|
||||||
|
- **Super UI**: Beautiful UI using Shadcn UI and Tailwind CSS.
|
||||||
|
|
||||||
The most notable difference between this version and the original version is the use of Turborepo to manage multiple packages in a single repository.
|
The most notable difference between this version and the original version is the use of Turborepo to manage multiple packages in a single repository.
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useState, useTransition } from 'react';
|
|||||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||||
|
|
||||||
import { EmbeddedCheckout, PlanPicker } from '@kit/billing-gateway/components';
|
import { EmbeddedCheckout, PlanPicker } from '@kit/billing-gateway/components';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
import { Alert, AlertTitle } from '@kit/ui/alert';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@kit/ui/card';
|
} from '@kit/ui/card';
|
||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import billingConfig from '~/config/billing.config';
|
import billingConfig from '~/config/billing.config';
|
||||||
|
|
||||||
@@ -79,13 +80,11 @@ export function PersonalAccountCheckoutForm() {
|
|||||||
function ErrorAlert() {
|
function ErrorAlert() {
|
||||||
return (
|
return (
|
||||||
<Alert variant={'destructive'}>
|
<Alert variant={'destructive'}>
|
||||||
<ExclamationTriangleIcon />
|
<ExclamationTriangleIcon className={'h-4'} />
|
||||||
|
|
||||||
<AlertTitle>Sorry, we encountered an error.</AlertTitle>
|
<AlertTitle>
|
||||||
|
<Trans i18nKey={'common:genericError'} />
|
||||||
<AlertDescription>
|
</AlertTitle>
|
||||||
We couldn't process your request. Please try again.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@kit/ui/card';
|
} from '@kit/ui/card';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import billingConfig from '~/config/billing.config';
|
import billingConfig from '~/config/billing.config';
|
||||||
|
|
||||||
@@ -36,9 +37,13 @@ export function TeamAccountCheckoutForm(params: { accountId: string }) {
|
|||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<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>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
CurrentPlanCard,
|
CurrentPlanCard,
|
||||||
} from '@kit/billing-gateway/components';
|
} from '@kit/billing-gateway/components';
|
||||||
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
@@ -34,6 +35,8 @@ async function TeamAccountBillingPage({ params }: Params) {
|
|||||||
const workspace = await loadTeamWorkspace(params.account);
|
const workspace = await loadTeamWorkspace(params.account);
|
||||||
const accountId = workspace.account.id;
|
const accountId = workspace.account.id;
|
||||||
const [subscription, customerId] = await loadAccountData(accountId);
|
const [subscription, customerId] = await loadAccountData(accountId);
|
||||||
|
const canManageBilling =
|
||||||
|
workspace.account.permissions.includes('billing.manage');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -44,17 +47,25 @@ async function TeamAccountBillingPage({ params }: Params) {
|
|||||||
|
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<div className={'mx-auto w-full max-w-2xl'}>
|
<div className={'mx-auto w-full max-w-2xl'}>
|
||||||
|
<If condition={!canManageBilling}>
|
||||||
|
<CannotManageBillingAlert />
|
||||||
|
</If>
|
||||||
|
|
||||||
<div className={'flex flex-col space-y-4'}>
|
<div className={'flex flex-col space-y-4'}>
|
||||||
<If
|
<If
|
||||||
condition={subscription}
|
condition={subscription}
|
||||||
fallback={<TeamAccountCheckoutForm accountId={accountId} />}
|
fallback={
|
||||||
|
<If condition={canManageBilling}>
|
||||||
|
<TeamAccountCheckoutForm accountId={accountId} />
|
||||||
|
</If>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{(data) => (
|
{(data) => (
|
||||||
<CurrentPlanCard subscription={data} config={billingConfig} />
|
<CurrentPlanCard subscription={data} config={billingConfig} />
|
||||||
)}
|
)}
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
<If condition={customerId}>
|
<If condition={customerId && canManageBilling}>
|
||||||
<form action={createBillingPortalSession}>
|
<form action={createBillingPortalSession}>
|
||||||
<input type="hidden" name={'accountId'} value={accountId} />
|
<input type="hidden" name={'accountId'} value={accountId} />
|
||||||
<input type="hidden" name={'slug'} value={params.account} />
|
<input type="hidden" name={'slug'} value={params.account} />
|
||||||
@@ -71,6 +82,19 @@ async function TeamAccountBillingPage({ params }: Params) {
|
|||||||
|
|
||||||
export default withI18n(TeamAccountBillingPage);
|
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) {
|
async function loadAccountData(accountId: string) {
|
||||||
const client = getSupabaseServerComponentClient();
|
const client = getSupabaseServerComponentClient();
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@kit/ui/card';
|
} from '@kit/ui/card';
|
||||||
|
import { If } from '@kit/ui/if';
|
||||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
@@ -99,13 +100,10 @@ async function TeamAccountMembersPage({ params }: Params) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const canManageRoles = account.permissions.includes('roles.manage');
|
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 = {
|
const isPrimaryOwner = account.primary_owner_user_id === user.id;
|
||||||
canUpdateRole: canManageRoles,
|
const currentUserRoleHierarchy = account.role_hierarchy_level;
|
||||||
canRemoveFromAccount: canManageRoles,
|
|
||||||
canTransferOwnership: isPrimaryOwner,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -126,23 +124,32 @@ async function TeamAccountMembersPage({ params }: Params) {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Here you can manage the members of your organization.
|
<Trans i18nKey={'common:membersTabDescription'} />
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<InviteMembersDialogContainer account={account.slug}>
|
<If condition={canManageInvitations}>
|
||||||
<Button size={'sm'}>
|
<InviteMembersDialogContainer
|
||||||
<PlusCircle className={'mr-2 w-4'} />
|
userRoleHierarchy={currentUserRoleHierarchy}
|
||||||
<span>Add Member</span>
|
account={account.slug}
|
||||||
</Button>
|
>
|
||||||
</InviteMembersDialogContainer>
|
<Button size={'sm'}>
|
||||||
|
<PlusCircle className={'mr-2 w-4'} />
|
||||||
|
<span>
|
||||||
|
<Trans i18nKey={'teams:inviteMembersButton'} />
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</InviteMembersDialogContainer>
|
||||||
|
</If>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<AccountMembersTable
|
<AccountMembersTable
|
||||||
|
userRoleHierarchy={currentUserRoleHierarchy}
|
||||||
currentUserId={user.id}
|
currentUserId={user.id}
|
||||||
permissions={permissions}
|
|
||||||
members={members}
|
members={members}
|
||||||
|
isPrimaryOwner={isPrimaryOwner}
|
||||||
|
canManageRoles={canManageRoles}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -150,11 +157,12 @@ async function TeamAccountMembersPage({ params }: Params) {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className={'flex flex-row justify-between'}>
|
<CardHeader className={'flex flex-row justify-between'}>
|
||||||
<div className={'flex flex-col space-y-1.5'}>
|
<div className={'flex flex-col space-y-1.5'}>
|
||||||
<CardTitle>Pending Invitations</CardTitle>
|
<CardTitle>
|
||||||
|
<Trans i18nKey={'teams:pendingInvitesHeading'} />
|
||||||
|
</CardTitle>
|
||||||
|
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Here you can manage the pending invitations to your
|
<Trans i18nKey={'teams:pendingInvitesDescription'} />
|
||||||
organization.
|
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -164,6 +172,7 @@ async function TeamAccountMembersPage({ params }: Params) {
|
|||||||
permissions={{
|
permissions={{
|
||||||
canUpdateInvitation: canManageRoles,
|
canUpdateInvitation: canManageRoles,
|
||||||
canRemoveInvitation: canManageRoles,
|
canRemoveInvitation: canManageRoles,
|
||||||
|
currentUserRoleHierarchy,
|
||||||
}}
|
}}
|
||||||
invitations={invitations}
|
invitations={invitations}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,14 +2,13 @@
|
|||||||
|
|
||||||
import Link from 'next/link';
|
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 { ChevronRight } from 'lucide-react';
|
||||||
|
|
||||||
import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown';
|
import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown';
|
||||||
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
|
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
|
||||||
import { useUser } from '@kit/supabase/hooks/use-user';
|
import { useUser } from '@kit/supabase/hooks/use-user';
|
||||||
import { useUserSession } from '@kit/supabase/hooks/use-user-session';
|
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export async function generateMetadata({
|
|||||||
const { title, date, description, image, slug } = post;
|
const { title, date, description, image, slug } = post;
|
||||||
const url = [appConfig.url, 'blog', slug].join('/');
|
const url = [appConfig.url, 'blog', slug].join('/');
|
||||||
|
|
||||||
return {
|
return Promise.resolve({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
openGraph: {
|
openGraph: {
|
||||||
@@ -46,7 +46,7 @@ export async function generateMetadata({
|
|||||||
description,
|
description,
|
||||||
images: image ? [image] : [],
|
images: image ? [image] : [],
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function BlogPost({ params }: { params: { slug: string } }) {
|
function BlogPost({ params }: { params: { slug: string } }) {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export const buildDocumentationTree = cache(
|
|||||||
.filter(
|
.filter(
|
||||||
(_) =>
|
(_) =>
|
||||||
_.pathSegments.length === level + 1 &&
|
_.pathSegments.length === level + 1 &&
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
_.pathSegments
|
_.pathSegments
|
||||||
.map(({ pathName }: { pathName: string }) => pathName)
|
.map(({ pathName }: { pathName: string }) => pathName)
|
||||||
.join('/')
|
.join('/')
|
||||||
@@ -37,6 +38,7 @@ export const buildDocumentationTree = cache(
|
|||||||
return pages.map((doc, index) => {
|
return pages.map((doc, index) => {
|
||||||
const children = buildDocumentationTree(
|
const children = buildDocumentationTree(
|
||||||
docs,
|
docs,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
doc.pathSegments.map(({ pathName }: { pathName: string }) => pathName),
|
doc.pathSegments.map(({ pathName }: { pathName: string }) => pathName),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -44,7 +46,7 @@ export const buildDocumentationTree = cache(
|
|||||||
...doc,
|
...doc,
|
||||||
pathSegments: doc.pathSegments || ([] as string[]),
|
pathSegments: doc.pathSegments || ([] as string[]),
|
||||||
collapsible: children.length > 0,
|
collapsible: children.length > 0,
|
||||||
nextPage: children[0] || pages[index + 1],
|
nextPage: children[0] ?? pages[index + 1],
|
||||||
previousPage: pages[index - 1],
|
previousPage: pages[index - 1],
|
||||||
children,
|
children,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { DocumentationPage } from 'contentlayer/generated';
|
|
||||||
import { allDocumentationPages } from 'contentlayer/generated';
|
import { allDocumentationPages } from 'contentlayer/generated';
|
||||||
|
|
||||||
import DocsNavigation from '~/(marketing)/docs/_components/docs-navigation';
|
import DocsNavigation from '~/(marketing)/docs/_components/docs-navigation';
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"build": "pnpm with-env next build",
|
"build": "pnpm with-env next build",
|
||||||
"clean": "git clean -xdf .next .turbo node_modules",
|
"clean": "git clean -xdf .next .turbo node_modules",
|
||||||
"dev": "pnpm with-env next dev --turbo",
|
"dev": "pnpm with-env next dev --turbo",
|
||||||
"lint": "next lint",
|
"next:lint": "next lint",
|
||||||
"format": "prettier --check \"**/*.{js,cjs,mjs,ts,tsx,md,json}\"",
|
"format": "prettier --check \"**/*.{js,cjs,mjs,ts,tsx,md,json}\"",
|
||||||
"start": "pnpm with-env next start",
|
"start": "pnpm with-env next start",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
"checkoutSuccessTitle": "Done! You're all set.",
|
"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 }}.",
|
"checkoutSuccessDescription": "Thank you for subscribing, we have successfully processed your subscription! A confirmation email will be sent to {{ customerEmail }}.",
|
||||||
"checkoutSuccessBackButton": "Proceed to App",
|
"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": {
|
"status": {
|
||||||
"free": {
|
"free": {
|
||||||
"badge": "Free Plan",
|
"badge": "Free Plan",
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"primaryOwnerLabel": "Primary Owner",
|
"primaryOwnerLabel": "Primary Owner",
|
||||||
"joinedAtLabel": "Joined at",
|
"joinedAtLabel": "Joined at",
|
||||||
"invitedAtLabel": "Invited 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",
|
"inviteMembersPageSubheading": "Invite members to your Team",
|
||||||
"createTeamModalHeading": "Create Team",
|
"createTeamModalHeading": "Create Team",
|
||||||
"createTeamModalDescription": "Create a new Team to manage your projects and members.",
|
"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.",
|
"inviteMembersDescription": "Invite members to your team by entering their email and role.",
|
||||||
"emailPlaceholder": "member@email.com",
|
"emailPlaceholder": "member@email.com",
|
||||||
"membersPageHeading": "Members",
|
"membersPageHeading": "Members",
|
||||||
"inviteMembersButtonLabel": "Invite Members",
|
"inviteMembersButton": "Invite Members",
|
||||||
"invitingMembers": "Inviting members...",
|
"invitingMembers": "Inviting members...",
|
||||||
"pendingInvitesHeading": "Pending Invites",
|
"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",
|
"noPendingInvites": "No pending invites found",
|
||||||
"loadingMembers": "Loading members...",
|
"loadingMembers": "Loading members...",
|
||||||
"loadMembersError": "Sorry, we couldn't fetch your team's members.",
|
"loadMembersError": "Sorry, we couldn't fetch your team's members.",
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ function buildLazyComponent<
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={fallback}>
|
<Suspense fallback={fallback}>
|
||||||
{/* @ts-expect-error */}
|
{/* @ts-expect-error: weird TS */}
|
||||||
<LoadedComponent
|
<LoadedComponent
|
||||||
onClose={props.onClose}
|
onClose={props.onClose}
|
||||||
checkoutToken={props.checkoutToken}
|
checkoutToken={props.checkoutToken}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ type AccountInvitationsTableProps = {
|
|||||||
permissions: {
|
permissions: {
|
||||||
canUpdateInvitation: boolean;
|
canUpdateInvitation: boolean;
|
||||||
canRemoveInvitation: boolean;
|
canRemoveInvitation: boolean;
|
||||||
|
currentUserRoleHierarchy: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -72,6 +73,7 @@ export function AccountInvitationsTable({
|
|||||||
function useGetColumns(permissions: {
|
function useGetColumns(permissions: {
|
||||||
canUpdateInvitation: boolean;
|
canUpdateInvitation: boolean;
|
||||||
canRemoveInvitation: boolean;
|
canRemoveInvitation: boolean;
|
||||||
|
currentUserRoleHierarchy: number;
|
||||||
}): ColumnDef<Invitations[0]>[] {
|
}): ColumnDef<Invitations[0]>[] {
|
||||||
const { t } = useTranslation('teams');
|
const { t } = useTranslation('teams');
|
||||||
|
|
||||||
@@ -197,6 +199,7 @@ function ActionsDropdown({
|
|||||||
setIsOpen={setIsUpdatingRole}
|
setIsOpen={setIsUpdatingRole}
|
||||||
invitationId={invitation.id}
|
invitationId={invitation.id}
|
||||||
userRole={invitation.role}
|
userRole={invitation.role}
|
||||||
|
userRoleHierarchy={permissions.currentUserRoleHierarchy}
|
||||||
/>
|
/>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Database } from '@kit/supabase/database';
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import {
|
import {
|
||||||
@@ -29,15 +28,17 @@ import { Trans } from '@kit/ui/trans';
|
|||||||
import { UpdateRoleSchema } from '../../schema/update-role-schema';
|
import { UpdateRoleSchema } from '../../schema/update-role-schema';
|
||||||
import { updateInvitationAction } from '../../server/actions/team-invitations-server-actions';
|
import { updateInvitationAction } from '../../server/actions/team-invitations-server-actions';
|
||||||
import { MembershipRoleSelector } from '../members/membership-role-selector';
|
import { MembershipRoleSelector } from '../members/membership-role-selector';
|
||||||
|
import { RolesDataProvider } from '../members/roles-data-provider';
|
||||||
|
|
||||||
type Role = Database['public']['Enums']['account_role'];
|
type Role = string;
|
||||||
|
|
||||||
export const UpdateInvitationDialog: React.FC<{
|
export const UpdateInvitationDialog: React.FC<{
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setIsOpen: (isOpen: boolean) => void;
|
setIsOpen: (isOpen: boolean) => void;
|
||||||
invitationId: number;
|
invitationId: number;
|
||||||
userRole: Role;
|
userRole: Role;
|
||||||
}> = ({ isOpen, setIsOpen, invitationId, userRole }) => {
|
userRoleHierarchy: number;
|
||||||
|
}> = ({ isOpen, setIsOpen, invitationId, userRole, userRoleHierarchy }) => {
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@@ -51,11 +52,16 @@ export const UpdateInvitationDialog: React.FC<{
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<UpdateInvitationForm
|
<RolesDataProvider maxRoleHierarchy={userRoleHierarchy}>
|
||||||
setIsOpen={setIsOpen}
|
{(roles) => (
|
||||||
invitationId={invitationId}
|
<UpdateInvitationForm
|
||||||
userRole={userRole}
|
invitationId={invitationId}
|
||||||
/>
|
userRole={userRole}
|
||||||
|
userRoleHierarchy={roles.length}
|
||||||
|
setIsOpen={setIsOpen}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</RolesDataProvider>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
@@ -64,10 +70,12 @@ export const UpdateInvitationDialog: React.FC<{
|
|||||||
function UpdateInvitationForm({
|
function UpdateInvitationForm({
|
||||||
invitationId,
|
invitationId,
|
||||||
userRole,
|
userRole,
|
||||||
|
userRoleHierarchy,
|
||||||
setIsOpen,
|
setIsOpen,
|
||||||
}: React.PropsWithChildren<{
|
}: React.PropsWithChildren<{
|
||||||
invitationId: number;
|
invitationId: number;
|
||||||
userRole: Role;
|
userRole: Role;
|
||||||
|
userRoleHierarchy: number;
|
||||||
setIsOpen: (isOpen: boolean) => void;
|
setIsOpen: (isOpen: boolean) => void;
|
||||||
}>) {
|
}>) {
|
||||||
const { t } = useTranslation('teams');
|
const { t } = useTranslation('teams');
|
||||||
@@ -128,11 +136,18 @@ function UpdateInvitationForm({
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<MembershipRoleSelector
|
<RolesDataProvider maxRoleHierarchy={userRoleHierarchy}>
|
||||||
currentUserRole={userRole}
|
{(roles) => (
|
||||||
value={field.value}
|
<MembershipRoleSelector
|
||||||
onChange={(newRole) => form.setValue('role', newRole)}
|
roles={roles}
|
||||||
/>
|
currentUserRole={userRole}
|
||||||
|
value={field.value}
|
||||||
|
onChange={(newRole) =>
|
||||||
|
form.setValue(field.name, newRole)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</RolesDataProvider>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
|
|||||||
@@ -28,25 +28,38 @@ import { UpdateMemberRoleDialog } from './update-member-role-dialog';
|
|||||||
type Members =
|
type Members =
|
||||||
Database['public']['Functions']['get_account_members']['Returns'];
|
Database['public']['Functions']['get_account_members']['Returns'];
|
||||||
|
|
||||||
|
interface Permissions {
|
||||||
|
canUpdateRole: (roleHierarchy: number) => boolean;
|
||||||
|
canRemoveFromAccount: (roleHierarchy: number) => boolean;
|
||||||
|
canTransferOwnership: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
type AccountMembersTableProps = {
|
type AccountMembersTableProps = {
|
||||||
members: Members;
|
members: Members;
|
||||||
|
|
||||||
currentUserId: string;
|
currentUserId: string;
|
||||||
|
userRoleHierarchy: number;
|
||||||
permissions: {
|
isPrimaryOwner: boolean;
|
||||||
canUpdateRole: boolean;
|
canManageRoles: boolean;
|
||||||
canTransferOwnership: boolean;
|
|
||||||
canRemoveFromAccount: boolean;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AccountMembersTable({
|
export function AccountMembersTable({
|
||||||
members,
|
members,
|
||||||
permissions,
|
|
||||||
currentUserId,
|
currentUserId,
|
||||||
|
isPrimaryOwner,
|
||||||
|
userRoleHierarchy,
|
||||||
|
canManageRoles,
|
||||||
}: AccountMembersTableProps) {
|
}: AccountMembersTableProps) {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const { t } = useTranslation('teams');
|
const { t } = useTranslation('teams');
|
||||||
|
|
||||||
|
const permissions = {
|
||||||
|
canUpdateRole: (targetRole: number) =>
|
||||||
|
canManageRoles && targetRole < userRoleHierarchy,
|
||||||
|
canRemoveFromAccount: (targetRole: number) =>
|
||||||
|
canManageRoles && targetRole < userRoleHierarchy,
|
||||||
|
canTransferOwnership: isPrimaryOwner,
|
||||||
|
};
|
||||||
|
|
||||||
const columns = useGetColumns(permissions, currentUserId);
|
const columns = useGetColumns(permissions, currentUserId);
|
||||||
|
|
||||||
const filteredMembers = members.filter((member) => {
|
const filteredMembers = members.filter((member) => {
|
||||||
@@ -73,11 +86,7 @@ export function AccountMembersTable({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function useGetColumns(
|
function useGetColumns(
|
||||||
permissions: {
|
permissions: Permissions,
|
||||||
canUpdateRole: boolean;
|
|
||||||
canTransferOwnership: boolean;
|
|
||||||
canRemoveFromAccount: boolean;
|
|
||||||
},
|
|
||||||
currentUserId: string,
|
currentUserId: string,
|
||||||
): ColumnDef<Members[0]>[] {
|
): ColumnDef<Members[0]>[] {
|
||||||
const { t } = useTranslation('teams');
|
const { t } = useTranslation('teams');
|
||||||
@@ -173,7 +182,7 @@ function ActionsDropdown({
|
|||||||
member,
|
member,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
}: {
|
}: {
|
||||||
permissions: AccountMembersTableProps['permissions'];
|
permissions: Permissions;
|
||||||
member: Members[0];
|
member: Members[0];
|
||||||
currentUserId: string;
|
currentUserId: string;
|
||||||
}) {
|
}) {
|
||||||
@@ -188,6 +197,22 @@ function ActionsDropdown({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const memberRoleHierarchy = member.role_hierarchy_level;
|
||||||
|
const canUpdateRole = permissions.canUpdateRole(memberRoleHierarchy);
|
||||||
|
|
||||||
|
const canRemoveFromAccount =
|
||||||
|
permissions.canRemoveFromAccount(memberRoleHierarchy);
|
||||||
|
|
||||||
|
// if has no permission to update role, transfer ownership or remove from account
|
||||||
|
// do not render the dropdown menu
|
||||||
|
if (
|
||||||
|
!canUpdateRole &&
|
||||||
|
!permissions.canTransferOwnership &&
|
||||||
|
!canRemoveFromAccount
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@@ -198,7 +223,7 @@ function ActionsDropdown({
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<If condition={permissions.canUpdateRole}>
|
<If condition={canUpdateRole}>
|
||||||
<DropdownMenuItem onClick={() => setIsUpdatingRole(true)}>
|
<DropdownMenuItem onClick={() => setIsUpdatingRole(true)}>
|
||||||
<Trans i18nKey={'teams:updateRole'} />
|
<Trans i18nKey={'teams:updateRole'} />
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -210,7 +235,7 @@ function ActionsDropdown({
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
<If condition={permissions.canRemoveFromAccount}>
|
<If condition={canRemoveFromAccount}>
|
||||||
<DropdownMenuItem onClick={() => setIsRemoving(true)}>
|
<DropdownMenuItem onClick={() => setIsRemoving(true)}>
|
||||||
<Trans i18nKey={'teams:removeMember'} />
|
<Trans i18nKey={'teams:removeMember'} />
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -234,6 +259,7 @@ function ActionsDropdown({
|
|||||||
accountId={member.id}
|
accountId={member.id}
|
||||||
userId={member.user_id}
|
userId={member.user_id}
|
||||||
userRole={member.role}
|
userRole={member.role}
|
||||||
|
userRoleHierarchy={memberRoleHierarchy}
|
||||||
/>
|
/>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { Plus, X } from 'lucide-react';
|
|||||||
import { useFieldArray, useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Database } from '@kit/supabase/database';
|
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -36,16 +35,19 @@ import { Trans } from '@kit/ui/trans';
|
|||||||
import { InviteMembersSchema } from '../../schema/invite-members.schema';
|
import { InviteMembersSchema } from '../../schema/invite-members.schema';
|
||||||
import { createInvitationsAction } from '../../server/actions/team-invitations-server-actions';
|
import { createInvitationsAction } from '../../server/actions/team-invitations-server-actions';
|
||||||
import { MembershipRoleSelector } from './membership-role-selector';
|
import { MembershipRoleSelector } from './membership-role-selector';
|
||||||
|
import { RolesDataProvider } from './roles-data-provider';
|
||||||
|
|
||||||
type InviteModel = ReturnType<typeof createEmptyInviteModel>;
|
type InviteModel = ReturnType<typeof createEmptyInviteModel>;
|
||||||
|
|
||||||
type Role = Database['public']['Enums']['account_role'];
|
type Role = string;
|
||||||
|
|
||||||
export function InviteMembersDialogContainer({
|
export function InviteMembersDialogContainer({
|
||||||
account,
|
account,
|
||||||
|
userRoleHierarchy,
|
||||||
children,
|
children,
|
||||||
}: React.PropsWithChildren<{
|
}: React.PropsWithChildren<{
|
||||||
account: string;
|
account: string;
|
||||||
|
userRoleHierarchy: number;
|
||||||
}>) {
|
}>) {
|
||||||
const [pending, startTransition] = useTransition();
|
const [pending, startTransition] = useTransition();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
@@ -65,19 +67,24 @@ export function InviteMembersDialogContainer({
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<InviteMembersForm
|
<RolesDataProvider maxRoleHierarchy={userRoleHierarchy}>
|
||||||
pending={pending}
|
{(roles) => (
|
||||||
onSubmit={(data) => {
|
<InviteMembersForm
|
||||||
startTransition(async () => {
|
pending={pending}
|
||||||
await createInvitationsAction({
|
roles={roles}
|
||||||
account,
|
onSubmit={(data) => {
|
||||||
invitations: data.invitations,
|
startTransition(async () => {
|
||||||
});
|
await createInvitationsAction({
|
||||||
|
account,
|
||||||
|
invitations: data.invitations,
|
||||||
|
});
|
||||||
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
</RolesDataProvider>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
@@ -85,10 +92,12 @@ export function InviteMembersDialogContainer({
|
|||||||
|
|
||||||
function InviteMembersForm({
|
function InviteMembersForm({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
roles,
|
||||||
pending,
|
pending,
|
||||||
}: {
|
}: {
|
||||||
onSubmit: (data: { invitations: InviteModel[] }) => void;
|
onSubmit: (data: { invitations: InviteModel[] }) => void;
|
||||||
pending: boolean;
|
pending: boolean;
|
||||||
|
roles: string[];
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation('teams');
|
const { t } = useTranslation('teams');
|
||||||
|
|
||||||
@@ -156,6 +165,7 @@ function InviteMembersForm({
|
|||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<MembershipRoleSelector
|
<MembershipRoleSelector
|
||||||
|
roles={roles}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onChange={(role) => {
|
onChange={(role) => {
|
||||||
form.setValue(field.name, role);
|
form.setValue(field.name, role);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Database } from '@kit/supabase/database';
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -8,15 +7,14 @@ import {
|
|||||||
} from '@kit/ui/select';
|
} from '@kit/ui/select';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
type Role = Database['public']['Enums']['account_role'];
|
type Role = string;
|
||||||
|
|
||||||
export const MembershipRoleSelector: React.FC<{
|
export const MembershipRoleSelector: React.FC<{
|
||||||
|
roles: Role[];
|
||||||
value: Role;
|
value: Role;
|
||||||
currentUserRole?: Role;
|
currentUserRole?: Role;
|
||||||
onChange: (role: Role) => unknown;
|
onChange: (role: Role) => unknown;
|
||||||
}> = ({ value, currentUserRole, onChange }) => {
|
}> = ({ roles, value, currentUserRole, onChange }) => {
|
||||||
const rolesList: Role[] = ['owner', 'member'];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select value={value} onValueChange={onChange}>
|
<Select value={value} onValueChange={onChange}>
|
||||||
<SelectTrigger data-test={'role-selector-trigger'}>
|
<SelectTrigger data-test={'role-selector-trigger'}>
|
||||||
@@ -24,12 +22,12 @@ export const MembershipRoleSelector: React.FC<{
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{rolesList.map((role) => {
|
{roles.map((role) => {
|
||||||
return (
|
return (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={role}
|
key={role}
|
||||||
data-test={`role-item-${role}`}
|
data-test={`role-item-${role}`}
|
||||||
disabled={currentUserRole && currentUserRole === role}
|
disabled={currentUserRole === role}
|
||||||
value={role}
|
value={role}
|
||||||
>
|
>
|
||||||
<span className={'text-sm capitalize'}>
|
<span className={'text-sm capitalize'}>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { cva } from 'class-variance-authority';
|
import { cva } from 'class-variance-authority';
|
||||||
|
|
||||||
import { Database } from '@kit/supabase/database';
|
|
||||||
import { Badge } from '@kit/ui/badge';
|
import { Badge } from '@kit/ui/badge';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
type Role = Database['public']['Enums']['account_role'];
|
type Role = string;
|
||||||
|
|
||||||
const roleClassNameBuilder = cva('font-medium capitalize', {
|
const roleClassNameBuilder = cva('font-medium capitalize', {
|
||||||
variants: {
|
variants: {
|
||||||
@@ -19,6 +18,7 @@ const roleClassNameBuilder = cva('font-medium capitalize', {
|
|||||||
export const RoleBadge: React.FC<{
|
export const RoleBadge: React.FC<{
|
||||||
role: Role;
|
role: Role;
|
||||||
}> = ({ role }) => {
|
}> = ({ role }) => {
|
||||||
|
// @ts-expect-error: hard to type this since users can add custom roles
|
||||||
const className = roleClassNameBuilder({ role });
|
const className = roleClassNameBuilder({ role });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||||
|
import { LoadingOverlay } from '@kit/ui/loading-overlay';
|
||||||
|
|
||||||
|
export function RolesDataProvider(props: {
|
||||||
|
maxRoleHierarchy: number;
|
||||||
|
children: (roles: string[]) => React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const rolesQuery = useFetchRoles({
|
||||||
|
maxRoleHierarchy: props.maxRoleHierarchy,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rolesQuery.isLoading) {
|
||||||
|
return <LoadingOverlay fullPage={false} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO handle error
|
||||||
|
if (rolesQuery.isError) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{props.children(rolesQuery.data ?? [])}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useFetchRoles(props: { maxRoleHierarchy: number }) {
|
||||||
|
const supabase = useSupabase();
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['roles', props.maxRoleHierarchy],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { error, data } = await supabase
|
||||||
|
.from('roles')
|
||||||
|
.select('name')
|
||||||
|
.gte('hierarchy_level', props.maxRoleHierarchy)
|
||||||
|
.order('hierarchy_level', { ascending: true });
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.map((item) => item.name);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Database } from '@kit/supabase/database';
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import {
|
import {
|
||||||
@@ -29,8 +28,9 @@ import { Trans } from '@kit/ui/trans';
|
|||||||
import { UpdateRoleSchema } from '../../schema/update-role-schema';
|
import { UpdateRoleSchema } from '../../schema/update-role-schema';
|
||||||
import { updateMemberRoleAction } from '../../server/actions/team-members-server-actions';
|
import { updateMemberRoleAction } from '../../server/actions/team-members-server-actions';
|
||||||
import { MembershipRoleSelector } from './membership-role-selector';
|
import { MembershipRoleSelector } from './membership-role-selector';
|
||||||
|
import { RolesDataProvider } from './roles-data-provider';
|
||||||
|
|
||||||
type Role = Database['public']['Enums']['account_role'];
|
type Role = string;
|
||||||
|
|
||||||
export const UpdateMemberRoleDialog: React.FC<{
|
export const UpdateMemberRoleDialog: React.FC<{
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -38,7 +38,15 @@ export const UpdateMemberRoleDialog: React.FC<{
|
|||||||
userId: string;
|
userId: string;
|
||||||
accountId: string;
|
accountId: string;
|
||||||
userRole: Role;
|
userRole: Role;
|
||||||
}> = ({ isOpen, setIsOpen, userId, accountId, userRole }) => {
|
userRoleHierarchy: number;
|
||||||
|
}> = ({
|
||||||
|
isOpen,
|
||||||
|
setIsOpen,
|
||||||
|
userId,
|
||||||
|
accountId,
|
||||||
|
userRole,
|
||||||
|
userRoleHierarchy,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@@ -52,12 +60,17 @@ export const UpdateMemberRoleDialog: React.FC<{
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<UpdateMemberForm
|
<RolesDataProvider maxRoleHierarchy={userRoleHierarchy}>
|
||||||
setIsOpen={setIsOpen}
|
{(data) => (
|
||||||
userId={userId}
|
<UpdateMemberForm
|
||||||
accountId={accountId}
|
setIsOpen={setIsOpen}
|
||||||
userRole={userRole}
|
userId={userId}
|
||||||
/>
|
accountId={accountId}
|
||||||
|
userRole={userRole}
|
||||||
|
roles={data}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</RolesDataProvider>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
@@ -68,11 +81,13 @@ function UpdateMemberForm({
|
|||||||
userRole,
|
userRole,
|
||||||
accountId,
|
accountId,
|
||||||
setIsOpen,
|
setIsOpen,
|
||||||
|
roles,
|
||||||
}: React.PropsWithChildren<{
|
}: React.PropsWithChildren<{
|
||||||
userId: string;
|
userId: string;
|
||||||
userRole: Role;
|
userRole: Role;
|
||||||
accountId: string;
|
accountId: string;
|
||||||
setIsOpen: (isOpen: boolean) => void;
|
setIsOpen: (isOpen: boolean) => void;
|
||||||
|
roles: Role[];
|
||||||
}>) {
|
}>) {
|
||||||
const [pending, startTransition] = useTransition();
|
const [pending, startTransition] = useTransition();
|
||||||
const [error, setError] = useState<boolean>();
|
const [error, setError] = useState<boolean>();
|
||||||
@@ -128,6 +143,7 @@ function UpdateMemberForm({
|
|||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<MembershipRoleSelector
|
<MembershipRoleSelector
|
||||||
|
roles={roles}
|
||||||
currentUserRole={userRole}
|
currentUserRole={userRole}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onChange={(newRole) => form.setValue('role', newRole)}
|
onChange={(newRole) => form.setValue('role', newRole)}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { Database } from '@kit/supabase/database';
|
type Role = string;
|
||||||
|
|
||||||
type Role = Database['public']['Enums']['account_role'];
|
|
||||||
|
|
||||||
const InviteSchema = z.object({
|
const InviteSchema = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { Database } from '@kit/supabase/database';
|
type Role = string;
|
||||||
|
|
||||||
type Role = Database['public']['Enums']['account_role'];
|
|
||||||
|
|
||||||
export const UpdateInvitationSchema = z.object({
|
export const UpdateInvitationSchema = z.object({
|
||||||
invitationId: z.number(),
|
invitationId: z.number(),
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { Database } from '@kit/supabase/database';
|
|
||||||
|
|
||||||
type Role = Database['public']['Enums']['account_role'];
|
|
||||||
|
|
||||||
export const UpdateRoleSchema = z.object({
|
export const UpdateRoleSchema = z.object({
|
||||||
role: z.custom<Role>((value) => z.string().parse(value)),
|
role: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export async function removeMemberFromAccountAction(params: {
|
|||||||
export async function updateMemberRoleAction(params: {
|
export async function updateMemberRoleAction(params: {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
role: Database['public']['Enums']['account_role'];
|
role: string;
|
||||||
}) {
|
}) {
|
||||||
const client = getSupabaseServerActionClient();
|
const client = getSupabaseServerActionClient();
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export class AccountMembersService {
|
|||||||
async updateMemberRole(params: {
|
async updateMemberRole(params: {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
role: Database['public']['Enums']['account_role'];
|
role: string;
|
||||||
}) {
|
}) {
|
||||||
const { data, error } = await this.client
|
const { data, error } = await this.client
|
||||||
.from('accounts_memberships')
|
.from('accounts_memberships')
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import { SupabaseClient } from '@supabase/supabase-js';
|
import { SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
import 'server-only';
|
import 'server-only';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
|
|
||||||
|
import { LeaveTeamAccountSchema } from '../../schema/leave-team-account.schema';
|
||||||
|
|
||||||
export class LeaveAccountService {
|
export class LeaveAccountService {
|
||||||
constructor(private readonly client: SupabaseClient<Database>) {}
|
constructor(private readonly client: SupabaseClient<Database>) {}
|
||||||
|
|
||||||
async leaveTeamAccount(params: { accountId: string; userId: string }) {
|
async leaveTeamAccount(params: z.infer<typeof LeaveTeamAccountSchema>) {
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
console.log(params);
|
||||||
// TODO
|
// TODO
|
||||||
// implement this method
|
// implement this method
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ type Config = z.infer<typeof MailerSchema>;
|
|||||||
*/
|
*/
|
||||||
export class CloudflareMailer implements Mailer {
|
export class CloudflareMailer implements Mailer {
|
||||||
async sendEmail(config: Config) {
|
async sendEmail(config: Config) {
|
||||||
|
// make lint happy for now
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
console.log('Sending email with Cloudflare Workers', config);
|
console.log('Sending email with Cloudflare Workers', config);
|
||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ export class StripeWebhookHandlerService
|
|||||||
const { subscription } = params;
|
const { subscription } = params;
|
||||||
const lineItem = subscription.items.data[0];
|
const lineItem = subscription.items.data[0];
|
||||||
const price = lineItem?.price;
|
const price = lineItem?.price;
|
||||||
const priceId = price?.id!;
|
const priceId = price?.id as string;
|
||||||
const interval = price?.recurring?.interval ?? null;
|
const interval = price?.recurring?.interval ?? null;
|
||||||
|
|
||||||
const active =
|
const active =
|
||||||
@@ -194,8 +194,8 @@ export class StripeWebhookHandlerService
|
|||||||
price_amount: params.amount,
|
price_amount: params.amount,
|
||||||
cancel_at_period_end: subscription.cancel_at_period_end ?? false,
|
cancel_at_period_end: subscription.cancel_at_period_end ?? false,
|
||||||
interval: interval as string,
|
interval: interval as string,
|
||||||
currency: price?.currency as string,
|
currency: (price as Stripe.Price).currency,
|
||||||
product_id: price?.product as string,
|
product_id: (price as Stripe.Price).product,
|
||||||
variant_id: priceId,
|
variant_id: priceId,
|
||||||
interval_count: price?.recurring?.interval_count ?? 1,
|
interval_count: price?.recurring?.interval_count ?? 1,
|
||||||
period_starts_at: getISOString(subscription.current_period_start),
|
period_starts_at: getISOString(subscription.current_period_start),
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
"@kit/prettier-config": "workspace:*",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/tailwind-config": "workspace:*",
|
"@kit/tailwind-config": "workspace:*",
|
||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
|
"@supabase/gotrue-js": "2.62.2",
|
||||||
"@supabase/ssr": "^0.1.0",
|
"@supabase/ssr": "^0.1.0",
|
||||||
"@supabase/supabase-js": "^2.41.1",
|
"@supabase/supabase-js": "^2.41.1",
|
||||||
"@tanstack/react-query": "5.28.6"
|
"@tanstack/react-query": "5.28.6"
|
||||||
|
|||||||
@@ -38,17 +38,17 @@ export type Database = {
|
|||||||
Row: {
|
Row: {
|
||||||
account_id: string;
|
account_id: string;
|
||||||
id: number;
|
id: number;
|
||||||
role: Database['public']['Enums']['account_role'];
|
role: string;
|
||||||
};
|
};
|
||||||
Insert: {
|
Insert: {
|
||||||
account_id: string;
|
account_id: string;
|
||||||
id?: number;
|
id?: number;
|
||||||
role: Database['public']['Enums']['account_role'];
|
role: string;
|
||||||
};
|
};
|
||||||
Update: {
|
Update: {
|
||||||
account_id?: string;
|
account_id?: string;
|
||||||
id?: number;
|
id?: number;
|
||||||
role?: Database['public']['Enums']['account_role'];
|
role?: string;
|
||||||
};
|
};
|
||||||
Relationships: [
|
Relationships: [
|
||||||
{
|
{
|
||||||
@@ -72,6 +72,13 @@ export type Database = {
|
|||||||
referencedRelation: 'user_accounts';
|
referencedRelation: 'user_accounts';
|
||||||
referencedColumns: ['id'];
|
referencedColumns: ['id'];
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: 'account_roles_role_fkey';
|
||||||
|
columns: ['role'];
|
||||||
|
isOneToOne: false;
|
||||||
|
referencedRelation: 'roles';
|
||||||
|
referencedColumns: ['name'];
|
||||||
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
accounts: {
|
accounts: {
|
||||||
@@ -141,7 +148,7 @@ export type Database = {
|
|||||||
accounts_memberships: {
|
accounts_memberships: {
|
||||||
Row: {
|
Row: {
|
||||||
account_id: string;
|
account_id: string;
|
||||||
account_role: Database['public']['Enums']['account_role'];
|
account_role: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
created_by: string | null;
|
created_by: string | null;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
@@ -150,7 +157,7 @@ export type Database = {
|
|||||||
};
|
};
|
||||||
Insert: {
|
Insert: {
|
||||||
account_id: string;
|
account_id: string;
|
||||||
account_role: Database['public']['Enums']['account_role'];
|
account_role: string;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
created_by?: string | null;
|
created_by?: string | null;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
@@ -159,7 +166,7 @@ export type Database = {
|
|||||||
};
|
};
|
||||||
Update: {
|
Update: {
|
||||||
account_id?: string;
|
account_id?: string;
|
||||||
account_role?: Database['public']['Enums']['account_role'];
|
account_role?: string;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
created_by?: string | null;
|
created_by?: string | null;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
@@ -188,6 +195,13 @@ export type Database = {
|
|||||||
referencedRelation: 'user_accounts';
|
referencedRelation: 'user_accounts';
|
||||||
referencedColumns: ['id'];
|
referencedColumns: ['id'];
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: 'accounts_memberships_account_role_fkey';
|
||||||
|
columns: ['account_role'];
|
||||||
|
isOneToOne: false;
|
||||||
|
referencedRelation: 'roles';
|
||||||
|
referencedColumns: ['name'];
|
||||||
|
},
|
||||||
{
|
{
|
||||||
foreignKeyName: 'accounts_memberships_created_by_fkey';
|
foreignKeyName: 'accounts_memberships_created_by_fkey';
|
||||||
columns: ['created_by'];
|
columns: ['created_by'];
|
||||||
@@ -287,7 +301,7 @@ export type Database = {
|
|||||||
id: number;
|
id: number;
|
||||||
invite_token: string;
|
invite_token: string;
|
||||||
invited_by: string;
|
invited_by: string;
|
||||||
role: Database['public']['Enums']['account_role'];
|
role: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
};
|
};
|
||||||
Insert: {
|
Insert: {
|
||||||
@@ -298,7 +312,7 @@ export type Database = {
|
|||||||
id?: number;
|
id?: number;
|
||||||
invite_token: string;
|
invite_token: string;
|
||||||
invited_by: string;
|
invited_by: string;
|
||||||
role: Database['public']['Enums']['account_role'];
|
role: string;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
};
|
};
|
||||||
Update: {
|
Update: {
|
||||||
@@ -309,7 +323,7 @@ export type Database = {
|
|||||||
id?: number;
|
id?: number;
|
||||||
invite_token?: string;
|
invite_token?: string;
|
||||||
invited_by?: string;
|
invited_by?: string;
|
||||||
role?: Database['public']['Enums']['account_role'];
|
role?: string;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
};
|
};
|
||||||
Relationships: [
|
Relationships: [
|
||||||
@@ -341,25 +355,83 @@ export type Database = {
|
|||||||
referencedRelation: 'users';
|
referencedRelation: 'users';
|
||||||
referencedColumns: ['id'];
|
referencedColumns: ['id'];
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: 'invitations_role_fkey';
|
||||||
|
columns: ['role'];
|
||||||
|
isOneToOne: false;
|
||||||
|
referencedRelation: 'roles';
|
||||||
|
referencedColumns: ['name'];
|
||||||
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
role_permissions: {
|
role_permissions: {
|
||||||
Row: {
|
Row: {
|
||||||
id: number;
|
id: number;
|
||||||
permission: Database['public']['Enums']['app_permissions'];
|
permission: Database['public']['Enums']['app_permissions'];
|
||||||
role: Database['public']['Enums']['account_role'];
|
role: string;
|
||||||
};
|
};
|
||||||
Insert: {
|
Insert: {
|
||||||
id?: number;
|
id?: number;
|
||||||
permission: Database['public']['Enums']['app_permissions'];
|
permission: Database['public']['Enums']['app_permissions'];
|
||||||
role: Database['public']['Enums']['account_role'];
|
role: string;
|
||||||
};
|
};
|
||||||
Update: {
|
Update: {
|
||||||
id?: number;
|
id?: number;
|
||||||
permission?: Database['public']['Enums']['app_permissions'];
|
permission?: Database['public']['Enums']['app_permissions'];
|
||||||
role?: Database['public']['Enums']['account_role'];
|
role?: string;
|
||||||
};
|
};
|
||||||
Relationships: [];
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: 'role_permissions_role_fkey';
|
||||||
|
columns: ['role'];
|
||||||
|
isOneToOne: false;
|
||||||
|
referencedRelation: 'roles';
|
||||||
|
referencedColumns: ['name'];
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
roles: {
|
||||||
|
Row: {
|
||||||
|
account_id: string | null;
|
||||||
|
hierarchy_level: number;
|
||||||
|
is_custom: boolean;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
Insert: {
|
||||||
|
account_id?: string | null;
|
||||||
|
hierarchy_level: number;
|
||||||
|
is_custom?: boolean;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
Update: {
|
||||||
|
account_id?: string | null;
|
||||||
|
hierarchy_level?: number;
|
||||||
|
is_custom?: boolean;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: 'roles_account_id_fkey';
|
||||||
|
columns: ['account_id'];
|
||||||
|
isOneToOne: false;
|
||||||
|
referencedRelation: 'accounts';
|
||||||
|
referencedColumns: ['id'];
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: 'roles_account_id_fkey';
|
||||||
|
columns: ['account_id'];
|
||||||
|
isOneToOne: false;
|
||||||
|
referencedRelation: 'user_account_workspace';
|
||||||
|
referencedColumns: ['id'];
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: 'roles_account_id_fkey';
|
||||||
|
columns: ['account_id'];
|
||||||
|
isOneToOne: false;
|
||||||
|
referencedRelation: 'user_accounts';
|
||||||
|
referencedColumns: ['id'];
|
||||||
|
},
|
||||||
|
];
|
||||||
};
|
};
|
||||||
subscriptions: {
|
subscriptions: {
|
||||||
Row: {
|
Row: {
|
||||||
@@ -474,10 +546,18 @@ export type Database = {
|
|||||||
id: string | null;
|
id: string | null;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
picture_url: string | null;
|
picture_url: string | null;
|
||||||
role: Database['public']['Enums']['account_role'] | null;
|
role: string | null;
|
||||||
slug: string | null;
|
slug: string | null;
|
||||||
};
|
};
|
||||||
Relationships: [];
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: 'accounts_memberships_account_role_fkey';
|
||||||
|
columns: ['role'];
|
||||||
|
isOneToOne: false;
|
||||||
|
referencedRelation: 'roles';
|
||||||
|
referencedColumns: ['name'];
|
||||||
|
},
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
Functions: {
|
Functions: {
|
||||||
@@ -559,7 +639,7 @@ export type Database = {
|
|||||||
Args: {
|
Args: {
|
||||||
account_id: string;
|
account_id: string;
|
||||||
email: string;
|
email: string;
|
||||||
role: Database['public']['Enums']['account_role'];
|
role: string;
|
||||||
};
|
};
|
||||||
Returns: {
|
Returns: {
|
||||||
account_id: string;
|
account_id: string;
|
||||||
@@ -569,7 +649,7 @@ export type Database = {
|
|||||||
id: number;
|
id: number;
|
||||||
invite_token: string;
|
invite_token: string;
|
||||||
invited_by: string;
|
invited_by: string;
|
||||||
role: Database['public']['Enums']['account_role'];
|
role: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -582,7 +662,7 @@ export type Database = {
|
|||||||
email: string;
|
email: string;
|
||||||
account_id: string;
|
account_id: string;
|
||||||
invited_by: string;
|
invited_by: string;
|
||||||
role: Database['public']['Enums']['account_role'];
|
role: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
expires_at: string;
|
expires_at: string;
|
||||||
@@ -598,7 +678,8 @@ export type Database = {
|
|||||||
id: string;
|
id: string;
|
||||||
user_id: string;
|
user_id: string;
|
||||||
account_id: string;
|
account_id: string;
|
||||||
role: Database['public']['Enums']['account_role'];
|
role: string;
|
||||||
|
role_hierarchy_level: number;
|
||||||
primary_owner_user_id: string;
|
primary_owner_user_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
@@ -627,6 +708,14 @@ export type Database = {
|
|||||||
updated_by: string | null;
|
updated_by: string | null;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
has_more_elevated_role: {
|
||||||
|
Args: {
|
||||||
|
target_user_id: string;
|
||||||
|
target_account_id: string;
|
||||||
|
role_name: string;
|
||||||
|
};
|
||||||
|
Returns: boolean;
|
||||||
|
};
|
||||||
has_permission: {
|
has_permission: {
|
||||||
Args: {
|
Args: {
|
||||||
user_id: string;
|
user_id: string;
|
||||||
@@ -638,7 +727,7 @@ export type Database = {
|
|||||||
has_role_on_account: {
|
has_role_on_account: {
|
||||||
Args: {
|
Args: {
|
||||||
account_id: string;
|
account_id: string;
|
||||||
account_role?: Database['public']['Enums']['account_role'];
|
account_role?: string;
|
||||||
};
|
};
|
||||||
Returns: boolean;
|
Returns: boolean;
|
||||||
};
|
};
|
||||||
@@ -670,7 +759,8 @@ export type Database = {
|
|||||||
name: string;
|
name: string;
|
||||||
picture_url: string;
|
picture_url: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
role: Database['public']['Enums']['account_role'];
|
role: string;
|
||||||
|
role_hierarchy_level: number;
|
||||||
primary_owner_user_id: string;
|
primary_owner_user_id: string;
|
||||||
subscription_status: Database['public']['Enums']['subscription_status'];
|
subscription_status: Database['public']['Enums']['subscription_status'];
|
||||||
permissions: Database['public']['Enums']['app_permissions'][];
|
permissions: Database['public']['Enums']['app_permissions'][];
|
||||||
@@ -690,7 +780,6 @@ export type Database = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
Enums: {
|
Enums: {
|
||||||
account_role: 'owner' | 'member';
|
|
||||||
app_permissions:
|
app_permissions:
|
||||||
| 'roles.manage'
|
| 'roles.manage'
|
||||||
| 'billing.manage'
|
| 'billing.manage'
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import type {
|
import type { SignInWithPasswordlessCredentials } from '@supabase/gotrue-js';
|
||||||
AuthError,
|
|
||||||
SignInWithPasswordlessCredentials,
|
|
||||||
} from '@supabase/gotrue-js';
|
|
||||||
|
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
@@ -15,7 +12,7 @@ export function useSignInWithOtp() {
|
|||||||
const result = await client.auth.signInWithOtp(credentials);
|
const result = await client.auth.signInWithOtp(credentials);
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
if (shouldIgnoreError(result.error)) {
|
if (shouldIgnoreError(result.error.message)) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Ignoring error during development: ${result.error.message}`,
|
`Ignoring error during development: ${result.error.message}`,
|
||||||
);
|
);
|
||||||
@@ -37,10 +34,10 @@ export function useSignInWithOtp() {
|
|||||||
|
|
||||||
export default useSignInWithOtp;
|
export default useSignInWithOtp;
|
||||||
|
|
||||||
function shouldIgnoreError(error: AuthError) {
|
function shouldIgnoreError(error: string) {
|
||||||
return isSmsProviderNotSetupError(error);
|
return isSmsProviderNotSetupError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSmsProviderNotSetupError(error: AuthError) {
|
function isSmsProviderNotSetupError(error: string) {
|
||||||
return error.message.includes(`sms Provider could not be found`);
|
return error.includes(`sms Provider could not be found`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import { useCallback, useState } from 'react';
|
|
||||||
|
|
||||||
const SIDEBAR_COLLAPSED_STORAGE_KEY = 'sidebarState';
|
|
||||||
|
|
||||||
function useCollapsible(initialValue?: boolean) {
|
|
||||||
const [isCollapsed, setIsCollapsed] = useState(initialValue);
|
|
||||||
|
|
||||||
const onCollapseChange = useCallback((collapsed: boolean) => {
|
|
||||||
setIsCollapsed(collapsed);
|
|
||||||
storeCollapsibleState(collapsed);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return [isCollapsed, onCollapseChange] as [boolean, typeof onCollapseChange];
|
|
||||||
}
|
|
||||||
|
|
||||||
function storeCollapsibleState(collapsed: boolean) {
|
|
||||||
// TODO: implement below
|
|
||||||
/*
|
|
||||||
setCookie(
|
|
||||||
SIDEBAR_COLLAPSED_STORAGE_KEY,
|
|
||||||
collapsed ? 'collapsed' : 'expanded',
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useCollapsible;
|
|
||||||
@@ -138,7 +138,6 @@ export const ImageUploadInput = forwardRef<React.ElementRef<'input'>, Props>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line jsx-a11y/label-has-associated-control
|
|
||||||
<label
|
<label
|
||||||
id={'image-upload-input'}
|
id={'image-upload-input'}
|
||||||
className={`relative flex h-10 w-full cursor-pointer rounded-md border border-dashed border-input
|
className={`relative flex h-10 w-full cursor-pointer rounded-md border border-dashed border-input
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ export function ImageUploader(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex items-center space-x-4'}>
|
<div className={'flex items-center space-x-4'}>
|
||||||
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
|
|
||||||
<label className={'relative h-20 w-20 animate-in fade-in zoom-in-50'}>
|
<label className={'relative h-20 w-20 animate-in fade-in zoom-in-50'}>
|
||||||
<Image fill className={'h-20 w-20 rounded-full'} src={image} alt={''} />
|
<Image fill className={'h-20 w-20 rounded-full'} src={image} alt={''} />
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { MDXComponents } from 'mdx/types';
|
|||||||
import { getMDXComponent } from 'next-contentlayer/hooks';
|
import { getMDXComponent } from 'next-contentlayer/hooks';
|
||||||
|
|
||||||
import Components from './mdx-components';
|
import Components from './mdx-components';
|
||||||
// @ts-ignore
|
// @ts-expect-error: weird typescript error with css modules
|
||||||
import styles from './mdx-renderer.module.css';
|
import styles from './mdx-renderer.module.css';
|
||||||
|
|
||||||
export function Mdx({
|
export function Mdx({
|
||||||
|
|||||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -586,6 +586,9 @@ importers:
|
|||||||
'@kit/tsconfig':
|
'@kit/tsconfig':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../tooling/typescript
|
version: link:../../tooling/typescript
|
||||||
|
'@supabase/gotrue-js':
|
||||||
|
specifier: 2.62.2
|
||||||
|
version: 2.62.2
|
||||||
'@supabase/ssr':
|
'@supabase/ssr':
|
||||||
specifier: ^0.1.0
|
specifier: ^0.1.0
|
||||||
version: 0.1.0(@supabase/supabase-js@2.41.1)
|
version: 0.1.0(@supabase/supabase-js@2.41.1)
|
||||||
@@ -3978,6 +3981,12 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@supabase/node-fetch': 2.6.15
|
'@supabase/node-fetch': 2.6.15
|
||||||
|
|
||||||
|
/@supabase/gotrue-js@2.62.2:
|
||||||
|
resolution: {integrity: sha512-AP6e6W9rQXFTEJ7sTTNYQrNf0LCcnt1hUW+RIgUK+Uh3jbWvcIST7wAlYyNZiMlS9+PYyymWQ+Ykz/rOYSO0+A==}
|
||||||
|
dependencies:
|
||||||
|
'@supabase/node-fetch': 2.6.15
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@supabase/node-fetch@2.6.15:
|
/@supabase/node-fetch@2.6.15:
|
||||||
resolution: {integrity: sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==}
|
resolution: {integrity: sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==}
|
||||||
engines: {node: 4.x || >=6.0.0}
|
engines: {node: 4.x || >=6.0.0}
|
||||||
|
|||||||
@@ -86,13 +86,6 @@ grant usage on schema public to service_role;
|
|||||||
* We create the enums for the schema
|
* We create the enums for the schema
|
||||||
* -------------------------------------------------------
|
* -------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
/*
|
|
||||||
* Roles
|
|
||||||
- We create the roles for the Supabase MakerKit. These roles are used to manage the permissions for the accounts
|
|
||||||
- The roles are 'owner' and 'member'.
|
|
||||||
- You can add more roles as needed.
|
|
||||||
*/
|
|
||||||
create type public.account_role as enum('owner', 'member');
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Permissions
|
* Permissions
|
||||||
@@ -396,6 +389,34 @@ after
|
|||||||
update of email on auth.users for each row
|
update of email on auth.users for each row
|
||||||
execute procedure kit.handle_update_user_email ();
|
execute procedure kit.handle_update_user_email ();
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------------------------------------
|
||||||
|
* Section: Roles
|
||||||
|
* We create the schema for the roles. Roles are the roles for an account. For example, an account might have the roles 'owner', 'admin', and 'member'.
|
||||||
|
* -------------------------------------------------------
|
||||||
|
*/
|
||||||
|
-- Account Memberships table
|
||||||
|
create table if not exists public.roles (
|
||||||
|
name varchar(50) not null,
|
||||||
|
hierarchy_level int not null,
|
||||||
|
account_id uuid references public.accounts (id) on delete cascade,
|
||||||
|
is_custom boolean not null default false,
|
||||||
|
unique (hierarchy_level, account_id, is_custom),
|
||||||
|
primary key (name)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Seed the roles table with default roles 'owner' and 'member'
|
||||||
|
insert into public.roles (name, hierarchy_level) values ('owner', 1);
|
||||||
|
insert into public.roles (name, hierarchy_level) values ('member', 2);
|
||||||
|
|
||||||
|
-- RLS
|
||||||
|
-- SELECT: authenticated users can query roles
|
||||||
|
create policy roles_read on public.roles for
|
||||||
|
select
|
||||||
|
to authenticated
|
||||||
|
using (true);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------------------------------------
|
* -------------------------------------------------------
|
||||||
* Section: Memberships
|
* Section: Memberships
|
||||||
@@ -407,7 +428,7 @@ create table if not exists
|
|||||||
public.accounts_memberships (
|
public.accounts_memberships (
|
||||||
user_id uuid references auth.users on delete cascade not null,
|
user_id uuid references auth.users on delete cascade not null,
|
||||||
account_id uuid references public.accounts (id) on delete cascade not null,
|
account_id uuid references public.accounts (id) on delete cascade not null,
|
||||||
account_role public.account_role not null,
|
account_role varchar(50) references public.roles (name) not null,
|
||||||
created_at timestamptz default current_timestamp not null,
|
created_at timestamptz default current_timestamp not null,
|
||||||
updated_at timestamptz default current_timestamp not null,
|
updated_at timestamptz default current_timestamp not null,
|
||||||
created_by uuid references auth.users,
|
created_by uuid references auth.users,
|
||||||
@@ -435,7 +456,7 @@ alter table public.accounts_memberships enable row level security;
|
|||||||
create
|
create
|
||||||
or replace function public.has_role_on_account (
|
or replace function public.has_role_on_account (
|
||||||
account_id uuid,
|
account_id uuid,
|
||||||
account_role public.account_role default null
|
account_role varchar(50) default null
|
||||||
) returns boolean language sql security definer
|
) returns boolean language sql security definer
|
||||||
set
|
set
|
||||||
search_path = public as $$
|
search_path = public as $$
|
||||||
@@ -453,7 +474,7 @@ set
|
|||||||
$$;
|
$$;
|
||||||
|
|
||||||
grant
|
grant
|
||||||
execute on function public.has_role_on_account (uuid, public.account_role) to authenticated;
|
execute on function public.has_role_on_account (uuid, varchar) to authenticated;
|
||||||
|
|
||||||
create
|
create
|
||||||
or replace function public.is_team_member (account_id uuid, user_id uuid) returns boolean language sql security definer
|
or replace function public.is_team_member (account_id uuid, user_id uuid) returns boolean language sql security definer
|
||||||
@@ -479,7 +500,8 @@ execute on function public.is_team_member (uuid, uuid) to authenticated;
|
|||||||
create or replace function kit.can_remove_account_member (target_team_account_id uuid, user_id uuid) returns boolean as $$
|
create or replace function kit.can_remove_account_member (target_team_account_id uuid, user_id uuid) returns boolean as $$
|
||||||
declare
|
declare
|
||||||
permission_granted boolean;
|
permission_granted boolean;
|
||||||
target_user_role public.account_role;
|
target_user_hierarchy_level int;
|
||||||
|
current_user_hierarchy_level int;
|
||||||
begin
|
begin
|
||||||
-- validate the auth user has the required permission on the account
|
-- validate the auth user has the required permission on the account
|
||||||
-- to manage members of the account
|
-- to manage members of the account
|
||||||
@@ -494,20 +516,12 @@ begin
|
|||||||
raise exception 'You cannot remove yourself from the account';
|
raise exception 'You cannot remove yourself from the account';
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
-- retrieve the user target role in the account
|
select hierarchy_level into target_user_hierarchy_level from public.roles where name = target_user_role;
|
||||||
select
|
select hierarchy_level into current_user_hierarchy_level from public.roles where name = (select account_role from public.accounts_memberships where account_id = target_team_account_id and user_id = auth.uid());
|
||||||
account_role
|
|
||||||
into
|
|
||||||
target_user_role
|
|
||||||
from
|
|
||||||
public.accounts_memberships as membership
|
|
||||||
where
|
|
||||||
membership.account_id = target_team_account_id
|
|
||||||
and membership.user_id = can_remove_account_member.user_id;
|
|
||||||
|
|
||||||
-- check if the target user is the owner of the account
|
-- check if the current user has a higher hierarchy level than the target user
|
||||||
if target_user_role = 'owner' then
|
if current_user_hierarchy_level <= target_user_hierarchy_level then
|
||||||
raise exception 'You cannot remove the primary owner from the account';
|
raise exception 'You do not have permission to remove this user from the account';
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -569,7 +583,7 @@ create table
|
|||||||
public.account_roles (
|
public.account_roles (
|
||||||
id bigint generated by default as identity primary key,
|
id bigint generated by default as identity primary key,
|
||||||
account_id uuid references public.accounts (id) on delete cascade not null,
|
account_id uuid references public.accounts (id) on delete cascade not null,
|
||||||
role public.account_role not null,
|
role varchar(50) references public.roles(name) not null,
|
||||||
unique (account_id, role)
|
unique (account_id, role)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -608,7 +622,7 @@ select
|
|||||||
create table if not exists
|
create table if not exists
|
||||||
public.role_permissions (
|
public.role_permissions (
|
||||||
id bigint generated by default as identity primary key,
|
id bigint generated by default as identity primary key,
|
||||||
role public.account_role not null,
|
role varchar(50) references public.roles(name) not null,
|
||||||
permission app_permissions not null,
|
permission app_permissions not null,
|
||||||
unique (role, permission)
|
unique (role, permission)
|
||||||
);
|
);
|
||||||
@@ -651,6 +665,34 @@ $$ language plpgsql;
|
|||||||
|
|
||||||
grant execute on function public.has_permission (uuid, uuid, public.app_permissions) to authenticated, service_role;
|
grant execute on function public.has_permission (uuid, uuid, public.app_permissions) to authenticated, service_role;
|
||||||
|
|
||||||
|
create or replace function public.has_more_elevated_role (
|
||||||
|
target_user_id uuid,
|
||||||
|
target_account_id uuid,
|
||||||
|
role_name varchar
|
||||||
|
) returns boolean as $$
|
||||||
|
declare
|
||||||
|
declare is_primary_owner boolean;
|
||||||
|
user_role_hierarchy_level int;
|
||||||
|
target_role_hierarchy_level int;
|
||||||
|
begin
|
||||||
|
select exists (select 1 from public.accounts where id = target_account_id and primary_owner_user_id = target_user_id) into is_primary_owner;
|
||||||
|
|
||||||
|
-- If the user is the primary owner, they have the highest role and can perform any action
|
||||||
|
if is_primary_owner then
|
||||||
|
return true;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select hierarchy_level into user_role_hierarchy_level from public.roles where name = (select account_role from public.accounts_memberships where account_id = target_account_id and target_user_id = user_id);
|
||||||
|
select hierarchy_level into target_role_hierarchy_level from public.roles where name = role_name;
|
||||||
|
|
||||||
|
-- If the user's role is higher than the target role, they can perform the action
|
||||||
|
return user_role_hierarchy_level < target_role_hierarchy_level;
|
||||||
|
|
||||||
|
end;
|
||||||
|
$$ language plpgsql;
|
||||||
|
|
||||||
|
grant execute on function public.has_more_elevated_role (uuid, uuid, varchar) to authenticated, service_role;
|
||||||
|
|
||||||
-- Enable RLS on the role_permissions table
|
-- Enable RLS on the role_permissions table
|
||||||
alter table public.role_permissions enable row level security;
|
alter table public.role_permissions enable row level security;
|
||||||
|
|
||||||
@@ -672,7 +714,7 @@ create table if not exists
|
|||||||
email varchar(255) not null,
|
email varchar(255) not null,
|
||||||
account_id uuid references public.accounts (id) on delete cascade not null,
|
account_id uuid references public.accounts (id) on delete cascade not null,
|
||||||
invited_by uuid references auth.users on delete cascade not null,
|
invited_by uuid references auth.users on delete cascade not null,
|
||||||
role public.account_role not null,
|
role varchar(50) references public.roles (name) not null,
|
||||||
invite_token varchar(255) unique not null,
|
invite_token varchar(255) unique not null,
|
||||||
created_at timestamptz default current_timestamp not null,
|
created_at timestamptz default current_timestamp not null,
|
||||||
updated_at timestamptz default current_timestamp not null,
|
updated_at timestamptz default current_timestamp not null,
|
||||||
@@ -728,23 +770,24 @@ select
|
|||||||
to authenticated using (has_role_on_account (account_id));
|
to authenticated using (has_role_on_account (account_id));
|
||||||
|
|
||||||
-- INSERT: Users can create invitations to users of an account they are a member of
|
-- INSERT: Users can create invitations to users of an account they are a member of
|
||||||
-- and have the 'invites.manage' permission
|
-- and have the 'invites.manage' permission AND the target role is not higher than the user's role
|
||||||
create policy invitations_create_self on public.invitations for
|
create policy invitations_create_self on public.invitations for
|
||||||
insert
|
insert
|
||||||
to authenticated with check (
|
to authenticated with check (
|
||||||
has_role_on_account (account_id)
|
public.has_permission (auth.uid (), account_id, 'invites.manage'::app_permissions) and
|
||||||
and public.has_permission (auth.uid (), account_id, 'invites.manage'::app_permissions));
|
public.has_more_elevated_role (auth.uid (), account_id, role)
|
||||||
|
);
|
||||||
|
|
||||||
-- UPDATE: Users can update invitations to users of an account they are a member of
|
-- UPDATE: Users can update invitations to users of an account they are a member of
|
||||||
-- and have the 'invites.manage' permission
|
-- and have the 'invites.manage' permission AND the target role is not higher than the user's role
|
||||||
create policy invitations_update on public.invitations for
|
create policy invitations_update on public.invitations for
|
||||||
update
|
update
|
||||||
to authenticated using (
|
to authenticated using (
|
||||||
has_role_on_account (account_id)
|
public.has_permission (auth.uid (), account_id, 'invites.manage'::app_permissions)
|
||||||
and public.has_permission (auth.uid (), account_id, 'invites.manage'::app_permissions)
|
and public.has_more_elevated_role (auth.uid (), account_id, role)
|
||||||
) with check (
|
) with check (
|
||||||
has_role_on_account (account_id)
|
public.has_permission (auth.uid (), account_id, 'invites.manage'::app_permissions)
|
||||||
and public.has_permission (auth.uid (), account_id, 'invites.manage'::app_permissions)
|
and public.has_more_elevated_role (auth.uid (), account_id, role)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- DELETE: Users can delete invitations to users of an account they are a member of
|
-- DELETE: Users can delete invitations to users of an account they are a member of
|
||||||
@@ -761,7 +804,7 @@ delete
|
|||||||
create or replace function accept_invitation(token text, user_id uuid) returns void as $$
|
create or replace function accept_invitation(token text, user_id uuid) returns void as $$
|
||||||
declare
|
declare
|
||||||
target_account_id uuid;
|
target_account_id uuid;
|
||||||
target_role public.account_role;
|
target_role varchar(50);
|
||||||
begin
|
begin
|
||||||
select
|
select
|
||||||
account_id,
|
account_id,
|
||||||
@@ -1192,7 +1235,7 @@ create
|
|||||||
or replace function public.create_invitation (
|
or replace function public.create_invitation (
|
||||||
account_id uuid,
|
account_id uuid,
|
||||||
email text,
|
email text,
|
||||||
role public.account_role
|
role varchar(50)
|
||||||
) returns public.invitations as $$
|
) returns public.invitations as $$
|
||||||
declare
|
declare
|
||||||
new_invitation public.invitations;
|
new_invitation public.invitations;
|
||||||
@@ -1283,7 +1326,8 @@ or replace function public.organization_account_workspace (account_slug text) re
|
|||||||
name varchar(255),
|
name varchar(255),
|
||||||
picture_url varchar(1000),
|
picture_url varchar(1000),
|
||||||
slug text,
|
slug text,
|
||||||
role public.account_role,
|
role varchar(50),
|
||||||
|
role_hierarchy_level int,
|
||||||
primary_owner_user_id uuid,
|
primary_owner_user_id uuid,
|
||||||
subscription_status public.subscription_status,
|
subscription_status public.subscription_status,
|
||||||
permissions public.app_permissions[]
|
permissions public.app_permissions[]
|
||||||
@@ -1296,6 +1340,7 @@ begin
|
|||||||
accounts.picture_url,
|
accounts.picture_url,
|
||||||
accounts.slug,
|
accounts.slug,
|
||||||
accounts_memberships.account_role,
|
accounts_memberships.account_role,
|
||||||
|
roles.hierarchy_level,
|
||||||
accounts.primary_owner_user_id,
|
accounts.primary_owner_user_id,
|
||||||
subscriptions.status,
|
subscriptions.status,
|
||||||
array_agg(role_permissions.permission)
|
array_agg(role_permissions.permission)
|
||||||
@@ -1305,13 +1350,15 @@ begin
|
|||||||
left join public.subscriptions on accounts.id = subscriptions.account_id
|
left join public.subscriptions on accounts.id = subscriptions.account_id
|
||||||
left join public.role_permissions on accounts_memberships.account_role =
|
left join public.role_permissions on accounts_memberships.account_role =
|
||||||
role_permissions.role
|
role_permissions.role
|
||||||
|
left join public.roles on accounts_memberships.account_role = roles.name
|
||||||
where
|
where
|
||||||
accounts.slug = account_slug
|
accounts.slug = account_slug
|
||||||
and public.accounts_memberships.user_id = auth.uid()
|
and public.accounts_memberships.user_id = auth.uid()
|
||||||
group by
|
group by
|
||||||
accounts.id,
|
accounts.id,
|
||||||
accounts_memberships.account_role,
|
accounts_memberships.account_role,
|
||||||
subscriptions.status;
|
subscriptions.status,
|
||||||
|
roles.hierarchy_level;
|
||||||
end;
|
end;
|
||||||
$$ language plpgsql;
|
$$ language plpgsql;
|
||||||
|
|
||||||
@@ -1324,7 +1371,8 @@ OR REPLACE FUNCTION public.get_account_members (account_slug text) RETURNS TABLE
|
|||||||
id uuid,
|
id uuid,
|
||||||
user_id uuid,
|
user_id uuid,
|
||||||
account_id uuid,
|
account_id uuid,
|
||||||
role public.account_role,
|
role varchar(50),
|
||||||
|
role_hierarchy_level int,
|
||||||
primary_owner_user_id uuid,
|
primary_owner_user_id uuid,
|
||||||
name varchar,
|
name varchar,
|
||||||
email varchar,
|
email varchar,
|
||||||
@@ -1334,10 +1382,11 @@ OR REPLACE FUNCTION public.get_account_members (account_slug text) RETURNS TABLE
|
|||||||
) LANGUAGE plpgsql AS $$
|
) LANGUAGE plpgsql AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
RETURN QUERY
|
RETURN QUERY
|
||||||
SELECT acc.id, am.user_id, am.account_id, am.account_role, a.primary_owner_user_id, acc.name, acc.email, acc.picture_url, am.created_at, am.updated_at
|
SELECT acc.id, am.user_id, am.account_id, am.account_role, r.hierarchy_level, a.primary_owner_user_id, acc.name, acc.email, acc.picture_url, am.created_at, am.updated_at
|
||||||
FROM public.accounts_memberships am
|
FROM public.accounts_memberships am
|
||||||
JOIN public.accounts a ON a.id = am.account_id
|
JOIN public.accounts a ON a.id = am.account_id
|
||||||
JOIN public.accounts acc on acc.id = am.user_id
|
JOIN public.accounts acc on acc.id = am.user_id
|
||||||
|
JOIN public.roles r ON r.name = am.account_role
|
||||||
WHERE a.slug = account_slug;
|
WHERE a.slug = account_slug;
|
||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
@@ -1351,7 +1400,7 @@ create or replace function public.get_account_invitations(account_slug text) ret
|
|||||||
email varchar(255),
|
email varchar(255),
|
||||||
account_id uuid,
|
account_id uuid,
|
||||||
invited_by uuid,
|
invited_by uuid,
|
||||||
role public.account_role,
|
role varchar(50),
|
||||||
created_at timestamptz,
|
created_at timestamptz,
|
||||||
updated_at timestamptz,
|
updated_at timestamptz,
|
||||||
expires_at timestamptz,
|
expires_at timestamptz,
|
||||||
@@ -1383,7 +1432,7 @@ grant execute on function public.get_account_invitations (text) to authenticated
|
|||||||
|
|
||||||
CREATE TYPE kit.invitation AS (
|
CREATE TYPE kit.invitation AS (
|
||||||
email text,
|
email text,
|
||||||
role public.account_role
|
role varchar(50)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Then, modify your function to use this type
|
-- Then, modify your function to use this type
|
||||||
@@ -1394,7 +1443,7 @@ DECLARE
|
|||||||
all_invitations public.invitations[] := ARRAY[]::public.invitations[];
|
all_invitations public.invitations[] := ARRAY[]::public.invitations[];
|
||||||
invite_token text;
|
invite_token text;
|
||||||
email text;
|
email text;
|
||||||
role public.account_role;
|
role varchar(50);
|
||||||
BEGIN
|
BEGIN
|
||||||
FOREACH email, role IN ARRAY invitations
|
FOREACH email, role IN ARRAY invitations
|
||||||
LOOP
|
LOOP
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const config = {
|
|||||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||||
'@typescript-eslint/consistent-type-definitions': 'off',
|
'@typescript-eslint/consistent-type-definitions': 'off',
|
||||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||||
|
'@typescript-eslint/non-nullable-type-assertion-style': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': [
|
'@typescript-eslint/no-unused-vars': [
|
||||||
'error',
|
'error',
|
||||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-config-turbo": "^1.13.0",
|
"eslint-config-turbo": "^1.13.0",
|
||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
|
||||||
"eslint-plugin-react": "^7.34.1",
|
"eslint-plugin-react": "^7.34.1",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0"
|
"eslint-plugin-react-hooks": "^4.6.0"
|
||||||
},
|
},
|
||||||
|
|||||||
6
tooling/eslint/react.js
vendored
6
tooling/eslint/react.js
vendored
@@ -1,10 +1,6 @@
|
|||||||
/** @type {import('eslint').Linter.Config} */
|
/** @type {import('eslint').Linter.Config} */
|
||||||
const config = {
|
const config = {
|
||||||
extends: [
|
extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended'],
|
||||||
'plugin:react/recommended',
|
|
||||||
'plugin:react-hooks/recommended',
|
|
||||||
'plugin:jsx-a11y/recommended',
|
|
||||||
],
|
|
||||||
rules: {
|
rules: {
|
||||||
'react/prop-types': 'off',
|
'react/prop-types': 'off',
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user