diff --git a/package-lock.json b/package-lock.json
index 1cceff3e2..8461bfd51 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -78,6 +78,7 @@
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
+ "@types/lodash": "^4.17.23",
"@types/node": "^20.19.19",
"@types/papaparse": "^5.3.15",
"@types/react": "^19.0.0",
@@ -2451,6 +2452,21 @@
"jiti": "lib/jiti-cli.mjs"
}
},
+ "node_modules/@commitlint/load/node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
"node_modules/@commitlint/message": {
"version": "19.8.1",
"resolved": "https://registry.npmjs.org/@commitlint/message/-/message-19.8.1.tgz",
@@ -5536,11 +5552,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/lodash": {
+ "version": "4.17.23",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz",
+ "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/node": {
"version": "20.19.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.19.tgz",
"integrity": "sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@@ -5608,7 +5631,7 @@
"version": "19.2.0",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
@@ -14801,7 +14824,7 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/unicode-canonical-property-names-ecmascript": {
diff --git a/package.json b/package.json
index bdfbc12b7..c0add04d2 100644
--- a/package.json
+++ b/package.json
@@ -96,6 +96,7 @@
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
+ "@types/lodash": "^4.17.23",
"@types/node": "^20.19.19",
"@types/papaparse": "^5.3.15",
"@types/react": "^19.0.0",
diff --git a/src/components/core/divider/divider.tsx b/src/components/core/divider/divider.tsx
index 64e7fb4ff..14bb0be32 100644
--- a/src/components/core/divider/divider.tsx
+++ b/src/components/core/divider/divider.tsx
@@ -6,11 +6,11 @@ export const Divider = ({ text }: DividerProps) => {
return (
);
diff --git a/src/components/core/index.ts b/src/components/core/index.ts
index e9b075284..96b1251a7 100644
--- a/src/components/core/index.ts
+++ b/src/components/core/index.ts
@@ -32,3 +32,5 @@ export { Notification } from './notification/component/notification/notification
export { NotificationItem } from './notification/component/notification-item/notification-item';
export * from './notification/hooks/use-notification';
export { ExtensionBanner } from './extension-banner/extension-banner';
+export { ThemeSwitcher } from './theme-switcher/theme-switcher';
+export { OrgSwitcher } from './org-switcher/org-switcher';
diff --git a/src/components/core/notification/component/notification-item/notification-item.tsx b/src/components/core/notification/component/notification-item/notification-item.tsx
index f2393ca97..bc664d768 100644
--- a/src/components/core/notification/component/notification-item/notification-item.tsx
+++ b/src/components/core/notification/component/notification-item/notification-item.tsx
@@ -50,7 +50,7 @@ export const NotificationItem = ({ notification }: Readonly
-
+
diff --git a/src/components/core/org-switcher/org-switcher.tsx b/src/components/core/org-switcher/org-switcher.tsx
new file mode 100644
index 000000000..de880ea50
--- /dev/null
+++ b/src/components/core/org-switcher/org-switcher.tsx
@@ -0,0 +1,167 @@
+import { useState, useMemo } from 'react';
+import { Building2, ChevronDown, ChevronUp } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from '@/components/ui-kit/dropdown-menu';
+import { Skeleton } from '@/components/ui-kit/skeleton';
+import { useGetAccount } from '@/modules/profile/hooks/use-account';
+import { useGetMultiOrgs } from '@/lib/api/hooks/use-multi-orgs';
+import { switchOrganization } from '@/modules/auth/services/auth.service';
+import { useAuthStore } from '@/state/store/auth';
+import { useToast } from '@/hooks/use-toast';
+import { HttpError } from '@/lib/https';
+import { decodeJWT } from '@/lib/utils/decode-jwt-utils';
+
+const projectKey = import.meta.env.VITE_X_BLOCKS_KEY || '';
+
+export const OrgSwitcher = () => {
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
+ const [isSwitching, setIsSwitching] = useState(false);
+ const { t } = useTranslation();
+ const { setTokens, accessToken } = useAuthStore();
+ const { toast } = useToast();
+
+ const currentOrgId = useMemo(() => {
+ if (!accessToken) return null;
+ const decoded = decodeJWT(accessToken);
+ return decoded?.org_id ?? null;
+ }, [accessToken]);
+
+ const { data, isLoading } = useGetAccount();
+ const { data: orgsData, isLoading: isLoadingOrgs } = useGetMultiOrgs({
+ ProjectKey: projectKey,
+ Page: 0,
+ PageSize: 10,
+ });
+
+ const organizations = orgsData?.organizations ?? [];
+ const enabledOrganizations = organizations.filter((org) => org.isEnable);
+
+ const selectedOrg = currentOrgId
+ ? enabledOrganizations.find((org) => org.itemId === currentOrgId)
+ : enabledOrganizations[0];
+
+ const currentOrgRoles = useMemo(() => {
+ if (!data?.memberships?.length || !currentOrgId) return [];
+ const membership = data.memberships.find((m) => m.organizationId === currentOrgId);
+ return membership?.roles ?? [];
+ }, [data, currentOrgId]);
+
+ const translatedRoles = currentOrgRoles
+ .map((role: string) => {
+ const roleKey = role.toUpperCase();
+ return t(roleKey);
+ })
+ .join(', ');
+
+ const handleOrgSelect = async (orgId: string) => {
+ if (isSwitching || orgId === currentOrgId) {
+ return;
+ }
+
+ try {
+ setIsSwitching(true);
+ setIsDropdownOpen(false);
+
+ const response = await switchOrganization(orgId);
+
+ setTokens({
+ accessToken: response.access_token,
+ refreshToken: (response.refresh_token || useAuthStore.getState().refreshToken) ?? '',
+ });
+
+ localStorage.setItem('selected-org-id', orgId);
+
+ window.location.reload();
+ } catch (error) {
+ console.error('Failed to switch organization:', error);
+ setIsSwitching(false);
+
+ let errorTitle = t('FAILED_TO_SWITCH_ORGANIZATION');
+ let errorDescription = t('SOMETHING_WENT_WRONG');
+
+ if (error instanceof HttpError) {
+ const errorData = error.error;
+
+ if (errorData?.error === 'user_inactive_or_not_verified') {
+ errorTitle = t('ACCESS_DENIED');
+ errorDescription =
+ typeof errorData?.error_description === 'string'
+ ? errorData.error_description
+ : t('USER_NOT_EXIST_IN_ORGANIZATION');
+ } else if (typeof errorData?.error_description === 'string') {
+ errorDescription = errorData.error_description;
+ } else if (typeof errorData?.error === 'string') {
+ errorDescription = errorData.error;
+ }
+ }
+
+ toast({
+ variant: 'destructive',
+ title: errorTitle,
+ description: errorDescription,
+ });
+ }
+ };
+
+ const isComponentLoading = isLoading || isLoadingOrgs || isSwitching;
+
+ return (
+
+
+
+
+ {isComponentLoading ? (
+
+ ) : (
+
+ )}
+
+
+ {isComponentLoading ? (
+ <>
+
+
+ >
+ ) : (
+ <>
+
+ {selectedOrg?.name ?? '_'}
+
+
{translatedRoles}
+ >
+ )}
+
+ {isDropdownOpen ? (
+
+ ) : (
+
+ )}
+
+
+
+ {enabledOrganizations.length > 0 ? (
+ enabledOrganizations.map((org) => (
+ handleOrgSelect(org.itemId)}>
+ {org.name}
+
+ ))
+ ) : (
+ No orgs found
+ )}
+
+ {t('CREATE_NEW')}
+
+
+ );
+};
diff --git a/src/components/core/phone-input/phone-input.tsx b/src/components/core/phone-input/phone-input.tsx
index e18d4549e..e2a2f9987 100644
--- a/src/components/core/phone-input/phone-input.tsx
+++ b/src/components/core/phone-input/phone-input.tsx
@@ -81,7 +81,7 @@ const UIPhoneInput = forwardRef
(
placeholder={placeholder}
defaultCountry={defaultCountry}
className={cn(
- 'flex h-11 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
+ 'flex h-11 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
className
)}
countryCallingCodeEditable={countryCallingCodeEditable}
diff --git a/src/components/core/profile-menu/profile-menu.tsx b/src/components/core/profile-menu/profile-menu.tsx
index 3fc24f070..30126e47f 100644
--- a/src/components/core/profile-menu/profile-menu.tsx
+++ b/src/components/core/profile-menu/profile-menu.tsx
@@ -1,6 +1,5 @@
-import { useEffect, useState } from 'react';
+import { useEffect, useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
-import { ChevronDown, ChevronUp, Moon, Sun } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import {
DropdownMenu,
@@ -13,7 +12,6 @@ import { useSignoutMutation } from '@/modules/auth/hooks/use-auth';
import { useAuthStore } from '@/state/store/auth';
import DummyProfile from '@/assets/images/dummy_profile.png';
import { Skeleton } from '@/components/ui-kit/skeleton';
-import { useTheme } from '@/styles/theme/theme-provider';
import { useGetAccount } from '@/modules/profile/hooks/use-account';
/**
@@ -23,15 +21,12 @@ import { useGetAccount } from '@/modules/profile/hooks/use-account';
* navigation and account management options.
*
* Features:
- * - Displays user profile image and name
+ * - Displays user profile image
* - Shows loading states with skeleton placeholders
* - Provides navigation to profile page
- * - Includes theme toggling functionality
* - Handles user logout with authentication state management
- * - Responsive design with different spacing for mobile and desktop
*
* Dependencies:
- * - Requires useTheme hook for theme management
* - Requires useAuthStore for authentication state management
* - Requires useSignoutMutation for API logout functionality
* - Requires useGetAccount for fetching user account data
@@ -45,7 +40,8 @@ import { useGetAccount } from '@/modules/profile/hooks/use-account';
export const ProfileMenu = () => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
- const { theme, setTheme } = useTheme();
+ const [isImageLoaded, setIsImageLoaded] = useState(false);
+ const imgRef = useRef(null);
const { t } = useTranslation();
const { logout } = useAuthStore();
@@ -66,13 +62,8 @@ export const ProfileMenu = () => {
};
const fullName = `${data?.firstName ?? ''} ${data?.lastName ?? ''}`.trim() ?? ' ';
-
- const translatedRoles = data?.roles
- ?.map((role) => {
- const roleKey = role.toUpperCase();
- return t(roleKey);
- })
- .join(', ');
+ const profileImageUrl =
+ data?.profileImageUrl !== '' ? (data?.profileImageUrl ?? DummyProfile) : DummyProfile;
useEffect(() => {
if (data) {
@@ -86,39 +77,35 @@ export const ProfileMenu = () => {
}
}, [data, fullName]);
+ useEffect(() => {
+ setIsImageLoaded(false);
+ }, [profileImageUrl]);
+
+ useEffect(() => {
+ const img = imgRef.current;
+ if (img && img.complete && img.naturalHeight !== 0) {
+ setIsImageLoaded(true);
+ }
+ }, [profileImageUrl, isLoading]);
+
+ const showSkeleton = isLoading || !isImageLoaded;
+
return (
-
- {isLoading ? (
-
- ) : (
-

- )}
+
+ {showSkeleton &&
}
+

setIsImageLoaded(true)}
+ onError={() => setIsImageLoaded(true)}
+ />
-
- {isLoading ? (
-
- ) : (
-
{fullName}
- )}
-
{translatedRoles}
-
- {isDropdownOpen ? (
-
- ) : (
-
- )}
{
{t('ABOUT')}
{t('PRIVACY_POLICY')}
- setTheme(theme === 'dark' ? 'light' : 'dark')}
- >
- {t('THEME')}
-
-
-
{t('LOG_OUT')}
diff --git a/src/components/core/theme-switcher/theme-switcher.tsx b/src/components/core/theme-switcher/theme-switcher.tsx
new file mode 100644
index 000000000..f5fa0fe7f
--- /dev/null
+++ b/src/components/core/theme-switcher/theme-switcher.tsx
@@ -0,0 +1,18 @@
+import { Moon, Sun } from 'lucide-react';
+import { useTheme } from '@/styles/theme/theme-provider';
+import { Button } from '@/components/ui-kit/button';
+
+export const ThemeSwitcher = () => {
+ const { theme, setTheme } = useTheme();
+
+ return (
+
+ );
+};
diff --git a/src/components/ui-kit/badge.tsx b/src/components/ui-kit/badge.tsx
index fb5a01343..adf60e7c9 100644
--- a/src/components/ui-kit/badge.tsx
+++ b/src/components/ui-kit/badge.tsx
@@ -3,7 +3,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const badgeVariants = cva(
- 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
+ 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
diff --git a/src/components/ui-kit/breadcrumb.tsx b/src/components/ui-kit/breadcrumb.tsx
index 053545814..1bad714a8 100644
--- a/src/components/ui-kit/breadcrumb.tsx
+++ b/src/components/ui-kit/breadcrumb.tsx
@@ -40,13 +40,7 @@ const BreadcrumbLink = React.forwardRef<
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : 'a';
- return (
-
- );
+ return
;
});
BreadcrumbLink.displayName = 'BreadcrumbLink';
diff --git a/src/components/ui-kit/button.tsx b/src/components/ui-kit/button.tsx
index 84ee39abd..8480a194f 100644
--- a/src/components/ui-kit/button.tsx
+++ b/src/components/ui-kit/button.tsx
@@ -5,7 +5,7 @@ import { cn } from '../../lib/utils';
import { LoaderCircle } from 'lucide-react';
const buttonVariants = cva(
- 'button-text-select inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[6px] text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*="size-"])]:size-4 [&_svg]:shrink-0',
+ 'button-text-select inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[6px] text-sm font-medium focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*="size-"])]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
diff --git a/src/components/ui-kit/input.tsx b/src/components/ui-kit/input.tsx
index 9462cec30..3e4960ed7 100644
--- a/src/components/ui-kit/input.tsx
+++ b/src/components/ui-kit/input.tsx
@@ -10,7 +10,7 @@ const Input = React.forwardRef
(
))}
diff --git a/src/components/ui-kit/table.tsx b/src/components/ui-kit/table.tsx
index 519e4d307..babe23f57 100644
--- a/src/components/ui-kit/table.tsx
+++ b/src/components/ui-kit/table.tsx
@@ -47,10 +47,7 @@ const TableRow = React.forwardRef (
)
diff --git a/src/components/ui-kit/tabs.tsx b/src/components/ui-kit/tabs.tsx
index 65860725a..1d728ad11 100644
--- a/src/components/ui-kit/tabs.tsx
+++ b/src/components/ui-kit/tabs.tsx
@@ -24,7 +24,7 @@ function TabsTrigger({ className, ...props }: React.ComponentProps(
return (
@@ -231,7 +216,7 @@ const AudioPreview = ({ file, onClose }: Readonly) => {
{file.size}
-
@@ -267,19 +252,16 @@ const AudioPreview = ({ file, onClose }: Readonly
) => {
-
+
{isMuted ? : }
{isPlaying ? : }
-
+
@@ -358,7 +340,7 @@ const DocumentPreview = ({ file, onClose }: Readonly) => {
Next
-
+
diff --git a/src/modules/file-manager/components/modals/shared-user/shared-user.tsx b/src/modules/file-manager/components/modals/shared-user/shared-user.tsx
index 73872a839..90046f7cf 100644
--- a/src/modules/file-manager/components/modals/shared-user/shared-user.tsx
+++ b/src/modules/file-manager/components/modals/shared-user/shared-user.tsx
@@ -352,7 +352,7 @@ export const ShareWithMeModal = ({
/>
handleRemoveUser(user.id)}
- className="p-1.5 hover:bg-gray-100 rounded-full text-gray-400 hover:text-red-500 transition-colors"
+ className="p-1.5 hover:bg-gray-100 rounded-full text-gray-400 hover:text-red-500"
>
@@ -367,7 +367,7 @@ export const ShareWithMeModal = ({
Cancel
@@ -376,7 +376,7 @@ export const ShareWithMeModal = ({
{currentView === 'share' && selectedUsers.length > 0 && (
setCurrentView('manage')}
- className="px-4 py-2 text-sm text-blue-600 hover:bg-blue-50 rounded-md transition-colors"
+ className="px-4 py-2 text-sm text-blue-600 hover:bg-blue-50 rounded-md"
>
Manage ({selectedUsers.length})
@@ -384,14 +384,14 @@ export const ShareWithMeModal = ({
{currentView === 'manage' && (
setCurrentView('share')}
- className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
+ className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-md"
>
Back
)}
Send
diff --git a/src/modules/iam/types/user.types.ts b/src/modules/iam/types/user.types.ts
index b246438ba..42b9bc6fd 100644
--- a/src/modules/iam/types/user.types.ts
+++ b/src/modules/iam/types/user.types.ts
@@ -1,5 +1,5 @@
//TODO FE: only for the dev environment
-// import { Membership } from '@/types/user.type';
+import { Membership } from '@/types/user.type';
export interface IamData {
itemId: string;
@@ -24,6 +24,7 @@ export interface IamData {
userCreationType: number;
lastLoggedInDeviceInfo: string;
logInCount: number;
+ memberships?: Membership[];
}
export interface UserFilter {
diff --git a/src/modules/profile/components/modals/edit-iam-profile-details/edit-iam-profile-details.tsx b/src/modules/profile/components/modals/edit-iam-profile-details/edit-iam-profile-details.tsx
index b3f16d718..90cd55f28 100644
--- a/src/modules/profile/components/modals/edit-iam-profile-details/edit-iam-profile-details.tsx
+++ b/src/modules/profile/components/modals/edit-iam-profile-details/edit-iam-profile-details.tsx
@@ -259,18 +259,26 @@ export function EditIamProfileDetails({ userInfo, onClose }: Readonly
{
const { firstName, lastName } = parseFullName(data.fullName);
+ // Get organizationId from existing user memberships
+ const organizationId = userInfo?.memberships?.[0]?.organizationId ?? '';
+
const payload = {
itemId: data.itemId,
firstName,
lastName,
email: data.email,
phoneNumber: data.phoneNumber,
- roles: data.roles,
+ memberships: [
+ {
+ organizationId,
+ roles: data.roles,
+ },
+ ],
};
updateAccount(payload);
},
- [updateAccount]
+ [updateAccount, userInfo]
);
const handlePhoneChange = useCallback(
diff --git a/src/modules/profile/components/modals/edit-profile/edit-profile.tsx b/src/modules/profile/components/modals/edit-profile/edit-profile.tsx
index 3a43a08d2..8eb75a03c 100644
--- a/src/modules/profile/components/modals/edit-profile/edit-profile.tsx
+++ b/src/modules/profile/components/modals/edit-profile/edit-profile.tsx
@@ -196,6 +196,7 @@ export function EditProfile({ userInfo, onClose }: Readonly) {
email: data.email,
phoneNumber: data.phoneNumber,
profileImageUrl,
+ memberships: userInfo.memberships || [],
};
updateAccount(payload);
diff --git a/src/modules/profile/components/utils/index.utils.ts b/src/modules/profile/components/utils/index.utils.ts
index e30ca1c37..00334c285 100644
--- a/src/modules/profile/components/utils/index.utils.ts
+++ b/src/modules/profile/components/utils/index.utils.ts
@@ -11,6 +11,14 @@ export const getValidationSchemas = (t: (key: string) => string) => ({
firstName: z.string().min(1, { message: t('FIRST_NAME_CANT_EMPTY') }),
lastName: z.string().min(1, { message: t('LAST_NAME_CANT_EMPTY') }),
// email: z.string().email(),
+ memberships: z
+ .array(
+ z.object({
+ organizationId: z.string(),
+ roles: z.array(z.string()),
+ })
+ )
+ .optional(),
}),
changePasswordFormValidationSchema: z.object({
diff --git a/src/modules/profile/hooks/use-account.ts b/src/modules/profile/hooks/use-account.ts
index cefd9f55e..639d645a3 100644
--- a/src/modules/profile/hooks/use-account.ts
+++ b/src/modules/profile/hooks/use-account.ts
@@ -20,6 +20,13 @@ export const useGetAccount = () => {
return useQuery({
queryKey: ['getAccount'],
queryFn: getAccount,
+ retry: (failureCount, error: any) => {
+ if (error?.response?.status === 403) {
+ return false;
+ }
+ return failureCount < 3;
+ },
+ throwOnError: false,
});
};
diff --git a/src/modules/profile/services/accounts.service.ts b/src/modules/profile/services/accounts.service.ts
index 8d0ac3539..9570e3215 100644
--- a/src/modules/profile/services/accounts.service.ts
+++ b/src/modules/profile/services/accounts.service.ts
@@ -14,11 +14,7 @@ export const getAccount = async (): Promise => {
};
export const updateAccount = (data: ProfileFormType) => {
- return clients.post<{
- itemId: string;
- errors: unknown;
- isSuccess: boolean;
- }>('/idp/v1/Iam/UpdateAccount', JSON.stringify(data));
+ return clients.post('/idp/v1/Iam/UpdateAccount', JSON.stringify(data));
};
export const createAccount = (data: CreateUserFormType) => {
diff --git a/src/modules/task-manager/components/task-details-view/assignee-selector.tsx b/src/modules/task-manager/components/task-details-view/assignee-selector.tsx
index 5bdc7043a..0a83fb277 100644
--- a/src/modules/task-manager/components/task-details-view/assignee-selector.tsx
+++ b/src/modules/task-manager/components/task-details-view/assignee-selector.tsx
@@ -171,7 +171,7 @@ const AssigneeSelectorComponent = ({
>
(
@@ -438,7 +438,7 @@ export function AttachmentsSection({
diff --git a/src/state/query-client/hooks.tsx b/src/state/query-client/hooks.tsx
index 5a55effe4..012005bad 100644
--- a/src/state/query-client/hooks.tsx
+++ b/src/state/query-client/hooks.tsx
@@ -16,6 +16,9 @@ const processApiError = (err: any): ErrorResponse => {
// Extract errors object from backend response (e.g., {"isSuccess":false,"errors":{"Password":"..."}})
const backendErrors = err.error?.errors || err.response?.data?.errors;
+ // Extract status code from multiple possible locations
+ const status = err.status || err.response?.status || err.error?.status;
+
const errorInfo = {
error: err.error?.error || err.response?.data?.error || 'UNKNOWN_ERROR',
message: err.error?.message || err.response?.data?.message,
@@ -29,6 +32,7 @@ const processApiError = (err: any): ErrorResponse => {
err.error?.message ||
err.message ||
err.response?.data?.error_description,
+ status,
};
if (errorInfo.error === 'invalid_refresh_token') {
diff --git a/src/state/store/auth/guard.tsx b/src/state/store/auth/guard.tsx
index d91a32f5e..029d36bf6 100644
--- a/src/state/store/auth/guard.tsx
+++ b/src/state/store/auth/guard.tsx
@@ -1,15 +1,27 @@
import { useGetAccount } from '@/modules/profile/hooks/use-account';
import { useAuthStore } from '.';
-import { useEffect } from 'react';
+import { useEffect, useRef } from 'react';
+import { useErrorHandler } from '@/hooks/use-error-handler';
export const Guard = ({ children }: { children: React.ReactNode }) => {
- const { data, isSuccess } = useGetAccount();
+ const { data, isSuccess, error } = useGetAccount();
const { setUser, isAuthenticated } = useAuthStore();
+ const { handleError } = useErrorHandler();
+ const lastErrorRef = useRef
(null);
+
useEffect(() => {
if (!isAuthenticated) return;
+
+ if (error && error !== lastErrorRef.current) {
+ lastErrorRef.current = error;
+ handleError(error);
+ return;
+ }
+
if (!isSuccess) return;
setUser(data || null);
- }, [data, isAuthenticated, isSuccess, setUser]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [data, isAuthenticated, isSuccess, error, setUser]);
return <>{children}>;
};
diff --git a/src/state/store/auth/index.tsx b/src/state/store/auth/index.tsx
index d6ae01490..29aebf951 100644
--- a/src/state/store/auth/index.tsx
+++ b/src/state/store/auth/index.tsx
@@ -33,6 +33,8 @@ export const useAuthStore = create()(
setTokens: (tokens) =>
set((state) => ({
...state,
+ accessToken: tokens.accessToken,
+ refreshToken: tokens.refreshToken,
tokens,
})),
setUser: (user) =>
diff --git a/src/state/store/auth/use-is-protected.ts b/src/state/store/auth/use-is-protected.ts
index 536dcda4f..c8a19db4c 100644
--- a/src/state/store/auth/use-is-protected.ts
+++ b/src/state/store/auth/use-is-protected.ts
@@ -1,5 +1,6 @@
import { useMemo } from 'react';
import { useAuthStore } from '.';
+import { decodeJWT } from '@/lib/utils/decode-jwt-utils';
type UseIsProtectedOptions = {
roles?: string[];
@@ -7,6 +8,18 @@ type UseIsProtectedOptions = {
opt?: 'all' | 'any';
};
+const getCurrentOrgRoles = (user: any, accessToken: string | null): string[] => {
+ if (!user?.memberships?.length || !accessToken) return [];
+
+ const decoded = decodeJWT(accessToken);
+ const currentOrgId = decoded?.org_id;
+
+ if (!currentOrgId) return [];
+
+ const membership = user.memberships.find((m: any) => m.organizationId === currentOrgId);
+ return membership?.roles ?? [];
+};
+
const checkAllRoles = (userRoles: string[] | undefined, requiredRoles: string[]): boolean => {
if (requiredRoles.length === 0) return true;
return requiredRoles.every((role) => userRoles?.includes(role));
@@ -38,22 +51,24 @@ export const useIsProtected = ({
permissions = [],
opt = 'any',
}: UseIsProtectedOptions = {}) => {
- const { user, isAuthenticated } = useAuthStore();
+ const { user, isAuthenticated, accessToken } = useAuthStore();
const isProtected = useMemo(() => {
if (!isAuthenticated || !user) return false;
if (roles.length === 0 && permissions.length === 0) return false;
+ const userRoles = getCurrentOrgRoles(user, accessToken);
+
if (opt === 'all') {
- const hasAllRoles = checkAllRoles(user.roles, roles);
+ const hasAllRoles = checkAllRoles(userRoles, roles);
const hasAllPermissions = checkAllPermissions(user.permissions, permissions);
return hasAllRoles && hasAllPermissions;
}
- const hasAnyRole = checkAnyRole(user.roles, roles);
+ const hasAnyRole = checkAnyRole(userRoles, roles);
const hasAnyPermission = checkAnyPermission(user.permissions, permissions);
return hasAnyRole || hasAnyPermission;
- }, [isAuthenticated, user, roles, permissions, opt]);
+ }, [isAuthenticated, user, accessToken, roles, permissions, opt]);
return {
isProtected,
diff --git a/src/types/user.type.ts b/src/types/user.type.ts
index 51097376f..684a0030c 100644
--- a/src/types/user.type.ts
+++ b/src/types/user.type.ts
@@ -27,4 +27,5 @@ export type User = {
userCreationType: number;
lastLoggedInDeviceInfo: string;
logInCount: number;
+ memberships?: Membership[];
};