From 9796f109bad4a5b58d6e26907cd483d38aacaf71 Mon Sep 17 00:00:00 2001 From: giancarlo Date: Thu, 28 Mar 2024 16:05:18 +0800 Subject: [PATCH] Rename "Organization" to "Team" across web app and update related services Renamed all instances of "Organization" with "Team" across the entire web application to reflect the latest change in terminology. This further extends to renaming related services, components, and their respective invocation instances. Separate billing permissions have been defined for Team accounts, and security actions have been updated in SQL schema along with some layout adjustments. --- .../home/[account]/billing/layout.tsx | 6 +-- .../home/[account]/billing/page.tsx | 4 +- .../app/(dashboard)/home/[account]/layout.tsx | 4 +- .../home/[account]/members/page.tsx | 4 +- .../app/(dashboard)/home/[account]/page.tsx | 4 +- .../organization-account-sidebar.config.tsx | 2 +- apps/web/public/locales/en/common.json | 1 + .../delete-personal-account.service.ts | 19 +++++--- .../delete-team-account-server-actions.ts | 10 +++- .../services/delete-team-account.service.ts | 11 ++++- packages/ui/src/shadcn/badge.tsx | 6 +-- supabase/migrations/20221215192558_schema.sql | 48 ++++++++++--------- 12 files changed, 71 insertions(+), 48 deletions(-) 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