From 2284ed9f7b64a841c08545bc72ebaeb69d7a7eaa Mon Sep 17 00:00:00 2001 From: Sina Date: Wed, 15 May 2024 16:03:39 +0200 Subject: [PATCH] [feat] add permissions support in workspaces (#209) --- src/core/auth/AuthGuard.tsx | 9 ++- src/core/auth/UserContext.ts | 7 ++- src/core/auth/getPermissions.test.ts | 24 +++++++ src/core/auth/getPermissions.ts | 27 ++++++++ src/core/auth/getWorkspaces.query.ts | 5 +- src/core/auth/index.ts | 4 +- src/locales/de-DE/messages.po | 38 ++++++----- src/locales/en-US/messages.po | 38 ++++++----- src/pages/auth/login/LoginPage.tsx | 5 +- .../WorkspaceSettingsBillingPage.tsx | 7 ++- .../getWorkspaceBilling.query.ts | 2 +- .../panel-layout/UserProfileButton.tsx | 63 +++++++++++++++---- .../layouts/panel-layout/check-hooks/index.ts | 1 + .../useHasBillingPermissionCheck.tsx | 7 +++ src/shared/layouts/panel-layout/index.ts | 2 +- src/shared/layouts/panel-layout/menuList.tsx | 2 + .../types/server/responses/GetWorkspaces.ts | 2 + 17 files changed, 187 insertions(+), 56 deletions(-) create mode 100644 src/core/auth/getPermissions.test.ts create mode 100644 src/core/auth/getPermissions.ts create mode 100644 src/shared/layouts/panel-layout/check-hooks/useHasBillingPermissionCheck.tsx diff --git a/src/core/auth/AuthGuard.tsx b/src/core/auth/AuthGuard.tsx index 7b14c8fb..3a2d4359 100644 --- a/src/core/auth/AuthGuard.tsx +++ b/src/core/auth/AuthGuard.tsx @@ -11,6 +11,7 @@ import { getAuthData, setAuthData } from 'src/shared/utils/localstorage' import { TrackJS } from 'trackjs' import { UserContext, UserContextRealValues } from './UserContext' import { getCurrentUserQuery } from './getCurrentUser.query' +import { getPermissions, maxPermissionNumber } from './getPermissions' import { getWorkspacesQuery } from './getWorkspaces.query' import { logoutMutation } from './logout.mutation' @@ -34,6 +35,9 @@ export function AuthGuard({ children }: PropsWithChildren) { created_at: new Date().toISOString(), on_hold_since: null, trial_end_days: null, + user_has_access: true, + user_permissions: maxPermissionNumber, + permissions: getPermissions(maxPermissionNumber), } : undefined, isAuthenticated, @@ -75,8 +79,9 @@ export function AuthGuard({ children }: PropsWithChildren) { const workspaces = await getWorkspacesQuery(instance ?? axiosWithAuth) setAuth((prev) => { const selectedWorkspace = - (prev.selectedWorkspace?.id ? workspaces.find((workspace) => workspace.id === prev.selectedWorkspace?.id) : workspaces[0]) ?? - workspaces[0] + (prev.selectedWorkspace?.id + ? workspaces.find((workspace) => workspace.id === prev.selectedWorkspace?.id) + : workspaces.find((workspace) => workspace.user_has_access && workspace.permissions.includes('read'))) ?? workspaces[0] window.setTimeout(() => { window.location.hash = selectedWorkspace?.id ?? '' }) diff --git a/src/core/auth/UserContext.ts b/src/core/auth/UserContext.ts index 4cc43157..52758aee 100644 --- a/src/core/auth/UserContext.ts +++ b/src/core/auth/UserContext.ts @@ -1,10 +1,13 @@ import { createContext } from 'react' import { GetCurrentUserResponse, GetWorkspaceResponse, GetWorkspacesResponse } from 'src/shared/types/server' +import { Permissions } from './getPermissions' + +export type UserContextWorkspace = GetWorkspaceResponse & { permissions: Permissions[] } export type UserContextRealValues = { isAuthenticated: boolean - workspaces: GetWorkspacesResponse | never[] - selectedWorkspace?: GetWorkspaceResponse + workspaces: UserContextWorkspace[] | never[] + selectedWorkspace?: UserContextWorkspace currentUser?: GetCurrentUserResponse } diff --git a/src/core/auth/getPermissions.test.ts b/src/core/auth/getPermissions.test.ts new file mode 100644 index 00000000..79da2337 --- /dev/null +++ b/src/core/auth/getPermissions.test.ts @@ -0,0 +1,24 @@ +import { getPermissions, maxPermissionNumber } from './getPermissions' + +describe('getPermissions', () => { + it('should return an empty array for zero', () => { + expect(getPermissions(0)).toEqual([]) + }) + + it('should return correct permissions for a single flag', () => { + expect(getPermissions(1)).toEqual(['create']) // 1 << 0 + expect(getPermissions(2)).toEqual(['read']) // 1 << 1 + expect(getPermissions(8)).toEqual(['delete']) // 1 << 3 + }) + + it('should return multiple permissions for combined flags', () => { + const combined = 13 // 1 << 0 | 1 << 2 | 1 << 3 + expect(getPermissions(combined)).toEqual(['create', 'update', 'delete']) + }) + + it('should handle all flags combined', () => { + expect(getPermissions(maxPermissionNumber).length).toBe(13) + expect(getPermissions(maxPermissionNumber)).toContain('create') + expect(getPermissions(maxPermissionNumber)).toContain('updateRoles') + }) +}) diff --git a/src/core/auth/getPermissions.ts b/src/core/auth/getPermissions.ts new file mode 100644 index 00000000..1e89319b --- /dev/null +++ b/src/core/auth/getPermissions.ts @@ -0,0 +1,27 @@ +export const workspacePermissions = { + create: 1 << 0, // Not currently used. + read: 1 << 1, // Required to read any content in the workspace; without this, the workspace is effectively disabled. + update: 1 << 2, // Update general workspace properties; not directly related to deletion or invitations. + delete: 1 << 3, // Not currently used. + inviteTo: 1 << 4, // Invite new members to the workspace. + removeFrom: 1 << 5, // Remove existing members from the workspace. + readSettings: 1 << 6, // Access to view workspace settings; necessary to display settings UI. + updateSettings: 1 << 7, // Modify workspace settings. + updateCloudAccounts: 1 << 8, // Manage cloud account integrations within the workspace. + readBilling: 1 << 9, // View billing and subscription details. + updateBilling: 1 << 10, // Modify billing and payment methods. + readRoles: 1 << 11, // View roles of workspace members. + updateRoles: 1 << 12, // Change roles of workspace members. +} as const + +export const maxPermissionNumber = (1 << 13) - 1 + +export type Permissions = keyof typeof workspacePermissions + +export const getPermissions = (value: number) => + Object.entries(workspacePermissions).reduce( + (prev, [permKey, permValue]) => ((value & permValue) === permValue ? [...prev, permKey] : prev), + [] as Permissions[], + ) + +export const allPermissions = getPermissions(maxPermissionNumber) diff --git a/src/core/auth/getWorkspaces.query.ts b/src/core/auth/getWorkspaces.query.ts index 23a453be..37b239fd 100644 --- a/src/core/auth/getWorkspaces.query.ts +++ b/src/core/auth/getWorkspaces.query.ts @@ -1,7 +1,10 @@ import { AxiosInstance } from 'axios' import { endPoints } from 'src/shared/constants' import { GetWorkspacesResponse } from 'src/shared/types/server' +import { getPermissions } from './getPermissions' export const getWorkspacesQuery = async (axios: AxiosInstance) => { - return axios.get(endPoints.workspaces.self).then((res) => res.data) + return axios + .get(endPoints.workspaces.self) + .then((res) => res.data?.map((workspace) => ({ ...workspace, permissions: getPermissions(workspace.user_permissions) }))) } diff --git a/src/core/auth/index.ts b/src/core/auth/index.ts index 853aadb8..be8874ea 100644 --- a/src/core/auth/index.ts +++ b/src/core/auth/index.ts @@ -1,5 +1,7 @@ export { AuthGuard } from './AuthGuard' export { RequireAuth } from './RequireAuth' -export type { UserContextRealValues, UserContextValue } from './UserContext' +export type { UserContextRealValues, UserContextValue, UserContextWorkspace } from './UserContext' export { getCurrentUserQuery } from './getCurrentUser.query' +export { allPermissions, getPermissions, maxPermissionNumber, workspacePermissions } from './getPermissions' +export type { Permissions } from './getPermissions' export { useUserProfile } from './useUserProfile' diff --git a/src/locales/de-DE/messages.po b/src/locales/de-DE/messages.po index 136ec3a4..9fec499b 100644 --- a/src/locales/de-DE/messages.po +++ b/src/locales/de-DE/messages.po @@ -692,7 +692,7 @@ msgstr "Möchten Sie diese Einladung löschen?" msgid "Do you want to delete this user?" msgstr "Möchten Sie diesen Benutzer löschen?" -#: src/pages/auth/login/LoginPage.tsx:227 +#: src/pages/auth/login/LoginPage.tsx:230 msgid "Don't have an account? Click here to Sign up." msgstr "Sie haben noch kein Konto? Klicken Sie hier, um sich anzumelden." @@ -713,7 +713,7 @@ msgid "Edit" msgstr "Edit" #: src/pages/auth/forgot-password/ForgotPasswordPage.tsx:62 -#: src/pages/auth/login/LoginPage.tsx:111 +#: src/pages/auth/login/LoginPage.tsx:114 #: src/pages/auth/register/RegisterPage.tsx:88 #: src/pages/panel/user-settings/UserSettingsFormEmail.tsx:49 #: src/pages/panel/user-settings/UserSettingsFormEmail.tsx:69 @@ -845,7 +845,7 @@ msgstr "Für wachsende Teams, die sicher bleiben möchten, während sie die Infr msgid "For solo software engineers who want to secure a single cloud account." msgstr "Für Solo-Softwareentwickler, die sich ein einziges Cloud-Konto sichern möchten." -#: src/pages/auth/login/LoginPage.tsx:232 +#: src/pages/auth/login/LoginPage.tsx:235 msgid "Forget your password? Click here to reset your password." msgstr "Passwort vergessen? Klicken Sie hier, um Ihr Passwort zurückzusetzen." @@ -1021,8 +1021,8 @@ msgstr "Letzte Anmeldung" msgid "Load Balancers with no backends" msgstr "Load Balancer ohne Backends" -#: src/pages/auth/login/LoginPage.tsx:102 -#: src/pages/auth/login/LoginPage.tsx:222 +#: src/pages/auth/login/LoginPage.tsx:105 +#: src/pages/auth/login/LoginPage.tsx:225 msgid "Log in" msgstr "Anmeldung" @@ -1034,7 +1034,7 @@ msgstr "Melden Sie sich mit {formattedName} an" msgid "Login into the AWS account you want to secure. Deploy a CloudFormation stack that creates a new IAM role for FIX." msgstr "Melden Sie sich bei dem AWS-Konto an, das Sie sichern möchten. Stellen Sie einen CloudFormation-Stack bereit, der eine neue IAM-Rolle für FIX erstellt." -#: src/shared/layouts/panel-layout/UserProfileButton.tsx:125 +#: src/shared/layouts/panel-layout/UserProfileButton.tsx:164 msgid "Logout" msgstr "Ausloggen" @@ -1238,8 +1238,8 @@ msgstr "Geöffnet um" msgid "Optional professional services" msgstr "Optionale professionelle Dienstleistungen" -#: src/pages/auth/login/LoginPage.tsx:160 -#: src/pages/auth/login/LoginPage.tsx:237 +#: src/pages/auth/login/LoginPage.tsx:163 +#: src/pages/auth/login/LoginPage.tsx:240 #: src/pages/auth/register/RegisterPage.tsx:135 #: src/pages/panel/user-settings/UserSettingsTotpDeactivationModal.tsx:87 msgid "Or" @@ -1261,7 +1261,7 @@ msgstr "Verwaiste Volumes" msgid "Other Workspace Settings" msgstr "Andere Arbeitsbereichseinstellungen" -#: src/pages/auth/login/LoginPage.tsx:143 +#: src/pages/auth/login/LoginPage.tsx:146 #: src/pages/panel/user-settings/UserSettingsTotpActivationModal.tsx:204 #: src/pages/panel/user-settings/UserSettingsTotpDeactivationModal.tsx:73 msgid "OTP Code" @@ -1272,7 +1272,7 @@ msgstr "OTP-Code" msgid "Owner" msgstr "Eigentümer" -#: src/pages/auth/login/LoginPage.tsx:126 +#: src/pages/auth/login/LoginPage.tsx:129 #: src/pages/auth/register/RegisterPage.tsx:102 #: src/pages/auth/reset-password/ResetPasswordPage.tsx:96 msgid "Password" @@ -1301,7 +1301,7 @@ msgstr "Wählen Sie eine der Empfehlungen rechts aus und verbessern Sie Ihre Sic msgid "Please add a payment method to switch your workspace's product tier" msgstr "" -#: src/pages/auth/login/LoginPage.tsx:187 +#: src/pages/auth/login/LoginPage.tsx:190 msgid "Please enter your One-Time-Password or one of your Recovery code." msgstr "Bitte geben Sie Ihr One-Time-Passwort oder einen Ihrer Wiederherstellungscodes ein." @@ -1345,7 +1345,7 @@ msgstr "Produktsupport per E-Mail, Live-Chat und Videoanruf" msgid "Product tier changed to {selectedProductTier}" msgstr "" -#: src/shared/layouts/panel-layout/UserProfileButton.tsx:53 +#: src/shared/layouts/panel-layout/UserProfileButton.tsx:92 msgid "Profile" msgstr "Profil" @@ -1377,7 +1377,7 @@ msgstr "Quittungen" msgid "Recently added accounts" msgstr "Kürzlich hinzugefügte Konten" -#: src/pages/auth/login/LoginPage.tsx:169 +#: src/pages/auth/login/LoginPage.tsx:172 #: src/pages/panel/user-settings/UserSettingsTotpDeactivationModal.tsx:105 msgid "Recovery Code" msgstr "Wiederherstellungscode" @@ -1721,7 +1721,7 @@ msgid "Upgrade" msgstr "Aktualisierung" #: src/pages/panel/user-settings/UserSettingsPage.tsx:16 -#: src/shared/layouts/panel-layout/UserProfileButton.tsx:117 +#: src/shared/layouts/panel-layout/UserProfileButton.tsx:156 msgid "User Settings" msgstr "Benutzereinstellungen" @@ -1749,7 +1749,7 @@ msgstr "Warnung" msgid "We appreciate your decision to subscribe to our service through AWS Marketplace." msgstr "Wir freuen uns über Ihre Entscheidung, unseren Service über AWS Marketplace zu abonnieren." -#: src/pages/auth/login/LoginPage.tsx:194 +#: src/pages/auth/login/LoginPage.tsx:197 msgid "We have sent an email with a confirmation link to your email address. Please follow the link to activate your account." msgstr "Wir haben eine E-Mail mit einem Bestätigungslink an Ihre E-Mail-Adresse gesendet. Bitte folgen Sie dem Link, um Ihr Konto zu aktivieren." @@ -1827,11 +1827,15 @@ msgstr "Sie können TOTP über den Wiederherstellungscode deaktivieren" msgid "You don't have access to this workspace" msgstr "" -#: src/pages/auth/login/LoginPage.tsx:207 +#: src/shared/layouts/panel-layout/UserProfileButton.tsx:146 +msgid "You don't have the permission to view this workspace, contact the workspace owner for more information." +msgstr "" + +#: src/pages/auth/login/LoginPage.tsx:210 msgid "You have successfully reset your password." msgstr "Sie haben Ihr Passwort erfolgreich zurückgesetzt." -#: src/pages/auth/login/LoginPage.tsx:200 +#: src/pages/auth/login/LoginPage.tsx:203 msgid "You have successfully verified your account." msgstr "Sie haben Ihr Konto erfolgreich verifiziert." diff --git a/src/locales/en-US/messages.po b/src/locales/en-US/messages.po index 930bea00..e5eadcb9 100644 --- a/src/locales/en-US/messages.po +++ b/src/locales/en-US/messages.po @@ -692,7 +692,7 @@ msgstr "Do you want to delete this invitation?" msgid "Do you want to delete this user?" msgstr "Do you want to delete this user?" -#: src/pages/auth/login/LoginPage.tsx:227 +#: src/pages/auth/login/LoginPage.tsx:230 msgid "Don't have an account? Click here to Sign up." msgstr "Don't have an account? Click here to Sign up." @@ -713,7 +713,7 @@ msgid "Edit" msgstr "Edit" #: src/pages/auth/forgot-password/ForgotPasswordPage.tsx:62 -#: src/pages/auth/login/LoginPage.tsx:111 +#: src/pages/auth/login/LoginPage.tsx:114 #: src/pages/auth/register/RegisterPage.tsx:88 #: src/pages/panel/user-settings/UserSettingsFormEmail.tsx:49 #: src/pages/panel/user-settings/UserSettingsFormEmail.tsx:69 @@ -845,7 +845,7 @@ msgstr "For growing teams looking to stay secure as they build out infrastructur msgid "For solo software engineers who want to secure a single cloud account." msgstr "For solo software engineers who want to secure a single cloud account." -#: src/pages/auth/login/LoginPage.tsx:232 +#: src/pages/auth/login/LoginPage.tsx:235 msgid "Forget your password? Click here to reset your password." msgstr "Forget your password? Click here to reset your password." @@ -1021,8 +1021,8 @@ msgstr "Last login" msgid "Load Balancers with no backends" msgstr "Load Balancers with no backends" -#: src/pages/auth/login/LoginPage.tsx:102 -#: src/pages/auth/login/LoginPage.tsx:222 +#: src/pages/auth/login/LoginPage.tsx:105 +#: src/pages/auth/login/LoginPage.tsx:225 msgid "Log in" msgstr "Log in" @@ -1034,7 +1034,7 @@ msgstr "Log in with {formattedName}" msgid "Login into the AWS account you want to secure. Deploy a CloudFormation stack that creates a new IAM role for FIX." msgstr "Login into the AWS account you want to secure. Deploy a CloudFormation stack that creates a new IAM role for FIX." -#: src/shared/layouts/panel-layout/UserProfileButton.tsx:125 +#: src/shared/layouts/panel-layout/UserProfileButton.tsx:164 msgid "Logout" msgstr "Logout" @@ -1238,8 +1238,8 @@ msgstr "Opened at" msgid "Optional professional services" msgstr "Optional professional services" -#: src/pages/auth/login/LoginPage.tsx:160 -#: src/pages/auth/login/LoginPage.tsx:237 +#: src/pages/auth/login/LoginPage.tsx:163 +#: src/pages/auth/login/LoginPage.tsx:240 #: src/pages/auth/register/RegisterPage.tsx:135 #: src/pages/panel/user-settings/UserSettingsTotpDeactivationModal.tsx:87 msgid "Or" @@ -1261,7 +1261,7 @@ msgstr "Orphaned Volumes" msgid "Other Workspace Settings" msgstr "Other Workspace Settings" -#: src/pages/auth/login/LoginPage.tsx:143 +#: src/pages/auth/login/LoginPage.tsx:146 #: src/pages/panel/user-settings/UserSettingsTotpActivationModal.tsx:204 #: src/pages/panel/user-settings/UserSettingsTotpDeactivationModal.tsx:73 msgid "OTP Code" @@ -1272,7 +1272,7 @@ msgstr "OTP Code" msgid "Owner" msgstr "Owner" -#: src/pages/auth/login/LoginPage.tsx:126 +#: src/pages/auth/login/LoginPage.tsx:129 #: src/pages/auth/register/RegisterPage.tsx:102 #: src/pages/auth/reset-password/ResetPasswordPage.tsx:96 msgid "Password" @@ -1301,7 +1301,7 @@ msgstr "Pick one of the recommendations to the right and improve your security" msgid "Please add a payment method to switch your workspace's product tier" msgstr "Please add a payment method to switch your workspace's product tier" -#: src/pages/auth/login/LoginPage.tsx:187 +#: src/pages/auth/login/LoginPage.tsx:190 msgid "Please enter your One-Time-Password or one of your Recovery code." msgstr "Please enter your One-Time-Password or one of your Recovery code." @@ -1345,7 +1345,7 @@ msgstr "Product support via email, live chat, and video call" msgid "Product tier changed to {selectedProductTier}" msgstr "Product tier changed to {selectedProductTier}" -#: src/shared/layouts/panel-layout/UserProfileButton.tsx:53 +#: src/shared/layouts/panel-layout/UserProfileButton.tsx:92 msgid "Profile" msgstr "Profile" @@ -1377,7 +1377,7 @@ msgstr "Receipts" msgid "Recently added accounts" msgstr "Recently added accounts" -#: src/pages/auth/login/LoginPage.tsx:169 +#: src/pages/auth/login/LoginPage.tsx:172 #: src/pages/panel/user-settings/UserSettingsTotpDeactivationModal.tsx:105 msgid "Recovery Code" msgstr "Recovery Code" @@ -1721,7 +1721,7 @@ msgid "Upgrade" msgstr "Upgrade" #: src/pages/panel/user-settings/UserSettingsPage.tsx:16 -#: src/shared/layouts/panel-layout/UserProfileButton.tsx:117 +#: src/shared/layouts/panel-layout/UserProfileButton.tsx:156 msgid "User Settings" msgstr "User Settings" @@ -1749,7 +1749,7 @@ msgstr "Warning" msgid "We appreciate your decision to subscribe to our service through AWS Marketplace." msgstr "We appreciate your decision to subscribe to our service through AWS Marketplace." -#: src/pages/auth/login/LoginPage.tsx:194 +#: src/pages/auth/login/LoginPage.tsx:197 msgid "We have sent an email with a confirmation link to your email address. Please follow the link to activate your account." msgstr "We have sent an email with a confirmation link to your email address. Please follow the link to activate your account." @@ -1827,11 +1827,15 @@ msgstr "You can deactivate TOTP via recovery code" msgid "You don't have access to this workspace" msgstr "You don't have access to this workspace" -#: src/pages/auth/login/LoginPage.tsx:207 +#: src/shared/layouts/panel-layout/UserProfileButton.tsx:146 +msgid "You don't have the permission to view this workspace, contact the workspace owner for more information." +msgstr "You don't have the permission to view this workspace, contact the workspace owner for more information." + +#: src/pages/auth/login/LoginPage.tsx:210 msgid "You have successfully reset your password." msgstr "You have successfully reset your password." -#: src/pages/auth/login/LoginPage.tsx:200 +#: src/pages/auth/login/LoginPage.tsx:203 msgid "You have successfully verified your account." msgstr "You have successfully verified your account." diff --git a/src/pages/auth/login/LoginPage.tsx b/src/pages/auth/login/LoginPage.tsx index fac35c01..5698d8e5 100644 --- a/src/pages/auth/login/LoginPage.tsx +++ b/src/pages/auth/login/LoginPage.tsx @@ -6,7 +6,7 @@ import { useMutation } from '@tanstack/react-query' import { AxiosError } from 'axios' import { FormEvent, Suspense, useRef, useState } from 'react' import { Link, Location, useLocation, useSearchParams } from 'react-router-dom' -import { useUserProfile } from 'src/core/auth' +import { allPermissions, maxPermissionNumber, useUserProfile } from 'src/core/auth' import { ErrorBoundaryFallback, NetworkErrorBoundary } from 'src/shared/error-boundary-fallback' import { LoginSocialMedia } from 'src/shared/login-social-media' import { PasswordTextField } from 'src/shared/password-text-field' @@ -68,6 +68,9 @@ export default function LoginPage() { created_at: new Date().toISOString(), on_hold_since: null, trial_end_days: null, + permissions: allPermissions, + user_has_access: false, + user_permissions: maxPermissionNumber, }, }, returnUrl, diff --git a/src/pages/panel/workspace-settings-billing/WorkspaceSettingsBillingPage.tsx b/src/pages/panel/workspace-settings-billing/WorkspaceSettingsBillingPage.tsx index 2fc7af70..db3cb228 100644 --- a/src/pages/panel/workspace-settings-billing/WorkspaceSettingsBillingPage.tsx +++ b/src/pages/panel/workspace-settings-billing/WorkspaceSettingsBillingPage.tsx @@ -3,6 +3,7 @@ import { useLingui } from '@lingui/react' import { Alert, Divider, Stack, Typography } from '@mui/material' import { useSuspenseQuery } from '@tanstack/react-query' import { useUserProfile } from 'src/core/auth' +import { useHasBillingPermissionCheck } from 'src/shared/layouts/panel-layout' import { ChangePaymentMethod } from './ChangePaymentMethod' import { WorkspaceSettingsBillingTable } from './WorkspaceSettingsBillingTable' import { getWorkspaceBillingQuery } from './getWorkspaceBilling.query' @@ -13,9 +14,13 @@ export default function WorkspaceSettingsBillingPage() { i18n: { locale }, } = useLingui() const { selectedWorkspace } = useUserProfile() + const hasBillingPermission = useHasBillingPermissionCheck() const { data: { product_tier, workspace_payment_method, available_payment_methods }, - } = useSuspenseQuery({ queryFn: getWorkspaceBillingQuery, queryKey: ['workspace-billing', selectedWorkspace?.id] }) + } = useSuspenseQuery({ + queryFn: getWorkspaceBillingQuery, + queryKey: ['workspace-billing', hasBillingPermission ? selectedWorkspace?.id : undefined], + }) const currentDate = new Date() currentDate.setMilliseconds(0) currentDate.setSeconds(0) diff --git a/src/pages/panel/workspace-settings-billing/getWorkspaceBilling.query.ts b/src/pages/panel/workspace-settings-billing/getWorkspaceBilling.query.ts index 3fbe19ce..70dfb289 100644 --- a/src/pages/panel/workspace-settings-billing/getWorkspaceBilling.query.ts +++ b/src/pages/panel/workspace-settings-billing/getWorkspaceBilling.query.ts @@ -11,5 +11,5 @@ export const getWorkspaceBillingQuery = async ({ ? axiosWithAuth .get(endPoints.workspaces.workspace(workspaceId).billing, { signal }) .then((res) => res.data) - : ({} as GetWorkspaceBillingResponse) + : ({ available_payment_methods: [], product_tier: 'Free', workspace_payment_method: { method: 'none' } } as GetWorkspaceBillingResponse) } diff --git a/src/shared/layouts/panel-layout/UserProfileButton.tsx b/src/shared/layouts/panel-layout/UserProfileButton.tsx index f7723597..930c18a0 100644 --- a/src/shared/layouts/panel-layout/UserProfileButton.tsx +++ b/src/shared/layouts/panel-layout/UserProfileButton.tsx @@ -2,12 +2,51 @@ import { Trans, t } from '@lingui/macro' import CorporateFareIcon from '@mui/icons-material/CorporateFare' import LogoutIcon from '@mui/icons-material/Logout' import SettingsIcon from '@mui/icons-material/Settings' -import { Avatar, Divider, IconButton, ListItemIcon, Menu, MenuItem, MenuList, Tooltip, Typography } from '@mui/material' +import WarningIcon from '@mui/icons-material/Warning' +import { Avatar, Badge, Divider, IconButton, ListItemIcon, Menu, MenuItem, MenuList, Tooltip, Typography } from '@mui/material' import { MouseEvent as MouseEventReact, useState, useTransition } from 'react' import { useUserProfile } from 'src/core/auth' import { useAbsoluteNavigate } from 'src/shared/absolute-navigate' import { FullPageLoadingSuspenseFallback } from 'src/shared/loading' +export const WorkspaceMenuItem = ({ + id, + name, + disabled, + error, + handleSelectWorkspace, +}: { + id: string + name: string + disabled?: boolean + error?: string + handleSelectWorkspace: (id: string) => void +}) => { + const menuItem = ( + <> + + + + + {name} + + + ) + return error ? ( + + + } anchorOrigin={{ horizontal: 'left', vertical: 'top' }}> + {menuItem} + + + + ) : ( + handleSelectWorkspace(id)} disabled={disabled}> + {menuItem} + + ) +} + export const UserProfileButton = () => { const { logout, workspaces, selectedWorkspace, selectWorkspace } = useUserProfile() const navigate = useAbsoluteNavigate() @@ -94,19 +133,19 @@ export const UserProfileButton = () => { onClose={handleCloseUserMenu} > - {workspaces?.map(({ name, id }) => ( - ( + handleSelectWorkspace(id)} + id={id} + handleSelectWorkspace={handleSelectWorkspace} + name={name} disabled={selectedWorkspace?.id === id} - > - - - - - {name} - - + error={ + permissions.includes('read') && user_has_access + ? undefined + : t`You don't have the permission to view this workspace, contact the workspace owner for more information.` + } + /> ))} diff --git a/src/shared/layouts/panel-layout/check-hooks/index.ts b/src/shared/layouts/panel-layout/check-hooks/index.ts index 6c0a0f82..80012904 100644 --- a/src/shared/layouts/panel-layout/check-hooks/index.ts +++ b/src/shared/layouts/panel-layout/check-hooks/index.ts @@ -1 +1,2 @@ export { useHasBenchmarkCheck } from './useHasBenchmarkCheck' +export { useHasBillingPermissionCheck } from './useHasBillingPermissionCheck' diff --git a/src/shared/layouts/panel-layout/check-hooks/useHasBillingPermissionCheck.tsx b/src/shared/layouts/panel-layout/check-hooks/useHasBillingPermissionCheck.tsx new file mode 100644 index 00000000..13326a29 --- /dev/null +++ b/src/shared/layouts/panel-layout/check-hooks/useHasBillingPermissionCheck.tsx @@ -0,0 +1,7 @@ +import { useUserProfile } from 'src/core/auth' + +export const useHasBillingPermissionCheck = () => { + const { selectedWorkspace } = useUserProfile() + + return selectedWorkspace?.permissions.includes('readBilling') ?? false +} diff --git a/src/shared/layouts/panel-layout/index.ts b/src/shared/layouts/panel-layout/index.ts index b750a7d0..c57bf922 100644 --- a/src/shared/layouts/panel-layout/index.ts +++ b/src/shared/layouts/panel-layout/index.ts @@ -2,5 +2,5 @@ export { AccountCheckGuard } from './AccountCheckGuard' export { BenchmarkCheckGuard } from './BenchmarkCheckGuard' export { BottomRegion, ContentRegion, LogoRegion, PanelLayout } from './PanelLayout' export { SubscriptionCheckGuard } from './SubscriptionCheckGuard' -export { useHasBenchmarkCheck } from './check-hooks' +export { useHasBenchmarkCheck, useHasBillingPermissionCheck } from './check-hooks' export { AdvancedTableView, TablePagination, TableView } from './table-view-page' diff --git a/src/shared/layouts/panel-layout/menuList.tsx b/src/shared/layouts/panel-layout/menuList.tsx index c9090be2..38991baf 100644 --- a/src/shared/layouts/panel-layout/menuList.tsx +++ b/src/shared/layouts/panel-layout/menuList.tsx @@ -9,6 +9,7 @@ import ReceiptIcon from '@mui/icons-material/Receipt' import RoomPreferencesIcon from '@mui/icons-material/RoomPreferences' import SecurityIcon from '@mui/icons-material/Security' import { ComponentType, ReactNode } from 'react' +import { useHasBillingPermissionCheck } from './check-hooks' import { useHasBenchmarkCheck } from './check-hooks/useHasBenchmarkCheck' export interface MenuListItem { @@ -76,6 +77,7 @@ export const bottomMenuList: MenuListItem[] = [ Icon: ReceiptIcon, name: Billing, route: '/workspace-settings/billing-receipts', + useGuard: useHasBillingPermissionCheck, }, // { // Icon: FolderCopyIcon, diff --git a/src/shared/types/server/responses/GetWorkspaces.ts b/src/shared/types/server/responses/GetWorkspaces.ts index 26a0579a..a0cec1c2 100644 --- a/src/shared/types/server/responses/GetWorkspaces.ts +++ b/src/shared/types/server/responses/GetWorkspaces.ts @@ -7,6 +7,8 @@ export interface GetWorkspaceResponse { on_hold_since: string | null created_at: string trial_end_days: number | null + user_has_access: boolean | null + user_permissions: number } export type GetWorkspacesResponse = GetWorkspaceResponse[]