+
+
+
+
}
+ fallback={
+
+
+
+ }
>
{(data) => (
)}
-
+
-
-
-
- Add Member
-
-
+
+
+
+
+
+
+
+
+
+
@@ -150,11 +157,12 @@ async function TeamAccountMembersPage({ params }: Params) {
- Pending Invitations
+
+
+
- Here you can manage the pending invitations to your
- organization.
+
@@ -164,6 +172,7 @@ async function TeamAccountMembersPage({ params }: Params) {
permissions={{
canUpdateInvitation: canManageRoles,
canRemoveInvitation: canManageRoles,
+ currentUserRoleHierarchy,
}}
invitations={invitations}
/>
diff --git a/apps/web/app/(marketing)/_components/site-header-account-section.tsx b/apps/web/app/(marketing)/_components/site-header-account-section.tsx
index f6fb20fb4..9758ae8e5 100644
--- a/apps/web/app/(marketing)/_components/site-header-account-section.tsx
+++ b/apps/web/app/(marketing)/_components/site-header-account-section.tsx
@@ -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';
diff --git a/apps/web/app/(marketing)/blog/[slug]/page.tsx b/apps/web/app/(marketing)/blog/[slug]/page.tsx
index 9ce0c447f..275a97765 100644
--- a/apps/web/app/(marketing)/blog/[slug]/page.tsx
+++ b/apps/web/app/(marketing)/blog/[slug]/page.tsx
@@ -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 } }) {
diff --git a/apps/web/app/(marketing)/docs/_lib/build-documentation-tree.ts b/apps/web/app/(marketing)/docs/_lib/build-documentation-tree.ts
index ca624f2f9..60b34f7a9 100644
--- a/apps/web/app/(marketing)/docs/_lib/build-documentation-tree.ts
+++ b/apps/web/app/(marketing)/docs/_lib/build-documentation-tree.ts
@@ -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,
};
diff --git a/apps/web/app/(marketing)/docs/layout.tsx b/apps/web/app/(marketing)/docs/layout.tsx
index 204352079..a2c52ef27 100644
--- a/apps/web/app/(marketing)/docs/layout.tsx
+++ b/apps/web/app/(marketing)/docs/layout.tsx
@@ -1,4 +1,3 @@
-import type { DocumentationPage } from 'contentlayer/generated';
import { allDocumentationPages } from 'contentlayer/generated';
import DocsNavigation from '~/(marketing)/docs/_components/docs-navigation';
diff --git a/apps/web/package.json b/apps/web/package.json
index 6293c5270..e70326a32 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -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",
diff --git a/apps/web/public/locales/en/billing.json b/apps/web/public/locales/en/billing.json
index 36649d70f..3843dc1b3 100644
--- a/apps/web/public/locales/en/billing.json
+++ b/apps/web/public/locales/en/billing.json
@@ -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",
diff --git a/apps/web/public/locales/en/teams.json b/apps/web/public/locales/en/teams.json
index 5f77f57f7..7286a9302 100644
--- a/apps/web/public/locales/en/teams.json
+++ b/apps/web/public/locales/en/teams.json
@@ -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.",
diff --git a/packages/billing-gateway/src/components/embedded-checkout.tsx b/packages/billing-gateway/src/components/embedded-checkout.tsx
index 68672c93c..02466b229 100644
--- a/packages/billing-gateway/src/components/embedded-checkout.tsx
+++ b/packages/billing-gateway/src/components/embedded-checkout.tsx
@@ -78,7 +78,7 @@ function buildLazyComponent<
return (
- {/* @ts-expect-error */}
+ {/* @ts-expect-error: weird TS */}
[] {
const { t } = useTranslation('teams');
@@ -197,6 +199,7 @@ function ActionsDropdown({
setIsOpen={setIsUpdatingRole}
invitationId={invitation.id}
userRole={invitation.role}
+ userRoleHierarchy={permissions.currentUserRoleHierarchy}
/>
diff --git a/packages/features/team-accounts/src/components/invitations/update-invitation-dialog.tsx b/packages/features/team-accounts/src/components/invitations/update-invitation-dialog.tsx
index 5c97d6439..b8cef8942 100644
--- a/packages/features/team-accounts/src/components/invitations/update-invitation-dialog.tsx
+++ b/packages/features/team-accounts/src/components/invitations/update-invitation-dialog.tsx
@@ -4,7 +4,6 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
-import { Database } from '@kit/supabase/database';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { Button } from '@kit/ui/button';
import {
@@ -29,15 +28,17 @@ import { Trans } from '@kit/ui/trans';
import { UpdateRoleSchema } from '../../schema/update-role-schema';
import { updateInvitationAction } from '../../server/actions/team-invitations-server-actions';
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<{
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
invitationId: number;
userRole: Role;
-}> = ({ isOpen, setIsOpen, invitationId, userRole }) => {
+ userRoleHierarchy: number;
+}> = ({ isOpen, setIsOpen, invitationId, userRole, userRoleHierarchy }) => {
return (
@@ -51,11 +52,16 @@ export const UpdateInvitationDialog: React.FC<{
-
+
+ {(roles) => (
+
+ )}
+
);
@@ -64,10 +70,12 @@ export const UpdateInvitationDialog: React.FC<{
function UpdateInvitationForm({
invitationId,
userRole,
+ userRoleHierarchy,
setIsOpen,
}: React.PropsWithChildren<{
invitationId: number;
userRole: Role;
+ userRoleHierarchy: number;
setIsOpen: (isOpen: boolean) => void;
}>) {
const { t } = useTranslation('teams');
@@ -128,11 +136,18 @@ function UpdateInvitationForm({
- form.setValue('role', newRole)}
- />
+
+ {(roles) => (
+
+ form.setValue(field.name, newRole)
+ }
+ />
+ )}
+
diff --git a/packages/features/team-accounts/src/components/members/account-members-table.tsx b/packages/features/team-accounts/src/components/members/account-members-table.tsx
index e2c1cbd60..bce170118 100644
--- a/packages/features/team-accounts/src/components/members/account-members-table.tsx
+++ b/packages/features/team-accounts/src/components/members/account-members-table.tsx
@@ -28,25 +28,38 @@ import { UpdateMemberRoleDialog } from './update-member-role-dialog';
type Members =
Database['public']['Functions']['get_account_members']['Returns'];
+interface Permissions {
+ canUpdateRole: (roleHierarchy: number) => boolean;
+ canRemoveFromAccount: (roleHierarchy: number) => boolean;
+ canTransferOwnership: boolean;
+}
+
type AccountMembersTableProps = {
members: Members;
-
currentUserId: string;
-
- permissions: {
- canUpdateRole: boolean;
- canTransferOwnership: boolean;
- canRemoveFromAccount: boolean;
- };
+ userRoleHierarchy: number;
+ isPrimaryOwner: boolean;
+ canManageRoles: boolean;
};
export function AccountMembersTable({
members,
- permissions,
currentUserId,
+ isPrimaryOwner,
+ userRoleHierarchy,
+ canManageRoles,
}: AccountMembersTableProps) {
const [search, setSearch] = useState('');
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 filteredMembers = members.filter((member) => {
@@ -73,11 +86,7 @@ export function AccountMembersTable({
}
function useGetColumns(
- permissions: {
- canUpdateRole: boolean;
- canTransferOwnership: boolean;
- canRemoveFromAccount: boolean;
- },
+ permissions: Permissions,
currentUserId: string,
): ColumnDef[] {
const { t } = useTranslation('teams');
@@ -173,7 +182,7 @@ function ActionsDropdown({
member,
currentUserId,
}: {
- permissions: AccountMembersTableProps['permissions'];
+ permissions: Permissions;
member: Members[0];
currentUserId: string;
}) {
@@ -188,6 +197,22 @@ function ActionsDropdown({
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 (
<>
@@ -198,7 +223,7 @@ function ActionsDropdown({
-
+
setIsUpdatingRole(true)}>
@@ -210,7 +235,7 @@ function ActionsDropdown({
-
+
setIsRemoving(true)}>
@@ -234,6 +259,7 @@ function ActionsDropdown({
accountId={member.id}
userId={member.user_id}
userRole={member.role}
+ userRoleHierarchy={memberRoleHierarchy}
/>
diff --git a/packages/features/team-accounts/src/components/members/invite-members-dialog-container.tsx b/packages/features/team-accounts/src/components/members/invite-members-dialog-container.tsx
index 09682a641..987882cff 100644
--- a/packages/features/team-accounts/src/components/members/invite-members-dialog-container.tsx
+++ b/packages/features/team-accounts/src/components/members/invite-members-dialog-container.tsx
@@ -7,7 +7,6 @@ import { Plus, X } from 'lucide-react';
import { useFieldArray, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
-import { Database } from '@kit/supabase/database';
import { Button } from '@kit/ui/button';
import {
Dialog,
@@ -36,16 +35,19 @@ import { Trans } from '@kit/ui/trans';
import { InviteMembersSchema } from '../../schema/invite-members.schema';
import { createInvitationsAction } from '../../server/actions/team-invitations-server-actions';
import { MembershipRoleSelector } from './membership-role-selector';
+import { RolesDataProvider } from './roles-data-provider';
type InviteModel = ReturnType;
-type Role = Database['public']['Enums']['account_role'];
+type Role = string;
export function InviteMembersDialogContainer({
account,
+ userRoleHierarchy,
children,
}: React.PropsWithChildren<{
account: string;
+ userRoleHierarchy: number;
}>) {
const [pending, startTransition] = useTransition();
const [isOpen, setIsOpen] = useState(false);
@@ -65,19 +67,24 @@ export function InviteMembersDialogContainer({
- {
- startTransition(async () => {
- await createInvitationsAction({
- account,
- invitations: data.invitations,
- });
+
+ {(roles) => (
+ {
+ startTransition(async () => {
+ await createInvitationsAction({
+ account,
+ invitations: data.invitations,
+ });
- setIsOpen(false);
- });
- }}
- />
+ setIsOpen(false);
+ });
+ }}
+ />
+ )}
+
);
@@ -85,10 +92,12 @@ export function InviteMembersDialogContainer({
function InviteMembersForm({
onSubmit,
+ roles,
pending,
}: {
onSubmit: (data: { invitations: InviteModel[] }) => void;
pending: boolean;
+ roles: string[];
}) {
const { t } = useTranslation('teams');
@@ -156,6 +165,7 @@ function InviteMembersForm({
{
form.setValue(field.name, role);
diff --git a/packages/features/team-accounts/src/components/members/membership-role-selector.tsx b/packages/features/team-accounts/src/components/members/membership-role-selector.tsx
index 25461f567..6f3a329ed 100644
--- a/packages/features/team-accounts/src/components/members/membership-role-selector.tsx
+++ b/packages/features/team-accounts/src/components/members/membership-role-selector.tsx
@@ -1,4 +1,3 @@
-import { Database } from '@kit/supabase/database';
import {
Select,
SelectContent,
@@ -8,15 +7,14 @@ import {
} from '@kit/ui/select';
import { Trans } from '@kit/ui/trans';
-type Role = Database['public']['Enums']['account_role'];
+type Role = string;
export const MembershipRoleSelector: React.FC<{
+ roles: Role[];
value: Role;
currentUserRole?: Role;
onChange: (role: Role) => unknown;
-}> = ({ value, currentUserRole, onChange }) => {
- const rolesList: Role[] = ['owner', 'member'];
-
+}> = ({ roles, value, currentUserRole, onChange }) => {
return (
@@ -24,12 +22,12 @@ export const MembershipRoleSelector: React.FC<{
- {rolesList.map((role) => {
+ {roles.map((role) => {
return (
diff --git a/packages/features/team-accounts/src/components/members/role-badge.tsx b/packages/features/team-accounts/src/components/members/role-badge.tsx
index 2e749fd81..12544a3b3 100644
--- a/packages/features/team-accounts/src/components/members/role-badge.tsx
+++ b/packages/features/team-accounts/src/components/members/role-badge.tsx
@@ -1,10 +1,9 @@
import { cva } from 'class-variance-authority';
-import { Database } from '@kit/supabase/database';
import { Badge } from '@kit/ui/badge';
import { Trans } from '@kit/ui/trans';
-type Role = Database['public']['Enums']['account_role'];
+type Role = string;
const roleClassNameBuilder = cva('font-medium capitalize', {
variants: {
@@ -19,6 +18,7 @@ const roleClassNameBuilder = cva('font-medium capitalize', {
export const RoleBadge: React.FC<{
role: Role;
}> = ({ role }) => {
+ // @ts-expect-error: hard to type this since users can add custom roles
const className = roleClassNameBuilder({ role });
return (
diff --git a/packages/features/team-accounts/src/components/members/roles-data-provider.tsx b/packages/features/team-accounts/src/components/members/roles-data-provider.tsx
new file mode 100644
index 000000000..3d6e65970
--- /dev/null
+++ b/packages/features/team-accounts/src/components/members/roles-data-provider.tsx
@@ -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 ;
+ }
+
+ // 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);
+ },
+ });
+}
diff --git a/packages/features/team-accounts/src/components/members/update-member-role-dialog.tsx b/packages/features/team-accounts/src/components/members/update-member-role-dialog.tsx
index 5e2387f95..335a9fba4 100644
--- a/packages/features/team-accounts/src/components/members/update-member-role-dialog.tsx
+++ b/packages/features/team-accounts/src/components/members/update-member-role-dialog.tsx
@@ -4,7 +4,6 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
-import { Database } from '@kit/supabase/database';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { Button } from '@kit/ui/button';
import {
@@ -29,8 +28,9 @@ import { Trans } from '@kit/ui/trans';
import { UpdateRoleSchema } from '../../schema/update-role-schema';
import { updateMemberRoleAction } from '../../server/actions/team-members-server-actions';
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<{
isOpen: boolean;
@@ -38,7 +38,15 @@ export const UpdateMemberRoleDialog: React.FC<{
userId: string;
accountId: string;
userRole: Role;
-}> = ({ isOpen, setIsOpen, userId, accountId, userRole }) => {
+ userRoleHierarchy: number;
+}> = ({
+ isOpen,
+ setIsOpen,
+ userId,
+ accountId,
+ userRole,
+ userRoleHierarchy,
+}) => {
return (
@@ -52,12 +60,17 @@ export const UpdateMemberRoleDialog: React.FC<{
-
+
+ {(data) => (
+
+ )}
+
);
@@ -68,11 +81,13 @@ function UpdateMemberForm({
userRole,
accountId,
setIsOpen,
+ roles,
}: React.PropsWithChildren<{
userId: string;
userRole: Role;
accountId: string;
setIsOpen: (isOpen: boolean) => void;
+ roles: Role[];
}>) {
const [pending, startTransition] = useTransition();
const [error, setError] = useState();
@@ -128,6 +143,7 @@ function UpdateMemberForm({
form.setValue('role', newRole)}
diff --git a/packages/features/team-accounts/src/schema/invite-members.schema.ts b/packages/features/team-accounts/src/schema/invite-members.schema.ts
index 6e255244d..772265091 100644
--- a/packages/features/team-accounts/src/schema/invite-members.schema.ts
+++ b/packages/features/team-accounts/src/schema/invite-members.schema.ts
@@ -1,8 +1,6 @@
import { z } from 'zod';
-import { Database } from '@kit/supabase/database';
-
-type Role = Database['public']['Enums']['account_role'];
+type Role = string;
const InviteSchema = z.object({
email: z.string().email(),
diff --git a/packages/features/team-accounts/src/schema/update-invitation-schema.ts b/packages/features/team-accounts/src/schema/update-invitation-schema.ts
index 44674f13a..f833fdd55 100644
--- a/packages/features/team-accounts/src/schema/update-invitation-schema.ts
+++ b/packages/features/team-accounts/src/schema/update-invitation-schema.ts
@@ -1,8 +1,6 @@
import { z } from 'zod';
-import { Database } from '@kit/supabase/database';
-
-type Role = Database['public']['Enums']['account_role'];
+type Role = string;
export const UpdateInvitationSchema = z.object({
invitationId: z.number(),
diff --git a/packages/features/team-accounts/src/schema/update-role-schema.ts b/packages/features/team-accounts/src/schema/update-role-schema.ts
index e07901d34..ab0b727f3 100644
--- a/packages/features/team-accounts/src/schema/update-role-schema.ts
+++ b/packages/features/team-accounts/src/schema/update-role-schema.ts
@@ -1,9 +1,5 @@
import { z } from 'zod';
-import { Database } from '@kit/supabase/database';
-
-type Role = Database['public']['Enums']['account_role'];
-
export const UpdateRoleSchema = z.object({
- role: z.custom((value) => z.string().parse(value)),
+ role: z.string().min(1),
});
diff --git a/packages/features/team-accounts/src/server/actions/team-members-server-actions.ts b/packages/features/team-accounts/src/server/actions/team-members-server-actions.ts
index b86c08055..344ab7841 100644
--- a/packages/features/team-accounts/src/server/actions/team-members-server-actions.ts
+++ b/packages/features/team-accounts/src/server/actions/team-members-server-actions.ts
@@ -31,7 +31,7 @@ export async function removeMemberFromAccountAction(params: {
export async function updateMemberRoleAction(params: {
accountId: string;
userId: string;
- role: Database['public']['Enums']['account_role'];
+ role: string;
}) {
const client = getSupabaseServerActionClient();
diff --git a/packages/features/team-accounts/src/server/services/account-members.service.ts b/packages/features/team-accounts/src/server/services/account-members.service.ts
index 8eb49ce06..d2d789820 100644
--- a/packages/features/team-accounts/src/server/services/account-members.service.ts
+++ b/packages/features/team-accounts/src/server/services/account-members.service.ts
@@ -26,7 +26,7 @@ export class AccountMembersService {
async updateMemberRole(params: {
accountId: string;
userId: string;
- role: Database['public']['Enums']['account_role'];
+ role: string;
}) {
const { data, error } = await this.client
.from('accounts_memberships')
diff --git a/packages/features/team-accounts/src/server/services/leave-account.service.ts b/packages/features/team-accounts/src/server/services/leave-account.service.ts
index 178470afd..0ce694485 100644
--- a/packages/features/team-accounts/src/server/services/leave-account.service.ts
+++ b/packages/features/team-accounts/src/server/services/leave-account.service.ts
@@ -1,13 +1,19 @@
import { SupabaseClient } from '@supabase/supabase-js';
import 'server-only';
+import { z } from 'zod';
import { Database } from '@kit/supabase/database';
+import { LeaveTeamAccountSchema } from '../../schema/leave-team-account.schema';
+
export class LeaveAccountService {
constructor(private readonly client: SupabaseClient) {}
- async leaveTeamAccount(params: { accountId: string; userId: string }) {
+ async leaveTeamAccount(params: z.infer) {
+ await Promise.resolve();
+
+ console.log(params);
// TODO
// implement this method
}
diff --git a/packages/mailers/src/impl/cloudflare/index.ts b/packages/mailers/src/impl/cloudflare/index.ts
index cf500b784..6d4510738 100644
--- a/packages/mailers/src/impl/cloudflare/index.ts
+++ b/packages/mailers/src/impl/cloudflare/index.ts
@@ -12,6 +12,9 @@ type Config = z.infer;
*/
export class CloudflareMailer implements Mailer {
async sendEmail(config: Config) {
+ // make lint happy for now
+ await Promise.resolve();
+
console.log('Sending email with Cloudflare Workers', config);
throw new Error('Not implemented');
}
diff --git a/packages/stripe/src/services/stripe-webhook-handler.service.ts b/packages/stripe/src/services/stripe-webhook-handler.service.ts
index d1ced848f..22b3aa8b1 100644
--- a/packages/stripe/src/services/stripe-webhook-handler.service.ts
+++ b/packages/stripe/src/services/stripe-webhook-handler.service.ts
@@ -180,7 +180,7 @@ export class StripeWebhookHandlerService
const { subscription } = params;
const lineItem = subscription.items.data[0];
const price = lineItem?.price;
- const priceId = price?.id!;
+ const priceId = price?.id as string;
const interval = price?.recurring?.interval ?? null;
const active =
@@ -194,8 +194,8 @@ export class StripeWebhookHandlerService
price_amount: params.amount,
cancel_at_period_end: subscription.cancel_at_period_end ?? false,
interval: interval as string,
- currency: price?.currency as string,
- product_id: price?.product as string,
+ currency: (price as Stripe.Price).currency,
+ product_id: (price as Stripe.Price).product,
variant_id: priceId,
interval_count: price?.recurring?.interval_count ?? 1,
period_starts_at: getISOString(subscription.current_period_start),
diff --git a/packages/supabase/package.json b/packages/supabase/package.json
index 70844e46d..7fac9a8be 100644
--- a/packages/supabase/package.json
+++ b/packages/supabase/package.json
@@ -28,6 +28,7 @@
"@kit/prettier-config": "workspace:*",
"@kit/tailwind-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
+ "@supabase/gotrue-js": "2.62.2",
"@supabase/ssr": "^0.1.0",
"@supabase/supabase-js": "^2.41.1",
"@tanstack/react-query": "5.28.6"
diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts
index 0b2a72f1c..29a1b133a 100644
--- a/packages/supabase/src/database.types.ts
+++ b/packages/supabase/src/database.types.ts
@@ -38,17 +38,17 @@ export type Database = {
Row: {
account_id: string;
id: number;
- role: Database['public']['Enums']['account_role'];
+ role: string;
};
Insert: {
account_id: string;
id?: number;
- role: Database['public']['Enums']['account_role'];
+ role: string;
};
Update: {
account_id?: string;
id?: number;
- role?: Database['public']['Enums']['account_role'];
+ role?: string;
};
Relationships: [
{
@@ -72,6 +72,13 @@ export type Database = {
referencedRelation: 'user_accounts';
referencedColumns: ['id'];
},
+ {
+ foreignKeyName: 'account_roles_role_fkey';
+ columns: ['role'];
+ isOneToOne: false;
+ referencedRelation: 'roles';
+ referencedColumns: ['name'];
+ },
];
};
accounts: {
@@ -141,7 +148,7 @@ export type Database = {
accounts_memberships: {
Row: {
account_id: string;
- account_role: Database['public']['Enums']['account_role'];
+ account_role: string;
created_at: string;
created_by: string | null;
updated_at: string;
@@ -150,7 +157,7 @@ export type Database = {
};
Insert: {
account_id: string;
- account_role: Database['public']['Enums']['account_role'];
+ account_role: string;
created_at?: string;
created_by?: string | null;
updated_at?: string;
@@ -159,7 +166,7 @@ export type Database = {
};
Update: {
account_id?: string;
- account_role?: Database['public']['Enums']['account_role'];
+ account_role?: string;
created_at?: string;
created_by?: string | null;
updated_at?: string;
@@ -188,6 +195,13 @@ export type Database = {
referencedRelation: 'user_accounts';
referencedColumns: ['id'];
},
+ {
+ foreignKeyName: 'accounts_memberships_account_role_fkey';
+ columns: ['account_role'];
+ isOneToOne: false;
+ referencedRelation: 'roles';
+ referencedColumns: ['name'];
+ },
{
foreignKeyName: 'accounts_memberships_created_by_fkey';
columns: ['created_by'];
@@ -287,7 +301,7 @@ export type Database = {
id: number;
invite_token: string;
invited_by: string;
- role: Database['public']['Enums']['account_role'];
+ role: string;
updated_at: string;
};
Insert: {
@@ -298,7 +312,7 @@ export type Database = {
id?: number;
invite_token: string;
invited_by: string;
- role: Database['public']['Enums']['account_role'];
+ role: string;
updated_at?: string;
};
Update: {
@@ -309,7 +323,7 @@ export type Database = {
id?: number;
invite_token?: string;
invited_by?: string;
- role?: Database['public']['Enums']['account_role'];
+ role?: string;
updated_at?: string;
};
Relationships: [
@@ -341,25 +355,83 @@ export type Database = {
referencedRelation: 'users';
referencedColumns: ['id'];
},
+ {
+ foreignKeyName: 'invitations_role_fkey';
+ columns: ['role'];
+ isOneToOne: false;
+ referencedRelation: 'roles';
+ referencedColumns: ['name'];
+ },
];
};
role_permissions: {
Row: {
id: number;
permission: Database['public']['Enums']['app_permissions'];
- role: Database['public']['Enums']['account_role'];
+ role: string;
};
Insert: {
id?: number;
permission: Database['public']['Enums']['app_permissions'];
- role: Database['public']['Enums']['account_role'];
+ role: string;
};
Update: {
id?: number;
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: {
Row: {
@@ -474,10 +546,18 @@ export type Database = {
id: string | null;
name: string | null;
picture_url: string | null;
- role: Database['public']['Enums']['account_role'] | null;
+ role: string | null;
slug: string | null;
};
- Relationships: [];
+ Relationships: [
+ {
+ foreignKeyName: 'accounts_memberships_account_role_fkey';
+ columns: ['role'];
+ isOneToOne: false;
+ referencedRelation: 'roles';
+ referencedColumns: ['name'];
+ },
+ ];
};
};
Functions: {
@@ -559,7 +639,7 @@ export type Database = {
Args: {
account_id: string;
email: string;
- role: Database['public']['Enums']['account_role'];
+ role: string;
};
Returns: {
account_id: string;
@@ -569,7 +649,7 @@ export type Database = {
id: number;
invite_token: string;
invited_by: string;
- role: Database['public']['Enums']['account_role'];
+ role: string;
updated_at: string;
};
};
@@ -582,7 +662,7 @@ export type Database = {
email: string;
account_id: string;
invited_by: string;
- role: Database['public']['Enums']['account_role'];
+ role: string;
created_at: string;
updated_at: string;
expires_at: string;
@@ -598,7 +678,8 @@ export type Database = {
id: string;
user_id: string;
account_id: string;
- role: Database['public']['Enums']['account_role'];
+ role: string;
+ role_hierarchy_level: number;
primary_owner_user_id: string;
name: string;
email: string;
@@ -627,6 +708,14 @@ export type Database = {
updated_by: string | null;
}[];
};
+ has_more_elevated_role: {
+ Args: {
+ target_user_id: string;
+ target_account_id: string;
+ role_name: string;
+ };
+ Returns: boolean;
+ };
has_permission: {
Args: {
user_id: string;
@@ -638,7 +727,7 @@ export type Database = {
has_role_on_account: {
Args: {
account_id: string;
- account_role?: Database['public']['Enums']['account_role'];
+ account_role?: string;
};
Returns: boolean;
};
@@ -670,7 +759,8 @@ export type Database = {
name: string;
picture_url: string;
slug: string;
- role: Database['public']['Enums']['account_role'];
+ role: string;
+ role_hierarchy_level: number;
primary_owner_user_id: string;
subscription_status: Database['public']['Enums']['subscription_status'];
permissions: Database['public']['Enums']['app_permissions'][];
@@ -690,7 +780,6 @@ export type Database = {
};
};
Enums: {
- account_role: 'owner' | 'member';
app_permissions:
| 'roles.manage'
| 'billing.manage'
diff --git a/packages/supabase/src/hooks/use-sign-in-with-otp.ts b/packages/supabase/src/hooks/use-sign-in-with-otp.ts
index 7b410a5a4..04bec38ca 100644
--- a/packages/supabase/src/hooks/use-sign-in-with-otp.ts
+++ b/packages/supabase/src/hooks/use-sign-in-with-otp.ts
@@ -1,7 +1,4 @@
-import type {
- AuthError,
- SignInWithPasswordlessCredentials,
-} from '@supabase/gotrue-js';
+import type { SignInWithPasswordlessCredentials } from '@supabase/gotrue-js';
import { useMutation } from '@tanstack/react-query';
@@ -15,7 +12,7 @@ export function useSignInWithOtp() {
const result = await client.auth.signInWithOtp(credentials);
if (result.error) {
- if (shouldIgnoreError(result.error)) {
+ if (shouldIgnoreError(result.error.message)) {
console.warn(
`Ignoring error during development: ${result.error.message}`,
);
@@ -37,10 +34,10 @@ export function useSignInWithOtp() {
export default useSignInWithOtp;
-function shouldIgnoreError(error: AuthError) {
+function shouldIgnoreError(error: string) {
return isSmsProviderNotSetupError(error);
}
-function isSmsProviderNotSetupError(error: AuthError) {
- return error.message.includes(`sms Provider could not be found`);
+function isSmsProviderNotSetupError(error: string) {
+ return error.includes(`sms Provider could not be found`);
}
diff --git a/packages/ui/src/makerkit/hooks/use-sidebar-state.ts b/packages/ui/src/makerkit/hooks/use-sidebar-state.ts
deleted file mode 100644
index eafbf447a..000000000
--- a/packages/ui/src/makerkit/hooks/use-sidebar-state.ts
+++ /dev/null
@@ -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;
diff --git a/packages/ui/src/makerkit/image-upload-input.tsx b/packages/ui/src/makerkit/image-upload-input.tsx
index 774aa53e6..9399ccb1c 100644
--- a/packages/ui/src/makerkit/image-upload-input.tsx
+++ b/packages/ui/src/makerkit/image-upload-input.tsx
@@ -138,7 +138,6 @@ export const ImageUploadInput = forwardRef, Props>(
}
return (
- // eslint-disable-next-line jsx-a11y/label-has-associated-control
- {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
diff --git a/packages/ui/src/makerkit/mdx/mdx-renderer.tsx b/packages/ui/src/makerkit/mdx/mdx-renderer.tsx
index 73931d83e..9375555d8 100644
--- a/packages/ui/src/makerkit/mdx/mdx-renderer.tsx
+++ b/packages/ui/src/makerkit/mdx/mdx-renderer.tsx
@@ -2,7 +2,7 @@ import type { MDXComponents } from 'mdx/types';
import { getMDXComponent } from 'next-contentlayer/hooks';
import Components from './mdx-components';
-// @ts-ignore
+// @ts-expect-error: weird typescript error with css modules
import styles from './mdx-renderer.module.css';
export function Mdx({
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1b938ac0e..05adf289c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -586,6 +586,9 @@ importers:
'@kit/tsconfig':
specifier: workspace:*
version: link:../../tooling/typescript
+ '@supabase/gotrue-js':
+ specifier: 2.62.2
+ version: 2.62.2
'@supabase/ssr':
specifier: ^0.1.0
version: 0.1.0(@supabase/supabase-js@2.41.1)
@@ -3978,6 +3981,12 @@ packages:
dependencies:
'@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:
resolution: {integrity: sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==}
engines: {node: 4.x || >=6.0.0}
diff --git a/supabase/migrations/20221215192558_schema.sql b/supabase/migrations/20221215192558_schema.sql
index 7e34782df..d06c0dc47 100644
--- a/supabase/migrations/20221215192558_schema.sql
+++ b/supabase/migrations/20221215192558_schema.sql
@@ -86,13 +86,6 @@ grant usage on schema public to service_role;
* 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
@@ -396,6 +389,34 @@ after
update of email on auth.users for each row
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
@@ -407,7 +428,7 @@ create table if not exists
public.accounts_memberships (
user_id uuid references auth.users 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,
updated_at timestamptz default current_timestamp not null,
created_by uuid references auth.users,
@@ -435,7 +456,7 @@ alter table public.accounts_memberships enable row level security;
create
or replace function public.has_role_on_account (
account_id uuid,
- account_role public.account_role default null
+ account_role varchar(50) default null
) returns boolean language sql security definer
set
search_path = public as $$
@@ -453,7 +474,7 @@ set
$$;
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
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 $$
declare
permission_granted boolean;
- target_user_role public.account_role;
+ target_user_hierarchy_level int;
+ current_user_hierarchy_level int;
begin
-- validate the auth user has the required permission on the account
-- to manage members of the account
@@ -494,20 +516,12 @@ begin
raise exception 'You cannot remove yourself from the account';
end if;
- -- retrieve the user target role in the account
- select
- 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;
+ select hierarchy_level into target_user_hierarchy_level from public.roles where name = target_user_role;
+ 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());
- -- check if the target user is the owner of the account
- if target_user_role = 'owner' then
- raise exception 'You cannot remove the primary owner from the account';
+ -- check if the current user has a higher hierarchy level than the target user
+ if current_user_hierarchy_level <= target_user_hierarchy_level then
+ raise exception 'You do not have permission to remove this user from the account';
end if;
return true;
@@ -569,7 +583,7 @@ create table
public.account_roles (
id bigint generated by default as identity primary key,
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)
);
@@ -608,7 +622,7 @@ select
create table if not exists
public.role_permissions (
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,
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;
+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
alter table public.role_permissions enable row level security;
@@ -672,7 +714,7 @@ create table if not exists
email varchar(255) 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,
- role public.account_role not null,
+ role varchar(50) references public.roles (name) not null,
invite_token varchar(255) unique not null,
created_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));
-- 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
insert
to authenticated with check (
- has_role_on_account (account_id)
- and public.has_permission (auth.uid (), account_id, 'invites.manage'::app_permissions));
+ public.has_permission (auth.uid (), account_id, 'invites.manage'::app_permissions) and
+ 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
--- 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
update
to authenticated using (
- has_role_on_account (account_id)
- and public.has_permission (auth.uid (), account_id, 'invites.manage'::app_permissions)
+ public.has_permission (auth.uid (), account_id, 'invites.manage'::app_permissions)
+ and public.has_more_elevated_role (auth.uid (), account_id, role)
) with check (
- has_role_on_account (account_id)
- and public.has_permission (auth.uid (), account_id, 'invites.manage'::app_permissions)
+ 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
@@ -761,7 +804,7 @@ delete
create or replace function accept_invitation(token text, user_id uuid) returns void as $$
declare
target_account_id uuid;
- target_role public.account_role;
+ target_role varchar(50);
begin
select
account_id,
@@ -1192,7 +1235,7 @@ create
or replace function public.create_invitation (
account_id uuid,
email text,
- role public.account_role
+ role varchar(50)
) returns public.invitations as $$
declare
new_invitation public.invitations;
@@ -1283,7 +1326,8 @@ or replace function public.organization_account_workspace (account_slug text) re
name varchar(255),
picture_url varchar(1000),
slug text,
- role public.account_role,
+ role varchar(50),
+ role_hierarchy_level int,
primary_owner_user_id uuid,
subscription_status public.subscription_status,
permissions public.app_permissions[]
@@ -1296,6 +1340,7 @@ begin
accounts.picture_url,
accounts.slug,
accounts_memberships.account_role,
+ roles.hierarchy_level,
accounts.primary_owner_user_id,
subscriptions.status,
array_agg(role_permissions.permission)
@@ -1305,13 +1350,15 @@ begin
left join public.subscriptions on accounts.id = subscriptions.account_id
left join public.role_permissions on accounts_memberships.account_role =
role_permissions.role
+ left join public.roles on accounts_memberships.account_role = roles.name
where
accounts.slug = account_slug
and public.accounts_memberships.user_id = auth.uid()
group by
accounts.id,
accounts_memberships.account_role,
- subscriptions.status;
+ subscriptions.status,
+ roles.hierarchy_level;
end;
$$ language plpgsql;
@@ -1324,7 +1371,8 @@ OR REPLACE FUNCTION public.get_account_members (account_slug text) RETURNS TABLE
id uuid,
user_id uuid,
account_id uuid,
- role public.account_role,
+ role varchar(50),
+ role_hierarchy_level int,
primary_owner_user_id uuid,
name varchar,
email varchar,
@@ -1334,10 +1382,11 @@ OR REPLACE FUNCTION public.get_account_members (account_slug text) RETURNS TABLE
) LANGUAGE plpgsql AS $$
BEGIN
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
JOIN public.accounts a ON a.id = am.account_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;
END;
$$;
@@ -1351,7 +1400,7 @@ create or replace function public.get_account_invitations(account_slug text) ret
email varchar(255),
account_id uuid,
invited_by uuid,
- role public.account_role,
+ role varchar(50),
created_at timestamptz,
updated_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 (
email text,
- role public.account_role
+ role varchar(50)
);
-- Then, modify your function to use this type
@@ -1394,7 +1443,7 @@ DECLARE
all_invitations public.invitations[] := ARRAY[]::public.invitations[];
invite_token text;
email text;
- role public.account_role;
+ role varchar(50);
BEGIN
FOREACH email, role IN ARRAY invitations
LOOP
diff --git a/tooling/eslint/base.js b/tooling/eslint/base.js
index 65c851e1a..68fb2be04 100644
--- a/tooling/eslint/base.js
+++ b/tooling/eslint/base.js
@@ -23,6 +23,7 @@ const config = {
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/consistent-type-definitions': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
+ '@typescript-eslint/non-nullable-type-assertion-style': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
diff --git a/tooling/eslint/package.json b/tooling/eslint/package.json
index a99946908..3d6730ffc 100644
--- a/tooling/eslint/package.json
+++ b/tooling/eslint/package.json
@@ -23,7 +23,6 @@
"eslint-config-prettier": "^9.1.0",
"eslint-config-turbo": "^1.13.0",
"eslint-plugin-import": "^2.29.1",
- "eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0"
},
diff --git a/tooling/eslint/react.js b/tooling/eslint/react.js
index 34a289f84..904632076 100644
--- a/tooling/eslint/react.js
+++ b/tooling/eslint/react.js
@@ -1,10 +1,6 @@
/** @type {import('eslint').Linter.Config} */
const config = {
- extends: [
- 'plugin:react/recommended',
- 'plugin:react-hooks/recommended',
- 'plugin:jsx-a11y/recommended',
- ],
+ extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended'],
rules: {
'react/prop-types': 'off',
},