diff --git a/apps/web/app/(dashboard)/home/[account]/billing/layout.tsx b/apps/web/app/(dashboard)/home/[account]/billing/layout.tsx index 36bd91250..e6657eb86 100644 --- a/apps/web/app/(dashboard)/home/[account]/billing/layout.tsx +++ b/apps/web/app/(dashboard)/home/[account]/billing/layout.tsx @@ -2,8 +2,8 @@ import { notFound } from 'next/navigation'; import featureFlagsConfig from '~/config/feature-flags.config'; -function OrganizationAccountBillingLayout(props: React.PropsWithChildren) { - const isEnabled = featureFlagsConfig.enableOrganizationBilling; +function TeamAccountBillingLayout(props: React.PropsWithChildren) { + const isEnabled = featureFlagsConfig.enableTeamAccountBilling; if (!isEnabled) { notFound(); @@ -12,4 +12,4 @@ function OrganizationAccountBillingLayout(props: React.PropsWithChildren) { return <>{props.children}; } -export default OrganizationAccountBillingLayout; +export default TeamAccountBillingLayout; diff --git a/apps/web/app/(dashboard)/home/[account]/billing/page.tsx b/apps/web/app/(dashboard)/home/[account]/billing/page.tsx index 75a9ac0ae..bff7a8661 100644 --- a/apps/web/app/(dashboard)/home/[account]/billing/page.tsx +++ b/apps/web/app/(dashboard)/home/[account]/billing/page.tsx @@ -21,7 +21,7 @@ interface Params { }; } -async function OrganizationAccountBillingPage({ params }: Params) { +async function TeamAccountBillingPage({ params }: Params) { const workspace = await loadTeamWorkspace(params.account); const accountId = workspace.account.id; const [subscription, customerId] = await loadAccountData(accountId); @@ -60,7 +60,7 @@ async function OrganizationAccountBillingPage({ params }: Params) { ); } -export default withI18n(OrganizationAccountBillingPage); +export default withI18n(TeamAccountBillingPage); async function loadAccountData(accountId: string) { const client = getSupabaseServerComponentClient(); diff --git a/apps/web/app/(dashboard)/home/[account]/layout.tsx b/apps/web/app/(dashboard)/home/[account]/layout.tsx index 16d0c6df1..5f88f62b8 100644 --- a/apps/web/app/(dashboard)/home/[account]/layout.tsx +++ b/apps/web/app/(dashboard)/home/[account]/layout.tsx @@ -13,7 +13,7 @@ interface Params { account: string; } -function OrganizationWorkspaceLayout({ +function TeamWorkspaceLayout({ children, params, }: React.PropsWithChildren<{ @@ -48,7 +48,7 @@ function OrganizationWorkspaceLayout({ ); } -export default withI18n(OrganizationWorkspaceLayout); +export default withI18n(TeamWorkspaceLayout); function getUIStateCookies() { return { diff --git a/apps/web/app/(dashboard)/home/[account]/members/page.tsx b/apps/web/app/(dashboard)/home/[account]/members/page.tsx index 42d73b816..36977dd58 100644 --- a/apps/web/app/(dashboard)/home/[account]/members/page.tsx +++ b/apps/web/app/(dashboard)/home/[account]/members/page.tsx @@ -56,7 +56,7 @@ async function loadInvitations(account: string) { return data ?? []; } -async function OrganizationAccountMembersPage({ params }: Params) { +async function TeamAccountMembersPage({ params }: Params) { const slug = params.account; const [{ account, user }, members, invitations] = await Promise.all([ @@ -142,4 +142,4 @@ async function OrganizationAccountMembersPage({ params }: Params) { ); } -export default withI18n(OrganizationAccountMembersPage); +export default withI18n(TeamAccountMembersPage); diff --git a/apps/web/app/(dashboard)/home/[account]/page.tsx b/apps/web/app/(dashboard)/home/[account]/page.tsx index 820e36cea..5e4329d46 100644 --- a/apps/web/app/(dashboard)/home/[account]/page.tsx +++ b/apps/web/app/(dashboard)/home/[account]/page.tsx @@ -35,7 +35,7 @@ export const metadata = { title: 'Organization Account Home', }; -function OrganizationAccountHomePage({ +function TeamAccountHomePage({ params, }: { params: { @@ -62,4 +62,4 @@ function OrganizationAccountHomePage({ ); } -export default withI18n(OrganizationAccountHomePage); +export default withI18n(TeamAccountHomePage); diff --git a/apps/web/config/organization-account-sidebar.config.tsx b/apps/web/config/organization-account-sidebar.config.tsx index 60fa02dce..f8668ec79 100644 --- a/apps/web/config/organization-account-sidebar.config.tsx +++ b/apps/web/config/organization-account-sidebar.config.tsx @@ -28,7 +28,7 @@ const routes = (account: string) => [ path: createPath(pathsConfig.app.accountMembers, account), Icon: , }, - featureFlagsConfig.enableOrganizationBilling + featureFlagsConfig.enableTeamAccountBilling ? { label: 'common:billingTabLabel', path: createPath(pathsConfig.app.accountBilling, account), diff --git a/apps/web/public/locales/en/common.json b/apps/web/public/locales/en/common.json index 92ef71bc2..4efdbf74e 100644 --- a/apps/web/public/locales/en/common.json +++ b/apps/web/public/locales/en/common.json @@ -2,6 +2,7 @@ "homeTabLabel": "Home", "homeTabDescription": "Welcome to your home page", "accountMembers": "Members", + "membersTabDescription": "Manage your organization's members", "billingTabLabel": "Billing", "billingTabDescription": "Manage your billing and subscription", "yourAccountTabLabel": "Account Settings", diff --git a/packages/features/accounts/src/server/services/delete-personal-account.service.ts b/packages/features/accounts/src/server/services/delete-personal-account.service.ts index 9bf3f22c1..cb1b5f38b 100644 --- a/packages/features/accounts/src/server/services/delete-personal-account.service.ts +++ b/packages/features/accounts/src/server/services/delete-personal-account.service.ts @@ -35,25 +35,30 @@ export class DeletePersonalAccountService { productName: string; }; }) { + const userId = params.userId; + Logger.info( - { userId: params.userId, name: this.namespace }, + { name: this.namespace, userId }, 'User requested deletion. Processing...', ); // Cancel all user subscriptions const billingService = new AccountBillingService(params.adminClient); - await billingService.cancelAllAccountSubscriptions(params.userId); + await billingService.cancelAllAccountSubscriptions({ + userId, + accountId: userId, + }); // execute the deletion of the user try { - await params.adminClient.auth.admin.deleteUser(params.userId); + await params.adminClient.auth.admin.deleteUser(userId); } catch (error) { Logger.error( { - userId: params.userId, - error, name: this.namespace, + userId, + error, }, 'Error deleting user', ); @@ -66,8 +71,8 @@ export class DeletePersonalAccountService { try { Logger.info( { - userId: params.userId, name: this.namespace, + userId, }, `Sending account deletion email...`, ); @@ -81,8 +86,8 @@ export class DeletePersonalAccountService { } catch (error) { Logger.error( { - userId: params.userId, name: this.namespace, + userId, error, }, `Error sending account deletion email`, diff --git a/packages/features/team-accounts/src/server/actions/delete-team-account-server-actions.ts b/packages/features/team-accounts/src/server/actions/delete-team-account-server-actions.ts index 6bff8cbeb..0e8af1895 100644 --- a/packages/features/team-accounts/src/server/actions/delete-team-account-server-actions.ts +++ b/packages/features/team-accounts/src/server/actions/delete-team-account-server-actions.ts @@ -17,6 +17,11 @@ export async function deleteTeamAccountAction(formData: FormData) { ); const client = getSupabaseServerActionClient(); + const auth = await requireAuth(client); + + if (auth.error) { + throw new Error('Authentication required'); + } // Check if the user has the necessary permissions to delete the team account await assertUserPermissionsToDeleteTeamAccount(client, params.accountId); @@ -29,7 +34,10 @@ export async function deleteTeamAccountAction(formData: FormData) { getSupabaseServerActionClient({ admin: true, }), - params, + { + accountId: params.accountId, + userId: auth.data.user.id, + }, ); return redirect('/home'); diff --git a/packages/features/team-accounts/src/server/services/delete-team-account.service.ts b/packages/features/team-accounts/src/server/services/delete-team-account.service.ts index 953ebe0db..d42f95e03 100644 --- a/packages/features/team-accounts/src/server/services/delete-team-account.service.ts +++ b/packages/features/team-accounts/src/server/services/delete-team-account.service.ts @@ -20,12 +20,16 @@ export class DeleteTeamAccountService { */ async deleteTeamAccount( adminClient: SupabaseClient, - params: { accountId: string }, + params: { + accountId: string; + userId: string; + }, ) { Logger.info( { name: this.namespace, accountId: params.accountId, + userId: params.userId, }, `Requested team account deletion. Processing...`, ); @@ -34,6 +38,7 @@ export class DeleteTeamAccountService { { name: this.namespace, accountId: params.accountId, + userId: params.userId, }, `Deleting all account subscriptions...`, ); @@ -41,7 +46,7 @@ export class DeleteTeamAccountService { // First - we want to cancel all Stripe active subscriptions const billingService = new AccountBillingService(adminClient); - await billingService.cancelAllAccountSubscriptions(params.accountId); + await billingService.cancelAllAccountSubscriptions(params); // now we can use the admin client to delete the account. const { error } = await adminClient @@ -54,6 +59,7 @@ export class DeleteTeamAccountService { { name: this.namespace, accountId: params.accountId, + userId: params.userId, error, }, 'Failed to delete team account', @@ -66,6 +72,7 @@ export class DeleteTeamAccountService { { name: this.namespace, accountId: params.accountId, + userId: params.userId, }, 'Successfully deleted team account', ); diff --git a/packages/ui/src/shadcn/badge.tsx b/packages/ui/src/shadcn/badge.tsx index 12411df06..a2d542b29 100644 --- a/packages/ui/src/shadcn/badge.tsx +++ b/packages/ui/src/shadcn/badge.tsx @@ -17,10 +17,10 @@ const badgeVariants = cva( 'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80', outline: 'text-foreground', success: - 'border-transparent bg-green-50 text-green-500 dark:bg-green-500/20', + 'border-transparent bg-green-50 hover:bg-green-50 text-green-500 dark:bg-green-500/20 dark:hover:bg-green-500/20', warning: - 'border-transparent bg-orange-50 text-orange-500 dark:bg-transparent', - info: 'border-transparent bg-blue-50 text-blue-500 dark:bg-transparent', + 'border-transparent bg-orange-50 hover:bg-orange-50 text-orange-500 dark:bg-orange-500/20 dark:hover:bg-orange-500/20', + info: 'border-transparent bg-blue-50 hover:bg-blue-50 text-blue-500 dark:bg-blue-500/20 dark:hover:bg-blue-500/20', }, }, defaultVariants: { diff --git a/supabase/migrations/20221215192558_schema.sql b/supabase/migrations/20221215192558_schema.sql index 9f513c636..acd37e264 100644 --- a/supabase/migrations/20221215192558_schema.sql +++ b/supabase/migrations/20221215192558_schema.sql @@ -516,7 +516,7 @@ begin $$ language plpgsql; -grant execute on function kit.can_remove_account_member (uuid, uuid) to authenticated, postgres; +grant execute on function kit.can_remove_account_member (uuid, uuid) to authenticated, service_role; -- RLS -- SELECT: Users can read their team members account memberships @@ -649,7 +649,7 @@ begin $$ language plpgsql; -grant execute on function public.has_permission (uuid, uuid, public.app_permissions) to authenticated, postgres; +grant execute on function public.has_permission (uuid, uuid, public.app_permissions) to authenticated, service_role; -- Enable RLS on the role_permissions table alter table public.role_permissions enable row level security; @@ -1116,7 +1116,7 @@ end; $$ language plpgsql; grant -execute on function public.create_account (text) to authenticated; +execute on function public.create_account (text) to authenticated, service_role; -- RLS -- Authenticated users can create organization accounts @@ -1194,7 +1194,7 @@ where grant select on public.user_account_workspace to authenticated, - postgres; + service_role; create or replace view public.user_accounts as @@ -1214,7 +1214,7 @@ where grant select on public.user_accounts to authenticated, - postgres; + service_role; create or replace function public.organization_account_workspace (account_slug text) returns table ( @@ -1256,7 +1256,7 @@ $$ language plpgsql; grant execute on function public.organization_account_workspace (text) to authenticated, -postgres; +service_role; CREATE OR REPLACE FUNCTION public.get_account_members (account_slug text) RETURNS TABLE ( @@ -1283,7 +1283,7 @@ $$; grant execute on function public.get_account_members (text) to authenticated, -postgres; +service_role; create or replace function public.get_account_invitations(account_slug text) returns table ( id integer, @@ -1316,7 +1316,7 @@ begin end; $$ language plpgsql; -grant execute on function public.get_account_invitations (text) to authenticated, postgres; +grant execute on function public.get_account_invitations (text) to authenticated, service_role; CREATE TYPE kit.invitation AS ( email text, @@ -1359,7 +1359,7 @@ BEGIN END; $$ LANGUAGE plpgsql; -grant execute on function public.add_invitations_to_account (text, kit.invitation[]) to authenticated, postgres; +grant execute on function public.add_invitations_to_account (text, kit.invitation[]) to authenticated, service_role; -- Storage -- Account Image @@ -1368,25 +1368,27 @@ insert into values ('account_image', 'account_image', true); +create or replace function kit.get_storage_filename_as_uuid (name text) returns uuid as $$ +begin + return replace( + storage.filename (name), + concat('.', storage.extension (name)), + '' + )::uuid; +end; +$$ language plpgsql; + +grant execute on function kit.get_storage_filename_as_uuid (text) to authenticated, service_role; + -- RLS policies for storage create policy account_image on storage.objects for all using ( bucket_id = 'account_image' - and ( - replace( - storage.filename (name), - concat('.', storage.extension (name)), - '' - )::uuid - ) = auth.uid () + and kit.get_storage_filename_as_uuid(name) = auth.uid () or + public.has_role_on_account(kit.get_storage_filename_as_uuid(name)) ) with check ( bucket_id = 'account_image' - and ( - replace( - storage.filename (name), - concat('.', storage.extension (name)), - '' - )::uuid - ) = auth.uid () + and kit.get_storage_filename_as_uuid(name) = auth.uid () or + public.has_permission(auth.uid(), kit.get_storage_filename_as_uuid(name), 'settings.manage') ); \ No newline at end of file