From 60a8dcfa1b3ff5874c70b12b13f78db92a4d555c Mon Sep 17 00:00:00 2001 From: Brion Date: Sat, 27 Jun 2026 00:10:34 +0530 Subject: [PATCH 1/5] Remove branding context and related hooks from React and Vue packages - Deleted useBrandingContext and useBranding hooks from React package. - Removed BrandingProvider and its related logic from both React and Vue packages. - Updated ThemeProvider to eliminate dependency on branding context. - Adjusted ThunderIDProvider to directly use ThemeProvider without BrandingProvider. - Cleaned up tests and keys related to branding context in Vue package. - Refactored ThemeContextValue to remove branding-related properties. --- .../__tests__/getBrandingPreference.test.ts | 268 ------------------ .../src/api/getBrandingPreference.ts | 201 ------------- packages/javascript/src/index.ts | 16 -- .../src/models/branding-preference.ts | 267 ----------------- packages/javascript/src/models/config.ts | 7 - .../contexts/ThunderID/ThunderIDProvider.tsx | 50 ++-- .../nextjs/src/server/ThunderIDProvider.tsx | 23 +- .../__tests__/getBrandingPreference.test.ts | 115 -------- .../server/actions/getBrandingPreference.ts | 49 ---- packages/nuxt/src/index.ts | 2 +- packages/nuxt/src/module.ts | 5 +- .../src/runtime/components/ThunderIDRoot.ts | 122 +++----- .../nuxt/src/runtime/plugins/thunderid.ts | 15 +- .../src/runtime/server/ThunderIDNuxtClient.ts | 7 - .../runtime/server/plugins/thunderid-ssr.ts | 13 +- .../routes/auth/branding/branding.get.ts | 78 ----- packages/nuxt/src/runtime/types.ts | 8 - .../nuxt/tests/unit/thunderid-root.test.ts | 46 +-- .../nuxt/tests/unit/thunderid-ssr.test.ts | 37 +-- .../src/contexts/Branding/BrandingContext.ts | 58 ---- .../contexts/Branding/BrandingProvider.tsx | 158 ----------- .../contexts/Branding/useBrandingContext.ts | 54 ---- .../react/src/contexts/Theme/ThemeContext.ts | 12 - .../src/contexts/Theme/ThemeProvider.tsx | 8 - .../contexts/ThunderID/ThunderIDProvider.tsx | 128 ++------- packages/react/src/hooks/useBranding.ts | 157 ---------- packages/react/src/index.ts | 16 +- .../composables/secondary-composables.test.ts | 47 +-- packages/vue/src/composables/useBranding.ts | 59 ---- packages/vue/src/index.ts | 4 - packages/vue/src/keys.ts | 6 - packages/vue/src/models/contexts.ts | 31 -- .../vue/src/providers/BrandingProvider.ts | 142 ---------- packages/vue/src/providers/ThemeProvider.ts | 76 +---- .../vue/src/providers/ThunderIDProvider.ts | 118 ++++---- 35 files changed, 172 insertions(+), 2231 deletions(-) delete mode 100644 packages/javascript/src/api/__tests__/getBrandingPreference.test.ts delete mode 100644 packages/javascript/src/api/getBrandingPreference.ts delete mode 100644 packages/javascript/src/models/branding-preference.ts delete mode 100644 packages/nextjs/src/server/actions/__tests__/getBrandingPreference.test.ts delete mode 100644 packages/nextjs/src/server/actions/getBrandingPreference.ts delete mode 100644 packages/nuxt/src/runtime/server/routes/auth/branding/branding.get.ts delete mode 100644 packages/react/src/contexts/Branding/BrandingContext.ts delete mode 100644 packages/react/src/contexts/Branding/BrandingProvider.tsx delete mode 100644 packages/react/src/contexts/Branding/useBrandingContext.ts delete mode 100644 packages/react/src/hooks/useBranding.ts delete mode 100644 packages/vue/src/composables/useBranding.ts delete mode 100644 packages/vue/src/providers/BrandingProvider.ts diff --git a/packages/javascript/src/api/__tests__/getBrandingPreference.test.ts b/packages/javascript/src/api/__tests__/getBrandingPreference.test.ts deleted file mode 100644 index fa6e884..0000000 --- a/packages/javascript/src/api/__tests__/getBrandingPreference.test.ts +++ /dev/null @@ -1,268 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {describe, it, expect, vi, beforeEach} from 'vitest'; -import ThunderIDAPIError from '../../errors/ThunderIDAPIError'; -import {BrandingPreference} from '../../models/branding-preference'; -import getBrandingPreference from '../getBrandingPreference'; - -describe('getBrandingPreference', (): void => { - beforeEach((): void => { - vi.resetAllMocks(); - }); - - it('should fetch branding preference successfully', async (): Promise => { - const mockBrandingPreference: BrandingPreference = { - locale: 'en-US', - name: 'dxlab', - preference: { - configs: { - isBrandingEnabled: true, - removeDefaultBranding: false, - }, - layout: { - activeLayout: 'centered', - }, - organizationDetails: { - displayName: '', - supportEmail: '', - }, - theme: { - DARK: { - buttons: { - primary: { - base: { - border: { - borderRadius: '22px', - }, - font: { - color: '#ffffff', - }, - }, - }, - }, - colors: { - primary: { - main: '#FF7300', - }, - secondary: { - main: '#E0E1E2', - }, - }, - }, - LIGHT: { - buttons: { - primary: { - base: { - border: { - borderRadius: '22px', - }, - font: { - color: '#ffffffe6', - }, - }, - }, - }, - colors: { - primary: { - main: '#FF7300', - }, - secondary: { - main: '#E0E1E2', - }, - }, - }, - activeTheme: 'DARK', - }, - urls: { - selfSignUpURL: 'https://localhost:5173/signup', - }, - }, - type: 'ORG', - }; - - global.fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mockBrandingPreference), - ok: true, - }); - - const baseUrl = 'https://localhost:8090'; - const result: BrandingPreference = await getBrandingPreference({baseUrl}); - - expect(fetch).toHaveBeenCalledWith(`${baseUrl}/api/server/v1/branding-preference/resolve`, { - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - method: 'GET', - }); - expect(result).toEqual(mockBrandingPreference); - }); - - it('should fetch branding preference with query parameters', async (): Promise => { - const mockBrandingPreference: BrandingPreference = { - locale: 'en-US', - name: 'custom', - type: 'ORG', - }; - - global.fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mockBrandingPreference), - ok: true, - }); - - const baseUrl = 'https://localhost:8090'; - await getBrandingPreference({ - baseUrl, - locale: 'en-US', - name: 'custom', - type: 'org', - }); - - expect(fetch).toHaveBeenCalledWith( - `${baseUrl}/api/server/v1/branding-preference/resolve?locale=en-US&name=custom&type=org`, - { - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - method: 'GET', - }, - ); - }); - - it('should handle custom fetcher', async (): Promise => { - const mockBrandingPreference: BrandingPreference = { - name: 'default', - type: 'ORG', - }; - - const customFetcher: typeof fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mockBrandingPreference), - ok: true, - }); - - const baseUrl = 'https://localhost:8090'; - await getBrandingPreference({baseUrl, fetcher: customFetcher}); - - expect(customFetcher).toHaveBeenCalledWith(`${baseUrl}/api/server/v1/branding-preference/resolve`, { - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - method: 'GET', - }); - }); - - it('should handle errors thrown directly by custom fetcher', async (): Promise => { - const customFetcher: typeof fetch = vi.fn().mockImplementation(() => { - throw new Error('Custom fetcher failure'); - }); - - const baseUrl = 'https://localhost:8090'; - - await expect(getBrandingPreference({baseUrl, fetcher: customFetcher})).rejects.toThrow(ThunderIDAPIError); - await expect(getBrandingPreference({baseUrl, fetcher: customFetcher})).rejects.toThrow( - 'Network or parsing error: Custom fetcher failure', - ); - }); - - it('should handle invalid base URL', async (): Promise => { - const invalidUrl = 'invalid-url'; - - await expect(getBrandingPreference({baseUrl: invalidUrl})).rejects.toThrow(ThunderIDAPIError); - await expect(getBrandingPreference({baseUrl: invalidUrl})).rejects.toThrow('Invalid base URL provided.'); - }); - - it('should throw ThunderIDAPIError for undefined baseUrl', async (): Promise => { - await expect(getBrandingPreference({} as any)).rejects.toThrow(ThunderIDAPIError); - await expect(getBrandingPreference({} as any)).rejects.toThrow('Invalid base URL provided.'); - }); - - it('should throw ThunderIDAPIError for empty string baseUrl', async (): Promise => { - await expect(getBrandingPreference({baseUrl: ''})).rejects.toThrow(ThunderIDAPIError); - await expect(getBrandingPreference({baseUrl: ''})).rejects.toThrow('Invalid base URL provided.'); - }); - - it('should handle HTTP error responses', async (): Promise => { - global.fetch = vi.fn().mockResolvedValue({ - ok: false, - status: 404, - statusText: 'Not Found', - text: () => Promise.resolve('Branding preference not found'), - }); - - const baseUrl = 'https://localhost:8090'; - - await expect(getBrandingPreference({baseUrl})).rejects.toThrow(ThunderIDAPIError); - await expect(getBrandingPreference({baseUrl})).rejects.toThrow( - 'Failed to get branding preference: Branding preference not found', - ); - }); - - it('should handle network errors', async (): Promise => { - global.fetch = vi.fn().mockRejectedValue(new Error('Network error')); - - const baseUrl = 'https://localhost:8090'; - - await expect(getBrandingPreference({baseUrl})).rejects.toThrow(ThunderIDAPIError); - await expect(getBrandingPreference({baseUrl})).rejects.toThrow('Network or parsing error: Network error'); - }); - - it('should handle non-Error rejections', async (): Promise => { - global.fetch = vi.fn().mockRejectedValue('unexpected failure'); - - const baseUrl = 'https://localhost:8090'; - - await expect(getBrandingPreference({baseUrl})).rejects.toThrow(ThunderIDAPIError); - await expect(getBrandingPreference({baseUrl})).rejects.toThrow('Network or parsing error: Unknown error'); - }); - - it('should pass through custom headers', async (): Promise => { - const mockBrandingPreference: BrandingPreference = { - name: 'default', - type: 'ORG', - }; - - global.fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mockBrandingPreference), - ok: true, - }); - - const baseUrl = 'https://localhost:8090'; - const customHeaders: Record = { - Authorization: 'Bearer token', - 'X-Custom-Header': 'custom-value', - }; - - await getBrandingPreference({ - baseUrl, - headers: customHeaders, - }); - - expect(fetch).toHaveBeenCalledWith(`${baseUrl}/api/server/v1/branding-preference/resolve`, { - headers: { - Accept: 'application/json', - Authorization: 'Bearer token', - 'Content-Type': 'application/json', - 'X-Custom-Header': 'custom-value', - }, - method: 'GET', - }); - }); -}); diff --git a/packages/javascript/src/api/getBrandingPreference.ts b/packages/javascript/src/api/getBrandingPreference.ts deleted file mode 100644 index 81d47ff..0000000 --- a/packages/javascript/src/api/getBrandingPreference.ts +++ /dev/null @@ -1,201 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import ThunderIDAPIError from '../errors/ThunderIDAPIError'; -import {BrandingPreference} from '../models/branding-preference'; -import logger from '../utils/logger'; - -/** - * Configuration for the getBrandingPreference request - */ -export interface GetBrandingPreferenceConfig extends Omit { - /** - * The base URL for the API endpoint. - */ - baseUrl: string; - /** - * Optional custom fetcher function. - * If not provided, native fetch will be used - */ - fetcher?: (url: string, config: RequestInit) => Promise; - /** - * Locale for the branding preference - */ - locale?: string; - /** - * Name of the branding preference - */ - name?: string; - /** - * Type of the branding preference - */ - type?: string; -} - -/** - * Retrieves branding preference configuration. - * - * @param config - Configuration object containing baseUrl, optional query parameters, and request config. - * @returns A promise that resolves with the branding preference information. - * @example - * ```typescript - * // Using default fetch - * try { - * const response = await getBrandingPreference({ - * baseUrl: "https://localhost:8090", - * locale: "en-US", - * name: "my-branding", - * type: "org" - * }); - * console.log(response.theme); - * } catch (error) { - * if (error instanceof ThunderIDAPIError) { - * console.error('Failed to get branding preference:', error.message); - * } - * } - * ``` - * - * @example - * ```typescript - * // Using custom fetcher (e.g., axios-based httpClient) - * try { - * const response = await getBrandingPreference({ - * baseUrl: "https://localhost:8090", - * locale: "en-US", - * name: "my-branding", - * type: "org", - * fetcher: async (url, config) => { - * const response = await httpClient({ - * url, - * method: config.method, - * headers: config.headers, - * ...config - * }); - * // Convert axios-like response to fetch-like Response - * return { - * ok: response.status >= 200 && response.status < 300, - * status: response.status, - * statusText: response.statusText, - * json: () => Promise.resolve(response.data), - * text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)) - * } as Response; - * } - * }); - * console.log(response.theme); - * } catch (error) { - * if (error instanceof ThunderIDAPIError) { - * console.error('Failed to get branding preference:', error.message); - * } - * } - * ``` - */ -const getBrandingPreference = async ({ - baseUrl, - locale, - name, - type, - fetcher, - ...requestConfig -}: GetBrandingPreferenceConfig): Promise => { - try { - // eslint-disable-next-line no-new - new URL(baseUrl); - } catch (error) { - throw new ThunderIDAPIError( - `Invalid base URL provided. ${error?.toString()}`, - 'getBrandingPreference-ValidationError-001', - 'javascript', - 400, - 'The provided `baseUrl` does not adhere to the URL schema.', - ); - } - - const queryParams: URLSearchParams = new URLSearchParams( - Object.fromEntries( - Object.entries({ - locale: locale || '', - name: name || '', - type: type || '', - }).filter(([, value]: [string, string]) => Boolean(value)), - ), - ); - - const fetchFn: typeof fetch = fetcher || fetch; - const resolvedUrl = `${baseUrl}/api/server/v1/branding-preference/resolve${ - queryParams.toString() ? `?${queryParams.toString()}` : '' - }`; - - const requestInit: RequestInit = { - ...requestConfig, - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - ...requestConfig.headers, - }, - method: 'GET', - }; - - try { - const response: Response = await fetchFn(resolvedUrl, requestInit); - - if (!response?.ok) { - const errorText: string = await response.text(); - - let errorDescription: string; - try { - const errorBody: {description?: string; message?: string} = JSON.parse(errorText) as { - description?: string; - message?: string; - }; - errorDescription = errorBody?.description || errorBody?.message || errorText; - } catch { - errorDescription = errorText; - } - - logger.warn( - `[BrandingError] ${errorDescription} To resolve this issue, please configure branding preferences in the ThunderID console. If you want to suppress this warning and stop fetching branding preferences, set \`\` -> \`preferences\` -> \`theme\` -> \`inheritFromBranding\` to false.`, - ); - - throw new ThunderIDAPIError( - errorText, - 'getBrandingPreference-ResponseError-001', - 'javascript', - response.status, - response.statusText, - 'Failed to get branding preference', - ); - } - - const data: BrandingPreference = (await response.json()) as BrandingPreference; - return data; - } catch (error) { - if (error instanceof ThunderIDAPIError) { - throw error; - } - - throw new ThunderIDAPIError( - `Network or parsing error: ${error instanceof Error ? error.message : 'Unknown error'}`, - 'getBrandingPreference-NetworkError-001', - 'javascript', - 0, - 'Network Error', - ); - } -}; - -export default getBrandingPreference; diff --git a/packages/javascript/src/index.ts b/packages/javascript/src/index.ts index 42f227e..ad8d796 100644 --- a/packages/javascript/src/index.ts +++ b/packages/javascript/src/index.ts @@ -42,8 +42,6 @@ export {default as updateOrganization, createPatchOperations} from './api/update export type {UpdateOrganizationConfig} from './api/updateOrganization'; export {default as updateMeProfile} from './api/updateMeProfile'; export type {UpdateMeProfileConfig} from './api/updateMeProfile'; -export {default as getBrandingPreference} from './api/getBrandingPreference'; -export type {GetBrandingPreferenceConfig} from './api/getBrandingPreference'; export {default as ApplicationNativeAuthenticationConstants} from './constants/ApplicationNativeAuthenticationConstants'; export {default as TokenConstants} from './constants/TokenConstants'; @@ -168,18 +166,6 @@ export type {SessionData} from './models/session'; export type {Organization} from './models/organization'; export type {TranslationFn} from './models/translation'; export type {ResolveFlowTemplateLiteralsOptions} from './models/vars'; -export type { - BrandingPreference, - BrandingPreferenceConfig, - BrandingLayout, - BrandingTheme, - ThemeVariant, - ButtonsConfig, - ColorsConfig, - ColorVariants, - BrandingOrganizationDetails, - UrlsConfig, -} from './models/branding-preference'; export {WellKnownSchemaIds} from './models/scim2-schema'; export type {Schema, SchemaAttribute, FlattenedSchema} from './models/scim2-schema'; export type {RecursivePartial} from './models/utility-types'; @@ -197,7 +183,6 @@ export {default as bem} from './utils/bem'; export {default as formatDate} from './utils/formatDate'; export {default as processUsername} from './utils/processUsername'; export {default as deepMerge} from './utils/deepMerge'; -export {default as deriveOrganizationHandleFromBaseUrl} from './utils/deriveOrganizationHandleFromBaseUrl'; export {default as extractUserClaimsFromIdToken} from './utils/extractUserClaimsFromIdToken'; export {default as isRecognizedBaseUrlPattern} from './utils/isRecognizedBaseUrlPattern'; export {default as extractPkceStorageKeyFromState} from './utils/extractPkceStorageKeyFromState'; @@ -222,7 +207,6 @@ export {default as buildValidatorFromRules} from './utils/buildValidatorFromRule export {default as evaluateValidationRule, DEFAULT_VALIDATION_MESSAGE_KEYS} from './utils/evaluateValidationRule'; export {default as processOpenIDScopes} from './utils/processOpenIDScopes'; export {default as withVendorCSSClassPrefix} from './utils/withVendorCSSClassPrefix'; -export {default as transformBrandingPreferenceToTheme} from './utils/transformBrandingPreferenceToTheme'; export { default as logger, diff --git a/packages/javascript/src/models/branding-preference.ts b/packages/javascript/src/models/branding-preference.ts deleted file mode 100644 index e14e631..0000000 --- a/packages/javascript/src/models/branding-preference.ts +++ /dev/null @@ -1,267 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * Interface for color configuration with multiple variants. - */ -export interface ColorVariants { - contrastText?: string; - dark?: string; - inverted?: string; - light?: string; - main?: string; -} - -/** - * Interface for text color configuration. - */ -export interface TextColors { - primary?: string; - secondary?: string; -} - -/** - * Interface for button styling configuration. - */ -export interface ButtonStyle { - base?: { - background?: { - backgroundColor?: string; - }; - border?: { - borderColor?: string; - borderRadius?: string; - }; - font?: { - color?: string; - }; - }; -} - -/** - * Interface for buttons configuration. - */ -export interface ButtonsConfig { - externalConnection?: ButtonStyle; - primary?: ButtonStyle; - secondary?: ButtonStyle; -} - -/** - * Interface for color palette configuration. - */ -export interface ColorsConfig { - alerts?: { - error?: ColorVariants; - info?: ColorVariants; - neutral?: ColorVariants; - warning?: ColorVariants; - }; - background?: { - body?: ColorVariants; - surface?: ColorVariants; - }; - illustrations?: { - accent1?: ColorVariants; - accent2?: ColorVariants; - accent3?: ColorVariants; - primary?: ColorVariants; - secondary?: ColorVariants; - }; - outlined?: { - default?: string; - }; - primary?: ColorVariants; - secondary?: ColorVariants; - text?: TextColors; -} - -/** - * Interface for footer configuration. - */ -export interface FooterConfig { - border?: { - borderColor?: string; - }; - font?: { - color?: string; - }; -} - -/** - * Interface for image configuration. - */ -export interface ImageConfig { - altText?: string; - imgURL?: string; - title?: string; -} - -/** - * Interface for images configuration. - */ -export interface ImagesConfig { - favicon?: Partial; - logo?: Partial; - myAccountLogo?: Partial; -} - -/** - * Interface for input styling configuration. - */ -export interface InputsConfig { - base?: { - background?: { - backgroundColor?: string; - }; - border?: { - borderColor?: string; - borderRadius?: string; - }; - font?: { - color?: string; - }; - labels?: { - font?: { - color?: string; - }; - }; - }; -} - -/** - * Interface for login box configuration. - */ -export interface LoginBoxConfig { - background?: { - backgroundColor?: string; - }; - border?: { - borderColor?: string; - borderRadius?: string; - borderWidth?: string; - }; - font?: { - color?: string; - }; -} - -/** - * Interface for login page configuration. - */ -export interface LoginPageConfig { - background?: { - backgroundColor?: string; - }; - font?: { - color?: string; - }; -} - -/** - * Interface for typography configuration. - */ -export interface TypographyConfig { - font?: { - color?: string; - fontFamily?: string; - importURL?: string; - }; - heading?: { - font?: { - color?: string; - }; - }; -} - -/** - * Interface for theme variant configuration (LIGHT/DARK). - */ -export interface ThemeVariant { - buttons?: ButtonsConfig; - colors?: ColorsConfig; - footer?: FooterConfig; - images?: ImagesConfig; - inputs?: InputsConfig; - loginBox?: LoginBoxConfig; - loginPage?: LoginPageConfig; - typography?: TypographyConfig; -} - -/** - * Interface for branding preference layout configuration. - */ -export interface BrandingLayout { - activeLayout?: string; - sideImg?: { - altText?: string; - imgURL?: string; - }; -} - -/** - * Interface for organization details configuration. - */ -export interface BrandingOrganizationDetails { - displayName?: string; - supportEmail?: string; -} - -/** - * Interface for URL configurations. - */ -export interface UrlsConfig { - cookiePolicyURL?: string; - privacyPolicyURL?: string; - selfSignUpURL?: string; - termsOfUseURL?: string; -} - -/** - * Interface for branding preference theme configuration. - */ -export interface BrandingTheme { - DARK?: ThemeVariant; - LIGHT?: ThemeVariant; - activeTheme?: string; -} - -/** - * Interface for branding preference configuration. - */ -export interface BrandingPreferenceConfig { - configs?: { - isBrandingEnabled?: boolean; - removeDefaultBranding?: boolean; - selfSignUpEnabled?: boolean; - }; - layout?: BrandingLayout; - organizationDetails?: BrandingOrganizationDetails; - theme?: BrandingTheme; - urls?: UrlsConfig; -} - -/** - * Interface for branding preference configuration. - */ -export interface BrandingPreference { - locale?: string; - name?: string; - preference?: BrandingPreferenceConfig; - type?: string; -} diff --git a/packages/javascript/src/models/config.ts b/packages/javascript/src/models/config.ts index 51a5522..3199396 100644 --- a/packages/javascript/src/models/config.ts +++ b/packages/javascript/src/models/config.ts @@ -449,13 +449,6 @@ export interface ThemePreferences { * @default 'ltr' */ direction?: 'ltr' | 'rtl'; - /** - * Inherit branding from WSO2 Identity Server or ThunderID. - * When set to `true`, the SDK will fetch and apply branding preferences from the server. - * Defaults to `false` — branding is not fetched unless explicitly enabled. - * @default false - */ - inheritFromBranding?: boolean; /** * The theme mode to use. Defaults to 'system'. */ diff --git a/packages/nextjs/src/client/contexts/ThunderID/ThunderIDProvider.tsx b/packages/nextjs/src/client/contexts/ThunderID/ThunderIDProvider.tsx index 500828d..0cd013c 100644 --- a/packages/nextjs/src/client/contexts/ThunderID/ThunderIDProvider.tsx +++ b/packages/nextjs/src/client/contexts/ThunderID/ThunderIDProvider.tsx @@ -26,7 +26,6 @@ import { UpdateMeProfileConfig, User, UserProfile, - BrandingPreference, TokenResponse, CreateOrganizationPayload, ThunderIDRuntimeError, @@ -39,7 +38,6 @@ import { ThemeProvider, ThunderIDProviderProps, OrganizationProvider, - BrandingProvider, getActiveTheme, } from '@thunderid/react'; import {ReadonlyURLSearchParams} from 'next/dist/client/components/navigation.react-server'; @@ -56,7 +54,6 @@ import logger from '../../../utils/logger'; export type ThunderIDClientProviderProps = Partial> & Pick & { applicationId: ThunderIDContextProps['applicationId']; - brandingPreference?: BrandingPreference | null; clearSession: () => Promise; createOrganization: (payload: CreateOrganizationPayload, sessionId: string) => Promise; currentOrganization: Organization; @@ -108,7 +105,6 @@ const ThunderIDClientProvider: FC) => { const reRenderCheckRef: RefObject = useRef(false); const router: AppRouterInstance = useRouter(); @@ -203,8 +199,11 @@ const ThunderIDClientProvider: FC - - - - - - {children} - - - - - + + + + + {children} + + + + diff --git a/packages/nextjs/src/server/ThunderIDProvider.tsx b/packages/nextjs/src/server/ThunderIDProvider.tsx index aba4f25..7579289 100644 --- a/packages/nextjs/src/server/ThunderIDProvider.tsx +++ b/packages/nextjs/src/server/ThunderIDProvider.tsx @@ -18,13 +18,12 @@ 'use server'; -import {BrandingPreference, ThunderIDRuntimeError, IdToken, Organization, User, UserProfile} from '@thunderid/node'; +import {ThunderIDRuntimeError, IdToken, Organization, User, UserProfile} from '@thunderid/node'; import {ThunderIDProviderProps} from '@thunderid/react'; import {FC, PropsWithChildren, ReactElement} from 'react'; import clearSession from './actions/clearSession'; import createOrganization from './actions/createOrganization'; import getAllOrganizations from './actions/getAllOrganizations'; -import getBrandingPreference from './actions/getBrandingPreference'; import getCurrentOrganizationAction from './actions/getCurrentOrganizationAction'; import getMyOrganizations from './actions/getMyOrganizations'; import getSessionId from './actions/getSessionId'; @@ -129,7 +128,6 @@ const ThunderIDServerProvider: FC {children} diff --git a/packages/nextjs/src/server/actions/__tests__/getBrandingPreference.test.ts b/packages/nextjs/src/server/actions/__tests__/getBrandingPreference.test.ts deleted file mode 100644 index 9a21e61..0000000 --- a/packages/nextjs/src/server/actions/__tests__/getBrandingPreference.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// src/server/actions/__tests__/getBrandingPreference.test.ts -import {ThunderIDAPIError, getBrandingPreference as baseGetBrandingPreference} from '@thunderid/node'; -import {describe, it, expect, vi, beforeEach, afterEach, type Mock} from 'vitest'; - -// Now import SUT and mocked exports -import getBrandingPreference from '../getBrandingPreference'; - -// Mock the upstream module first. Keep all dependencies inside the factory. -vi.mock('@thunderid/node', () => { - const getBrandingPreferenceMock: ReturnType = vi.fn(); - - class MockThunderIDAPIError extends Error { - code?: string; - - source?: string; - - statusCode?: number; - - constructor(message: string, code?: string, source?: string, statusCode?: number) { - super(message); - this.name = 'ThunderIDAPIError'; - this.code = code; - this.source = source; - this.statusCode = statusCode; - } - } - - return { - ThunderIDAPIError: MockThunderIDAPIError, - getBrandingPreference: getBrandingPreferenceMock, - }; -}); - -describe('getBrandingPreference (Next.js server action)', () => { - type BrandingPreference = Awaited>; - type Cfg = Parameters[0]; - - const cfg: Cfg = {locale: 'en-US', orgId: 'org-001'} as unknown as Cfg; - - const mockPref: BrandingPreference = { - logoUrl: 'https://cdn.example.com/logo.png', - theme: {colors: {primary: '#0055aa'}}, - } as unknown as BrandingPreference; - - beforeEach(() => { - vi.resetAllMocks(); - (baseGetBrandingPreference as unknown as Mock).mockResolvedValue(mockPref); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - it('should return branding preferences when upstream succeeds', async () => { - const result: BrandingPreference = await getBrandingPreference(cfg, 'sess-123'); - - expect(baseGetBrandingPreference).toHaveBeenCalledTimes(1); - expect(baseGetBrandingPreference).toHaveBeenCalledWith(cfg); - - // Ensure sessionId is not forwarded - const call: unknown[] = (baseGetBrandingPreference as unknown as Mock).mock.calls[0]; - expect(call.length).toBe(1); - - expect(result).toBe(mockPref); - }); - - it('should wrap an ThunderIDAPIError from upstream, preserving statusCode', async () => { - const upstream: ThunderIDAPIError = new ThunderIDAPIError('Not found', 'BRAND_404', 'server', 404); - (baseGetBrandingPreference as unknown as Mock).mockRejectedValueOnce(upstream); - - await expect(getBrandingPreference(cfg)).rejects.toMatchObject({ - constructor: ThunderIDAPIError, - message: expect.stringContaining('Failed to get branding preferences: Not found'), - statusCode: 404, - }); - }); - - it('should wrap a generic Error with undefined statusCode', async () => { - (baseGetBrandingPreference as unknown as Mock).mockRejectedValueOnce(new Error('network down')); - - await expect(getBrandingPreference(cfg)).rejects.toMatchObject({ - constructor: ThunderIDAPIError, - message: expect.stringContaining('Failed to get branding preferences: network down'), - statusCode: undefined, - }); - }); - - it('should wrap a non-Error rejection value using String(error)', async () => { - (baseGetBrandingPreference as unknown as Mock).mockRejectedValueOnce('boom'); - - await expect(getBrandingPreference(cfg)).rejects.toMatchObject({ - constructor: ThunderIDAPIError, - message: expect.stringContaining('Failed to get branding preferences: boom'), - statusCode: undefined, - }); - }); -}); diff --git a/packages/nextjs/src/server/actions/getBrandingPreference.ts b/packages/nextjs/src/server/actions/getBrandingPreference.ts deleted file mode 100644 index 433cba9..0000000 --- a/packages/nextjs/src/server/actions/getBrandingPreference.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -'use server'; - -import { - ThunderIDAPIError, - GetBrandingPreferenceConfig, - BrandingPreference, - getBrandingPreference as baseGetBrandingPreference, -} from '@thunderid/node'; - -/** - * Server action to get branding preferences. - */ -const getBrandingPreference = async ( - config: GetBrandingPreferenceConfig, - sessionId?: string, // eslint-disable-line @typescript-eslint/no-unused-vars -): Promise => { - try { - const brandingPreference: BrandingPreference = await baseGetBrandingPreference(config); - - return brandingPreference; - } catch (error) { - throw new ThunderIDAPIError( - `Failed to get branding preferences: ${error instanceof Error ? error.message : String(error)}`, - 'getBrandingPreferenceAction-ServerActionError-001', - 'nextjs', - error instanceof ThunderIDAPIError ? error.statusCode : undefined, - ); - } -}; - -export default getBrandingPreference; diff --git a/packages/nuxt/src/index.ts b/packages/nuxt/src/index.ts index 83ce2ef..88eb41d 100644 --- a/packages/nuxt/src/index.ts +++ b/packages/nuxt/src/index.ts @@ -26,7 +26,7 @@ export type {ThunderIDNuxtConfig, ThunderIDSessionPayload, ThunderIDAuthState} f // `useThunderID`. The rest are re-exports of `@thunderid/vue` composables — // their contexts are mounted by `` (see runtime/components). export {useThunderID} from './runtime/composables/useThunderID'; -export {useUser, useOrganization, useFlow, useFlowMeta, useTheme, useBranding} from '@thunderid/vue'; +export {useUser, useOrganization, useFlow, useFlowMeta, useTheme} from '@thunderid/vue'; export {useI18n as useThunderIDI18n} from '@thunderid/vue'; // ── Components ───────────────────────────────────────────────────────────── diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index 2444f34..e115e0d 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -231,7 +231,6 @@ export default defineNuxtModule({ {from: '@thunderid/vue', name: 'useFlow'}, {from: '@thunderid/vue', name: 'useFlowMeta'}, {from: '@thunderid/vue', name: 'useTheme'}, - {from: '@thunderid/vue', name: 'useBranding'}, // useI18n aliased to `useThunderIDI18n` to avoid collision with @nuxtjs/i18n {as: 'useThunderIDI18n', from: '@thunderid/vue', name: 'useI18n'}, // Middleware factory @@ -239,7 +238,7 @@ export default defineNuxtModule({ ]); // Register the Nuxt-specific root component that mounts the full Vue - // provider tree (I18nProvider, BrandingProvider, ThemeProvider, etc.). + // provider tree (I18nProvider, ThemeProvider, etc.). // Users wrap their `app.vue` with `` — matching the way // Next.js users wrap their app with ``. addComponent({ @@ -257,7 +256,7 @@ export default defineNuxtModule({ // This mirrors the Next.js SDK pattern where Base components come from // @thunderid/react and host-specific containers live in the Next.js package. // - // NOTE: Composables (useUser, useOrganization, useTheme, useBranding, + // NOTE: Composables (useUser, useOrganization, useTheme, // useFlow, useI18n) remain direct re-exports from @thunderid/vue via // addImports above — only the components need Nuxt wrappers. diff --git a/packages/nuxt/src/runtime/components/ThunderIDRoot.ts b/packages/nuxt/src/runtime/components/ThunderIDRoot.ts index f871cfd..19acaf0 100644 --- a/packages/nuxt/src/runtime/components/ThunderIDRoot.ts +++ b/packages/nuxt/src/runtime/components/ThunderIDRoot.ts @@ -19,7 +19,6 @@ import {generateFlattenedUserProfile} from '@thunderid/browser'; import type { AllOrganizationsApiResponse, - BrandingPreference, CreateOrganizationPayload, Organization, UpdateMeProfileConfig, @@ -27,7 +26,6 @@ import type { UserProfile, } from '@thunderid/node'; import { - BrandingProvider, FlowMetaProvider, FlowProvider, I18nProvider, @@ -47,8 +45,7 @@ import {useState, useRuntimeConfig} from '#imports'; * data as props to each Vue provider: * * - {@link I18nProvider} ← `preferences.i18n` - * - {@link BrandingProvider} ← `brandingPreference` (from `thunderid:branding`) - * - {@link ThemeProvider} ← `inheritFromBranding`, `mode` + * - {@link ThemeProvider} ← `mode` * - {@link FlowProvider} * - {@link UserProvider} ← `profile`, `flattenedProfile`, `schemas`, * `updateProfile`, `revalidateProfile`, `onUpdateProfile` @@ -59,7 +56,7 @@ import {useState, useRuntimeConfig} from '#imports'; * The `THUNDERID_KEY` (config + auth state + actions) is still provided at the * app level by the Nuxt plugin; this component only supplies the auxiliary * provider contexts so downstream composables (`useUser`, `useOrganization`, - * `useTheme`, `useBranding`, `useThunderIDI18n`) receive real data. + * `useTheme`, `useThunderIDI18n`) receive real data. * * @example * ```vue @@ -78,7 +75,6 @@ const ThunderIDRoot: Component = defineComponent({ const userProfileState: Ref = useState('thunderid:user-profile'); const currentOrgState: Ref = useState('thunderid:current-org'); const myOrgsState: Ref = useState('thunderid:my-orgs'); - const brandingState: Ref = useState('thunderid:branding'); // Used by onUpdateProfile to keep the top-level auth user claim in sync. const authState: Ref = useState('thunderid:auth'); @@ -93,7 +89,6 @@ const ThunderIDRoot: Component = defineComponent({ // always agree with what the Nitro plugin decided to fetch server-side. const shouldFetchProfile: boolean = prefs?.user?.fetchUserProfile !== false; const shouldFetchOrgs: boolean = prefs?.user?.fetchOrganizations !== false; - const shouldFetchBranding: boolean = prefs?.theme?.inheritFromBranding !== false; // Defaults to 'light' — matches the Vue SDK's ThunderIDProvider, which // passes no mode and therefore uses ThemeProvider's `DEFAULT_THEME`. const themeMode: string = prefs?.theme?.mode ?? 'light'; @@ -211,19 +206,6 @@ const ThunderIDRoot: Component = defineComponent({ } }; - /** - * Refresh the branding preference and update local state so - * `useBranding().brandingPreference` stays reactive. - */ - const revalidateBranding = async (): Promise => { - try { - const res: BrandingPreference | null = await $fetch('/api/auth/branding'); - if (res) brandingState.value = res; - } catch { - // Non-fatal — branding stays stale until the next navigation. - } - }; - // ── Render tree — mirrors ThunderIDClientProvider (Next.js) ───────────── // // FlowMetaProvider is mounted unconditionally with `enabled: false` (V1 @@ -245,73 +227,59 @@ const ThunderIDRoot: Component = defineComponent({ { default: (): VNode => h( - BrandingProvider, + ThemeProvider, { - // When inheritFromBranding is disabled, pass null so the provider - // falls back to its own default theme without using SSR-fetched data. - brandingPreference: shouldFetchBranding ? brandingState.value : null, - revalidateBranding: shouldFetchBranding ? revalidateBranding : undefined, + mode: themeMode as any, }, { default: (): VNode => - h( - ThemeProvider, - { - // Mirror the same flag used in the Nitro plugin gate. - inheritFromBranding: shouldFetchBranding, - mode: themeMode as any, - }, - { - default: (): VNode => - h(FlowProvider, null, { - default: (): VNode => + h(FlowProvider, null, { + default: (): VNode => + h( + UserProvider, + { + // When fetchUserProfile is false the Nitro plugin + // skips SCIM calls, so we must also pass empty values + // here to keep SSR and client in sync. + flattenedProfile: shouldFetchProfile + ? (userProfileState.value?.flattenedProfile ?? null) + : null, + onUpdateProfile: shouldFetchProfile ? onUpdateProfile : undefined, + profile: shouldFetchProfile ? userProfileState.value : null, + revalidateProfile: shouldFetchProfile ? revalidateProfile : undefined, + schemas: shouldFetchProfile ? (userProfileState.value?.schemas ?? null) : null, + updateProfile: shouldFetchProfile ? updateProfile : undefined, + }, + { + default: (): VNode | VNode[] | undefined => h( - UserProvider, + OrganizationProvider, { - // When fetchUserProfile is false the Nitro plugin - // skips SCIM calls, so we must also pass empty values - // here to keep SSR and client in sync. - flattenedProfile: shouldFetchProfile - ? (userProfileState.value?.flattenedProfile ?? null) - : null, - onUpdateProfile: shouldFetchProfile ? onUpdateProfile : undefined, - profile: shouldFetchProfile ? userProfileState.value : null, - revalidateProfile: shouldFetchProfile ? revalidateProfile : undefined, - schemas: shouldFetchProfile ? (userProfileState.value?.schemas ?? null) : null, - updateProfile: shouldFetchProfile ? updateProfile : undefined, + // When fetchOrganizations is false pass empty + // values so the provider renders without org data. + createOrganization: shouldFetchOrgs + ? (createOrganization as any) + : undefined, + currentOrganization: shouldFetchOrgs ? currentOrgState.value : null, + getAllOrganizations: shouldFetchOrgs ? getAllOrganizations : undefined, + myOrganizations: shouldFetchOrgs ? myOrgsState.value : [], + onOrganizationSwitch: shouldFetchOrgs + ? (onOrganizationSwitch as any) + : undefined, + revalidateCurrentOrganization: shouldFetchOrgs + ? revalidateCurrentOrganization + : undefined, + revalidateMyOrganizations: shouldFetchOrgs + ? revalidateMyOrganizations + : undefined, }, { - default: (): VNode | VNode[] | undefined => - h( - OrganizationProvider, - { - // When fetchOrganizations is false pass empty - // values so the provider renders without org data. - createOrganization: shouldFetchOrgs - ? (createOrganization as any) - : undefined, - currentOrganization: shouldFetchOrgs ? currentOrgState.value : null, - getAllOrganizations: shouldFetchOrgs ? getAllOrganizations : undefined, - myOrganizations: shouldFetchOrgs ? myOrgsState.value : [], - onOrganizationSwitch: shouldFetchOrgs - ? (onOrganizationSwitch as any) - : undefined, - revalidateCurrentOrganization: shouldFetchOrgs - ? revalidateCurrentOrganization - : undefined, - revalidateMyOrganizations: shouldFetchOrgs - ? revalidateMyOrganizations - : undefined, - }, - { - default: (): VNode | VNode[] | undefined => slots.default?.(), - }, - ), + default: (): VNode | VNode[] | undefined => slots.default?.(), }, ), - }), - }, - ), + }, + ), + }), }, ), }, diff --git a/packages/nuxt/src/runtime/plugins/thunderid.ts b/packages/nuxt/src/runtime/plugins/thunderid.ts index c35d1c5..fc8a043 100644 --- a/packages/nuxt/src/runtime/plugins/thunderid.ts +++ b/packages/nuxt/src/runtime/plugins/thunderid.ts @@ -16,16 +16,16 @@ * under the License. */ -import {defineNuxtPlugin, useState, useRequestEvent, useRuntimeConfig, navigateTo} from '#app'; -import type {NuxtApp} from '#app'; import {getRedirectBasedSignUpUrl} from '@thunderid/browser'; -import type {BrandingPreference, Organization, UserProfile} from '@thunderid/node'; +import type {Organization, UserProfile} from '@thunderid/node'; import {ThunderIDPlugin, THUNDERID_KEY} from '@thunderid/vue'; import type {H3Event} from 'h3'; import {computed} from 'vue'; import type {ComputedRef, Ref} from 'vue'; import ThunderIDRoot from '../components/ThunderIDRoot'; import type {ThunderIDAuthState, ThunderIDSSRData} from '../types'; +import type {NuxtApp} from '#app'; +import {defineNuxtPlugin, useState, useRequestEvent, useRuntimeConfig, navigateTo} from '#app'; /** * Universal Nuxt plugin (runs on both server and client) that wires up the @@ -41,8 +41,8 @@ import type {ThunderIDAuthState, ThunderIDSSRData} from '../types'; * Action helpers (`signIn` / `signOut` / `signUp`) use Nuxt's * `navigateTo` so redirects work on both server and client. * 3. **ThunderIDRoot** — register the wrapper component that mounts the rest - * of the provider tree (`I18nProvider`, `BrandingProvider`, - * `ThemeProvider`, `FlowProvider`, `UserProvider`, `OrganizationProvider`) + * of the provider tree (`I18nProvider`, `ThemeProvider`, `FlowProvider`, + * `UserProvider`, `OrganizationProvider`) * so downstream composables receive real context values. * 4. **ThunderIDPlugin (delegated)** — install the Vue SDK plugin in * delegated mode so it skips browser-only initialisation (SSR-safe). @@ -97,10 +97,6 @@ export default defineNuxtPlugin((nuxtApp: NuxtApp) => { const userProfileState: Ref = useState('thunderid:user-profile', () => null); const currentOrgState: Ref = useState('thunderid:current-org', () => null); const myOrgsState: Ref = useState('thunderid:my-orgs', () => []); - const brandingState: Ref = useState( - 'thunderid:branding', - () => null, - ); if (import.meta.server) { const event: H3Event | undefined = useRequestEvent(); @@ -116,7 +112,6 @@ export default defineNuxtPlugin((nuxtApp: NuxtApp) => { userProfileState.value = ssr.userProfile; currentOrgState.value = ssr.currentOrganization; myOrgsState.value = ssr.myOrganizations; - brandingState.value = ssr.brandingPreference; } else { // Backwards-compat: fall back to the legacy context shape (pre-Step-2 plugin). const ssrContext: {isSignedIn?: boolean; session?: {sub?: string}} | undefined = event?.context?.thunderid as diff --git a/packages/nuxt/src/runtime/server/ThunderIDNuxtClient.ts b/packages/nuxt/src/runtime/server/ThunderIDNuxtClient.ts index d647975..c7bd8f2 100644 --- a/packages/nuxt/src/runtime/server/ThunderIDNuxtClient.ts +++ b/packages/nuxt/src/runtime/server/ThunderIDNuxtClient.ts @@ -30,15 +30,12 @@ import { type UserProfile, type UpdateMeProfileConfig, type AllOrganizationsApiResponse, - getBrandingPreference, getMeOrganizations, getAllOrganizations, createOrganization, getOrganization, type ExtendedAuthorizeRequestUrlParams, type SignUpOptions, - type GetBrandingPreferenceConfig, - type BrandingPreference, } from '@thunderid/node'; import type {ThunderIDNuxtConfig, ThunderIDSessionPayload} from '../types'; @@ -202,10 +199,6 @@ class ThunderIDNuxtClient extends ThunderIDNodeClient { }); } - async getBrandingPreference(config: GetBrandingPreferenceConfig): Promise { - return getBrandingPreference(config); - } - override async updateUserProfile(config: UpdateMeProfileConfig, sessionId: string): Promise { throw new Error('Profile updates are not supported for the ThunderID platform.'); } diff --git a/packages/nuxt/src/runtime/server/plugins/thunderid-ssr.ts b/packages/nuxt/src/runtime/server/plugins/thunderid-ssr.ts index b31fba5..b44ac98 100644 --- a/packages/nuxt/src/runtime/server/plugins/thunderid-ssr.ts +++ b/packages/nuxt/src/runtime/server/plugins/thunderid-ssr.ts @@ -50,11 +50,10 @@ function resolveCallbackUrl(event: H3Event): string { * 4. In parallel (gated by `preferences`): * - Fetches user + SCIM2 user profile (`preferences.user.fetchUserProfile !== false`) * - Fetches current org + my orgs (`preferences.user.fetchOrganizations !== false`) - * - Fetches branding preference (`preferences.theme.inheritFromBranding !== false`) * 5. Writes the full {@link ThunderIDSSRData} to `event.context.thunderid.ssr` * so the Nuxt plugin can seed `useState` keys for zero-cost hydration. * - * Each fetch is individually wrapped in try/catch so a broken SCIM or branding + * Each fetch is individually wrapped in try/catch so a broken SCIM * call never crashes SSR — the client layer can recover via the existing * `/api/auth/*` routes. */ @@ -156,9 +155,8 @@ export default defineNitroPlugin((nitro: {hooks: {hook: Function}}) => { // ── 4. Parallel SSR data fetches (gated by preferences) ─────────────── const shouldFetchProfile: boolean = prefs?.user?.fetchUserProfile !== false; const shouldFetchOrgs: boolean = prefs?.user?.fetchOrganizations !== false; - const shouldFetchBranding: boolean = prefs?.theme?.inheritFromBranding !== false; - const [userResult, userProfileResult, orgsResult, currentOrgResult, brandingResult] = await Promise.allSettled([ + const [userResult, userProfileResult, orgsResult, currentOrgResult] = await Promise.allSettled([ // Always fetch the basic user object (needed for ThunderIDAuthState.user) client.getUser(session.sessionId), @@ -170,9 +168,6 @@ export default defineNitroPlugin((nitro: {hooks: {hook: Function}}) => { // Current organisation (derived from the ID token) shouldFetchOrgs ? client.getCurrentOrganization(session.sessionId) : Promise.resolve(null), - - // Branding preference (does not require a session) - shouldFetchBranding ? client.getBrandingPreference({baseUrl: resolvedBaseUrl}) : Promise.resolve(null), ]); if (userResult.status === 'rejected') { @@ -187,13 +182,9 @@ export default defineNitroPlugin((nitro: {hooks: {hook: Function}}) => { if (currentOrgResult.status === 'rejected') { log.warn('Failed to resolve current organisation:', currentOrgResult.reason); } - if (brandingResult.status === 'rejected') { - log.warn('Failed to fetch branding preference:', brandingResult.reason); - } // ── 5. Write to event context ────────────────────────────────────────── const ssrData: ThunderIDSSRData = { - brandingPreference: brandingResult.status === 'fulfilled' ? brandingResult.value : null, currentOrganization: currentOrgResult.status === 'fulfilled' ? currentOrgResult.value : null, isSignedIn: true, myOrganizations: orgsResult.status === 'fulfilled' && Array.isArray(orgsResult.value) ? orgsResult.value : [], diff --git a/packages/nuxt/src/runtime/server/routes/auth/branding/branding.get.ts b/packages/nuxt/src/runtime/server/routes/auth/branding/branding.get.ts deleted file mode 100644 index 84fbaa6..0000000 --- a/packages/nuxt/src/runtime/server/routes/auth/branding/branding.get.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import type {BrandingPreference} from '@thunderid/node'; -import {defineEventHandler, createError} from 'h3'; -import type {H3Event} from 'h3'; -import type {ThunderIDNuxtConfig} from '../../../../types'; -import ThunderIDNuxtClient from '../../../ThunderIDNuxtClient'; -import {verifyAndRehydrateSession} from '../../../utils/serverSession'; -import {useRuntimeConfig} from '#imports'; - -/** - * GET /api/auth/branding - * - * Returns the branding preference for the current tenant / organisation context. - * Resolves the correct `baseUrl` (org-scoped if the session is inside an org). - * Does not require an authenticated session — unauthenticated callers receive - * the root-tenant branding. - * - * Used by `ThunderIDRoot.revalidateBranding` to refresh client-side branding - * state without a full page reload. - */ -export default defineEventHandler(async (event: H3Event): Promise => { - const config: ReturnType = useRuntimeConfig(event); - const publicConfig: ThunderIDNuxtConfig = config.public.thunderid as ThunderIDNuxtConfig; - const sessionSecret: string | undefined = config.thunderid?.sessionSecret; - - const baseUrl: string = publicConfig?.baseUrl ?? ''; - let resolvedBaseUrl: string = baseUrl; - - // Attempt to resolve the org-scoped base URL from the session, if present. - try { - const session: Awaited> = await verifyAndRehydrateSession( - event, - sessionSecret, - ); - if (session) { - if (session.organizationId) { - resolvedBaseUrl = `${baseUrl}/o`; - } else { - const client: ThunderIDNuxtClient = ThunderIDNuxtClient.getInstance(); - const idToken: Awaited> = await client.getDecodedIdToken( - session.sessionId, - ); - if (idToken?.user_org) { - resolvedBaseUrl = `${baseUrl}/o`; - } - } - } - } catch { - // Non-fatal — fall back to the root tenant base URL - } - - try { - const client: ThunderIDNuxtClient = ThunderIDNuxtClient.getInstance(); - return await client.getBrandingPreference({baseUrl: resolvedBaseUrl}); - } catch (err) { - throw createError({ - statusCode: 500, - statusMessage: `Failed to retrieve branding preference: ${err instanceof Error ? err.message : String(err)}`, - }); - } -}); diff --git a/packages/nuxt/src/runtime/types.ts b/packages/nuxt/src/runtime/types.ts index 58a80d3..49465b6 100644 --- a/packages/nuxt/src/runtime/types.ts +++ b/packages/nuxt/src/runtime/types.ts @@ -17,7 +17,6 @@ */ import type { - BrandingPreference, I18nPreferences, Organization, TokenEndpointAuthMethod, @@ -53,11 +52,6 @@ export interface ThunderIDNuxtConfig { /** i18n configuration forwarded to `I18nProvider`. */ i18n?: I18nPreferences; theme?: { - /** - * When true (default), the Nitro plugin fetches the branding preference - * from ThunderID and passes it to `BrandingProvider` / `ThemeProvider`. - */ - inheritFromBranding?: boolean; /** * Theme mode forwarded to the Vue SDK's `ThemeProvider`. * - `'light'` (default) | `'dark'`: Fixed color scheme. Toggle at runtime with `useTheme().toggleTheme()`. @@ -138,8 +132,6 @@ export interface ThunderIDTempSessionPayload extends JWTPayload { * hydrated `useState` keys so the client never re-fetches on first render. */ export interface ThunderIDSSRData { - /** Branding preference fetched from ThunderID (null when `preferences.theme.inheritFromBranding` is false). */ - brandingPreference: BrandingPreference | null; /** The organisation the user is currently acting within (null when not in an org). */ currentOrganization: Organization | null; isSignedIn: boolean; diff --git a/packages/nuxt/tests/unit/thunderid-root.test.ts b/packages/nuxt/tests/unit/thunderid-root.test.ts index 8703e18..d974004 100644 --- a/packages/nuxt/tests/unit/thunderid-root.test.ts +++ b/packages/nuxt/tests/unit/thunderid-root.test.ts @@ -20,7 +20,6 @@ import {generateFlattenedUserProfile} from '@thunderid/browser'; import { - BrandingProvider, FlowMetaProvider, FlowProvider, I18nProvider, @@ -40,7 +39,6 @@ import {useRuntimeConfig} from '#imports'; // Provide minimal stubs for @thunderid/vue providers. They are used purely as // VNode type markers so we can locate them in the rendered VNode tree. vi.mock('@thunderid/vue', () => ({ - BrandingProvider: {name: 'BrandingProvider'}, ThemeProvider: {name: 'ThemeProvider'}, UserProvider: {name: 'UserProvider'}, OrganizationProvider: {name: 'OrganizationProvider'}, @@ -83,8 +81,6 @@ const MOCK_MY_ORGS = [ {id: 'org-2', name: 'Other Org', orgHandle: 'other-org'}, ]; -const MOCK_BRANDING = {organizationName: 'TestOrg', theme: 'default'}; - const MOCK_AUTH_STATE = {isSignedIn: true, user: {sub: 'user-123'}, isLoading: false}; // ── Helpers ─────────────────────────────────────────────────────────────────── @@ -94,7 +90,6 @@ function buildRenderFn( preferences?: Record, stateOverrides?: { auth?: any; - branding?: any; currentOrg?: any; myOrgs?: any; userProfile?: any; @@ -112,9 +107,6 @@ function buildRenderFn( mockStateStore.set('thunderid:my-orgs', { value: hasOverrides && 'myOrgs' in stateOverrides ? stateOverrides.myOrgs : MOCK_MY_ORGS, }); - mockStateStore.set('thunderid:branding', { - value: hasOverrides && 'branding' in stateOverrides ? stateOverrides.branding : MOCK_BRANDING, - }); mockStateStore.set('thunderid:auth', { value: hasOverrides && 'auth' in stateOverrides ? stateOverrides.auth : MOCK_AUTH_STATE, }); @@ -165,7 +157,7 @@ describe('ThunderIDRoot component', () => { // ── Provider tree structure ───────────────────────────────────────────── - it('renders all seven providers in the correct nesting order', () => { + it('renders all six providers in the correct nesting order', () => { const renderFn = buildRenderFn(); const root = renderFn(); @@ -175,8 +167,6 @@ describe('ThunderIDRoot component', () => { // FlowMetaProvider defaults to V1 (`enabled: false`) — `useFlowMeta()` // still resolves but the provider does not fetch v2 metadata. expect(flowMeta!.props.enabled).toBe(false); - const branding = findByType(root, BrandingProvider); - expect(branding).not.toBeNull(); const theme = findByType(root, ThemeProvider); expect(theme).not.toBeNull(); const flow = findByType(root, FlowProvider); @@ -189,22 +179,6 @@ describe('ThunderIDRoot component', () => { // ── Default preferences (all features enabled) ────────────────────────── - it('passes brandingPreference to BrandingProvider when inheritFromBranding is enabled (default)', () => { - const renderFn = buildRenderFn(); - const root = renderFn(); - - const vnode = findByType(root, BrandingProvider); - expect(vnode!.props.brandingPreference).toEqual(MOCK_BRANDING); - }); - - it('passes inheritFromBranding:true to ThemeProvider when preference is enabled (default)', () => { - const renderFn = buildRenderFn(); - const root = renderFn(); - - const vnode = findByType(root, ThemeProvider); - expect(vnode!.props.inheritFromBranding).toBe(true); - }); - it('passes full profile data to UserProvider when fetchUserProfile is enabled (default)', () => { const renderFn = buildRenderFn(); const root = renderFn(); @@ -287,24 +261,6 @@ describe('ThunderIDRoot component', () => { expect(vnode!.props.revalidateMyOrganizations).toBeUndefined(); }); - // ── preferences.theme.inheritFromBranding: false ───────────────────────── - - it('passes brandingPreference:null to BrandingProvider when inheritFromBranding is false', () => { - const renderFn = buildRenderFn({theme: {inheritFromBranding: false}}); - const root = renderFn(); - - const vnode = findByType(root, BrandingProvider); - expect(vnode!.props.brandingPreference).toBeNull(); - }); - - it('passes inheritFromBranding:false to ThemeProvider when preference is false', () => { - const renderFn = buildRenderFn({theme: {inheritFromBranding: false}}); - const root = renderFn(); - - const vnode = findByType(root, ThemeProvider); - expect(vnode!.props.inheritFromBranding).toBe(false); - }); - // ── onUpdateProfile callback logic ──────────────────────────────────────── it('onUpdateProfile updates userProfileState optimistically', () => { diff --git a/packages/nuxt/tests/unit/thunderid-ssr.test.ts b/packages/nuxt/tests/unit/thunderid-ssr.test.ts index abb2b6b..8643178 100644 --- a/packages/nuxt/tests/unit/thunderid-ssr.test.ts +++ b/packages/nuxt/tests/unit/thunderid-ssr.test.ts @@ -52,7 +52,6 @@ const mockClient = vi.hoisted(() => ({ name: 'Test Org', orgHandle: 'test-org', }), - getBrandingPreference: vi.fn<(config: any) => Promise>().mockResolvedValue({organizationName: 'TestOrg'}), getDecodedIdToken: vi.fn<(sessionId: string) => Promise>().mockResolvedValue({sub: 'user-123'}), })); @@ -149,7 +148,6 @@ describe('thunderid-ssr Nitro plugin', () => { name: 'Test Org', orgHandle: 'test-org', }); - mockClient.getBrandingPreference.mockResolvedValue({organizationName: 'TestOrg'}); mockClient.getDecodedIdToken.mockResolvedValue({sub: 'user-123'}); // Default: no session cookie, root path @@ -221,7 +219,6 @@ describe('thunderid-ssr Nitro plugin', () => { expect(ssr.userProfile).toBeDefined(); expect(ssr.myOrganizations).toHaveLength(1); expect(ssr.currentOrganization).toEqual({id: 'org-1', name: 'Test Org', orgHandle: 'test-org'}); - expect(ssr.brandingPreference).toEqual({organizationName: 'TestOrg'}); }); it('calls all client methods with the correct session ID', async () => { @@ -231,7 +228,6 @@ describe('thunderid-ssr Nitro plugin', () => { expect(mockClient.getUserProfile).toHaveBeenCalledWith(MOCK_SESSION.sessionId); expect(mockClient.getMyOrganizations).toHaveBeenCalledWith(MOCK_SESSION.sessionId); expect(mockClient.getCurrentOrganization).toHaveBeenCalledWith(MOCK_SESSION.sessionId); - expect(mockClient.getBrandingPreference).toHaveBeenCalled(); }); it('writes legacy __thunderidAuth to event context for backwards compatibility', async () => { @@ -260,7 +256,6 @@ describe('thunderid-ssr Nitro plugin', () => { expect(event.context.thunderid.ssr.userProfile).toBeNull(); // other fields should still be populated expect(event.context.thunderid.ssr.myOrganizations).toHaveLength(1); - expect(event.context.thunderid.ssr.brandingPreference).toBeDefined(); }); it('skips org fetches when preferences.user.fetchOrganizations is false', async () => { @@ -279,28 +274,8 @@ describe('thunderid-ssr Nitro plugin', () => { expect(mockClient.getCurrentOrganization).not.toHaveBeenCalled(); expect(event.context.thunderid.ssr.myOrganizations).toEqual([]); expect(event.context.thunderid.ssr.currentOrganization).toBeNull(); - // user and branding should still be populated + // user should still be populated expect(event.context.thunderid.ssr.user).toBeDefined(); - expect(event.context.thunderid.ssr.brandingPreference).toBeDefined(); - }); - - it('skips branding fetch when preferences.theme.inheritFromBranding is false', async () => { - vi.mocked(useRuntimeConfig).mockReturnValue({ - public: { - thunderid: { - baseUrl: 'https://localhost:8090', - preferences: {theme: {inheritFromBranding: false}}, - }, - }, - } as any); - - const event = await callHandler('/', 'valid-cookie'); - - expect(mockClient.getBrandingPreference).not.toHaveBeenCalled(); - expect(event.context.thunderid.ssr.brandingPreference).toBeNull(); - // other fields should still be populated - expect(event.context.thunderid.ssr.user).toBeDefined(); - expect(event.context.thunderid.ssr.myOrganizations).toHaveLength(1); }); // ── Non-fatal partial failures ──────────────────────────────────────────── @@ -327,16 +302,6 @@ describe('thunderid-ssr Nitro plugin', () => { expect(event.context.thunderid.ssr.user).toBeDefined(); }); - it('still writes SSR data when getBrandingPreference throws (non-fatal)', async () => { - mockClient.getBrandingPreference.mockRejectedValueOnce(new Error('branding error')); - - const event = await callHandler('/', 'valid-cookie'); - - expect(event.context.thunderid.ssr.isSignedIn).toBe(true); - expect(event.context.thunderid.ssr.brandingPreference).toBeNull(); - expect(event.context.thunderid.ssr.user).toBeDefined(); - }); - // ── Org-scoped base URL resolution ──────────────────────────────────────── it('sets resolvedBaseUrl to baseUrl/o when session has organizationId', async () => { diff --git a/packages/react/src/contexts/Branding/BrandingContext.ts b/packages/react/src/contexts/Branding/BrandingContext.ts deleted file mode 100644 index 6689f9f..0000000 --- a/packages/react/src/contexts/Branding/BrandingContext.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {BrandingPreference, Theme} from '@thunderid/browser'; -import {Context, createContext} from 'react'; - -export interface BrandingContextValue { - /** - * The active theme mode from branding preference ('light' | 'dark') - */ - activeTheme: 'light' | 'dark' | null; - /** - * The raw branding preference data - */ - brandingPreference: BrandingPreference | null; - /** - * Error state - */ - error: Error | null; - /** - * Function to manually fetch branding preference - */ - fetchBranding: () => Promise; - /** - * Loading state - */ - isLoading: boolean; - /** - * Function to refetch branding preference - * This bypasses the single-call restriction and forces a new API call - */ - refetch: () => Promise; - /** - * The transformed theme object - */ - theme: Theme | null; -} - -const BrandingContext: Context = createContext(null); - -BrandingContext.displayName = 'BrandingContext'; - -export default BrandingContext; diff --git a/packages/react/src/contexts/Branding/BrandingProvider.tsx b/packages/react/src/contexts/Branding/BrandingProvider.tsx deleted file mode 100644 index f3b406d..0000000 --- a/packages/react/src/contexts/Branding/BrandingProvider.tsx +++ /dev/null @@ -1,158 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {BrandingPreference, Theme, transformBrandingPreferenceToTheme} from '@thunderid/browser'; -import {FC, PropsWithChildren, ReactElement, useCallback, useEffect, useState} from 'react'; -import BrandingContext from './BrandingContext'; - -/** - * Configuration options for the BrandingProvider - */ -export interface BrandingProviderProps { - /** - * The branding preference data passed from parent (typically ThunderIDProvider) - */ - brandingPreference?: BrandingPreference | null; - /** - * Whether the branding provider is enabled - * @default true - */ - enabled?: boolean; - /** - * Error state passed from parent - */ - error?: Error | null; - /** - * Force a specific theme ('light' or 'dark') - * If not provided, will use the activeTheme from branding preference - */ - forceTheme?: 'light' | 'dark'; - /** - * Loading state passed from parent - */ - isLoading?: boolean; - /** - * Function to refetch branding preference passed from parent - */ - refetch?: () => Promise; -} - -/** - * BrandingProvider component that manages branding state and provides branding context to child components. - * - * This provider receives branding preferences from a parent component (typically ThunderIDProvider) - * and transforms them into theme objects, making them available to all child components. - * - * Features: - * - Receives branding preferences as props - * - Theme transformation from branding preferences - * - Loading and error states - * - Support for custom theme forcing - * - * @example - * Basic usage (typically used within ThunderIDProvider): - * ```tsx - * - * - * - * ``` - * - * @example - * With custom theme forcing: - * ```tsx - * - * - * - * ``` - */ -const BrandingProvider: FC> = ({ - children, - brandingPreference: externalBrandingPreference, - forceTheme, - enabled = true, - isLoading: externalIsLoading = false, - error: externalError = null, - refetch: externalRefetch, -}: PropsWithChildren): ReactElement => { - const [theme, setTheme] = useState(null); - const [activeTheme, setActiveTheme] = useState<'light' | 'dark' | null>(null); - - // Process branding preference when it changes - useEffect(() => { - if (!enabled || !externalBrandingPreference) { - setTheme(null); - setActiveTheme(null); - return; - } - - // Extract active theme from branding preference - const activeThemeFromBranding: string | undefined = externalBrandingPreference?.preference?.theme?.activeTheme; - let extractedActiveTheme: 'light' | 'dark' | null = null; - - if (activeThemeFromBranding) { - // Convert to lowercase and map to our expected values - const themeMode: string = activeThemeFromBranding.toLowerCase(); - if (themeMode === 'light' || themeMode === 'dark') { - extractedActiveTheme = themeMode; - } - } - - setActiveTheme(extractedActiveTheme); - - // Transform branding preference to theme - const transformedTheme: Theme | null = transformBrandingPreferenceToTheme(externalBrandingPreference, forceTheme); - setTheme(transformedTheme); - }, [externalBrandingPreference, forceTheme, enabled]); - - // Reset state when disabled - useEffect(() => { - if (!enabled) { - setTheme(null); - setActiveTheme(null); - } - }, [enabled]); - - // Dummy fetchBranding for backward compatibility - const fetchBranding: () => Promise = useCallback(async (): Promise => { - if (externalRefetch) { - await externalRefetch(); - } - }, [externalRefetch]); - - const value: any = { - activeTheme, - brandingPreference: externalBrandingPreference || null, - error: externalError, - fetchBranding, - isLoading: externalIsLoading, - refetch: externalRefetch || fetchBranding, - theme, - }; - - return {children}; -}; - -export default BrandingProvider; diff --git a/packages/react/src/contexts/Branding/useBrandingContext.ts b/packages/react/src/contexts/Branding/useBrandingContext.ts deleted file mode 100644 index fda05f3..0000000 --- a/packages/react/src/contexts/Branding/useBrandingContext.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {useContext} from 'react'; -import BrandingContext, {BrandingContextValue} from './BrandingContext'; - -/** - * Hook to access the branding context. - * This hook provides access to branding preferences, theme data, and loading states. - * - * @returns The branding context value containing branding preference data, theme, and control functions - * @throws Error if used outside of a BrandingProvider - * - * @example - * ```tsx - * function MyComponent() { - * const { theme, activeTheme, isLoading, error } = useBrandingContext(); - * - * if (isLoading) return
Loading branding...
; - * if (error) return
Error: {error.message}
; - * - * return ( - *
- *

Active theme mode: {activeTheme}

- *

Styled with ThunderID branding

- *
- * ); - * } - * ``` - */ -const useBrandingContext = (): BrandingContextValue => { - const context: BrandingContextValue | null = useContext(BrandingContext); - if (!context) { - throw new Error('useBrandingContext must be used within a BrandingProvider'); - } - return context; -}; - -export default useBrandingContext; diff --git a/packages/react/src/contexts/Theme/ThemeContext.ts b/packages/react/src/contexts/Theme/ThemeContext.ts index 7ea2642..cc891b3 100644 --- a/packages/react/src/contexts/Theme/ThemeContext.ts +++ b/packages/react/src/contexts/Theme/ThemeContext.ts @@ -20,23 +20,11 @@ import {Theme} from '@thunderid/browser'; import {Context, createContext} from 'react'; export interface ThemeContextValue { - /** - * Error from branding theme fetch, if any - */ - brandingError?: Error | null; colorScheme: 'light' | 'dark'; /** * The text direction for the UI. */ direction: 'ltr' | 'rtl'; - /** - * Whether branding inheritance is enabled - */ - inheritFromBranding?: boolean; - /** - * Whether branding theme is currently loading - */ - isBrandingLoading?: boolean; theme: Theme; toggleTheme: () => void; } diff --git a/packages/react/src/contexts/Theme/ThemeProvider.tsx b/packages/react/src/contexts/Theme/ThemeProvider.tsx index 8a32e3f..612043e 100644 --- a/packages/react/src/contexts/Theme/ThemeProvider.tsx +++ b/packages/react/src/contexts/Theme/ThemeProvider.tsx @@ -25,11 +25,6 @@ import normalizeThemeConfig from '../../utils/normalizeThemeConfig'; import FlowMetaContext, {FlowMetaContextValue} from '../FlowMeta/FlowMetaContext'; export interface ThemeProviderProps { - /** - * When true, seeds the theme from the nearest BrandingContext if no flow meta theme is available. - */ - inheritFromBranding?: boolean; - /** * Initial color scheme override. Overrides the server default when provided. */ @@ -150,11 +145,8 @@ const ThemeProvider: FC> = ({ }, [direction]); const value: any = { - brandingError: error, colorScheme, direction, - inheritFromBranding: false, - isBrandingLoading: isLoading, theme, toggleTheme, }; diff --git a/packages/react/src/contexts/ThunderID/ThunderIDProvider.tsx b/packages/react/src/contexts/ThunderID/ThunderIDProvider.tsx index 81bfe63..9309e8e 100644 --- a/packages/react/src/contexts/ThunderID/ThunderIDProvider.tsx +++ b/packages/react/src/contexts/ThunderID/ThunderIDProvider.tsx @@ -25,9 +25,6 @@ import { SignInOptions, User, UserProfile, - getBrandingPreference, - GetBrandingPreferenceConfig, - BrandingPreference, IdToken, extractUserClaimsFromIdToken, EmbeddedSignInFlowResponse, @@ -39,7 +36,6 @@ import ThunderIDContext from './ThunderIDContext'; import useBrowserUrl from '../../hooks/useBrowserUrl'; import {ThunderIDReactConfig} from '../../models/config'; import ThunderIDReactClient from '../../ThunderIDReactClient'; -import BrandingProvider from '../Branding/BrandingProvider'; import ComponentRendererProvider from '../ComponentRenderer/ComponentRendererProvider'; import FlowProvider from '../Flow/FlowProvider'; import FlowMetaProvider from '../FlowMeta/FlowMetaProvider'; @@ -112,21 +108,9 @@ const ThunderIDProvider: FC> = ({ const [isUpdatingSession, setIsUpdatingSession] = useState(false); const [wellKnown, setWellKnown] = useState(null); - // Branding state - const [brandingPreference, setBrandingPreference] = useState(null); - const [isBrandingLoading, setIsBrandingLoading] = useState(false); - const [brandingError, setBrandingError] = useState(null); - const [hasFetchedBranding, setHasFetchedBranding] = useState(false); - useEffect(() => { setBaseUrl(initialBaseUrl ?? ''); - // Reset branding state when baseUrl changes - if (initialBaseUrl !== baseUrl) { - setHasFetchedBranding(false); - setBrandingPreference(null); - setBrandingError(null); - } - }, [initialBaseUrl, baseUrl]); + }, [initialBaseUrl]); useEffect(() => { (async (): Promise => { @@ -366,66 +350,6 @@ const ThunderIDProvider: FC> = ({ }; }, [client, isLoadingSync, isSignedInSync, isUpdatingSession]); - // Branding fetch function - const fetchBranding: () => Promise = useCallback(async (): Promise => { - if (!baseUrl) { - return; - } - - // Prevent multiple calls if already fetching - if (isBrandingLoading) { - return; - } - - setIsBrandingLoading(true); - setBrandingError(null); - - try { - const getBrandingConfig: GetBrandingPreferenceConfig = { - baseUrl, - locale: preferences?.i18n?.language, - // Add other branding config options as needed - }; - - const brandingData: BrandingPreference = await getBrandingPreference(getBrandingConfig); - setBrandingPreference(brandingData); - setHasFetchedBranding(true); - } catch (err) { - const errorMessage: Error = err instanceof Error ? err : new Error('Failed to fetch branding preference'); - setBrandingError(errorMessage); - setBrandingPreference(null); - setHasFetchedBranding(true); // Mark as fetched even on error to prevent retries - } finally { - setIsBrandingLoading(false); - } - }, [baseUrl, preferences?.i18n?.language]); - - // Refetch branding function - const refetchBranding: () => Promise = useCallback(async (): Promise => { - setHasFetchedBranding(false); // Reset the flag to allow refetching - await fetchBranding(); - }, [fetchBranding]); - - // Auto-fetch branding when initialized and configured - useEffect(() => { - // TEMPORARY: Branding preference is not yet supported. - return; - - // Only fetch branding when explicitly enabled via preferences.theme.inheritFromBranding - const shouldFetchBranding: boolean = preferences?.theme?.inheritFromBranding === true; - - if (shouldFetchBranding && isInitializedSync && baseUrl && !hasFetchedBranding && !isBrandingLoading) { - fetchBranding(); - } - }, [ - preferences?.theme?.inheritFromBranding, - isInitializedSync, - baseUrl, - hasFetchedBranding, - isBrandingLoading, - fetchBranding, - ]); - const signInSilently = async (options?: SignInOptions): Promise => { try { setIsUpdatingSession(true); @@ -620,36 +544,28 @@ const ThunderIDProvider: FC> = ({ - - - - - => client.getAllOrganizations()} - myOrganizations={myOrganizations} - currentOrganization={currentOrganization} - onOrganizationSwitch={switchOrganization} - revalidateMyOrganizations={async (): Promise => client.getMyOrganizations()} - > - - {children} - - - - - - + + + => client.getAllOrganizations()} + myOrganizations={myOrganizations} + currentOrganization={currentOrganization} + onOrganizationSwitch={switchOrganization} + revalidateMyOrganizations={async (): Promise => client.getMyOrganizations()} + > + + {children} + + + + + diff --git a/packages/react/src/hooks/useBranding.ts b/packages/react/src/hooks/useBranding.ts deleted file mode 100644 index 99b017d..0000000 --- a/packages/react/src/hooks/useBranding.ts +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {BrandingPreference, Theme, createPackageComponentLogger} from '@thunderid/browser'; -import useBrandingContext from '../contexts/Branding/useBrandingContext'; - -const logger: ReturnType = createPackageComponentLogger( - '@thunderid/react', - 'useBranding', -); - -/** - * Configuration options for the useBranding hook - * @deprecated Use BrandingProvider instead for better performance and consistency - */ -export interface UseBrandingConfig { - /** - * @deprecated This configuration is now handled by BrandingProvider - */ - autoFetch?: boolean; - /** - * @deprecated This configuration is now handled by BrandingProvider - */ - fetcher?: (url: string, config: RequestInit) => Promise; - /** - * @deprecated This configuration is now handled by BrandingProvider - */ - forceTheme?: 'light' | 'dark'; - /** - * @deprecated This configuration is now handled by BrandingProvider - */ - locale?: string; - /** - * @deprecated This configuration is now handled by BrandingProvider - */ - name?: string; - /** - * @deprecated This configuration is now handled by BrandingProvider - */ - type?: string; -} - -/** - * Return type of the useBranding hook - */ -export interface UseBrandingReturn { - /** - * The active theme mode from branding preference ('light' | 'dark') - */ - activeTheme: 'light' | 'dark' | null; - /** - * The raw branding preference data - */ - brandingPreference: BrandingPreference | null; - /** - * Error state - */ - error: Error | null; - /** - * Function to manually fetch branding preference - */ - fetchBranding: () => Promise; - /** - * Loading state - */ - isLoading: boolean; - /** - * Function to refetch branding preference - * This bypasses the single-call restriction and forces a new API call - */ - refetch: () => Promise; - /** - * The transformed theme object - */ - theme: Theme | null; -} - -/** - * React hook for accessing branding preferences from the BrandingProvider context. - * This hook provides access to branding preferences, theme data, and loading states. - * - * @deprecated Consider using useBrandingContext directly for better performance. - * This hook is maintained for backward compatibility. - * - * @param config - Configuration options (deprecated, use BrandingProvider props instead) - * @returns Object containing branding preference data, theme, loading state, error, and refetch function - * - * @example - * Basic usage: - * ```tsx - * function MyComponent() { - * const { theme, activeTheme, isLoading, error } = useBranding(); - * - * if (isLoading) return
Loading branding...
; - * if (error) return
Error: {error.message}
; - * - * return ( - *
- *

Active theme mode: {activeTheme}

- *

Styled with ThunderID branding

- *
- * ); - * } - * ``` - * - * @example - * For new implementations, use BrandingProvider with useBrandingContext: - * ```tsx - * // In your root component - * - * - * - * - * // In your component - * function MyComponent() { - * const { theme, activeTheme, isLoading, error } = useBrandingContext(); - * // ... rest of your component - * } - * ``` - */ -export const useBranding = (): UseBrandingReturn => { - try { - return useBrandingContext(); - } catch (error) { - logger.warn( - 'useBranding: BrandingProvider not available. ' + - 'Make sure to wrap your app with BrandingProvider or ThunderIDProvider with branding preferences.', - ); - - return { - activeTheme: null, - brandingPreference: null, - error: new Error('BrandingProvider not available'), - fetchBranding: async (): Promise => {}, - isLoading: false, - refetch: async (): Promise => {}, - theme: null, - }; - } -}; - -export default useBranding; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 1a22241..ecc338c 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -64,14 +64,6 @@ export * from './contexts/Theme/ThemeProvider'; export {default as useTheme} from './contexts/Theme/useTheme'; -export {default as BrandingContext} from './contexts/Branding/BrandingContext'; -export * from './contexts/Branding/BrandingContext'; - -export {default as BrandingProvider} from './contexts/Branding/BrandingProvider'; -export * from './contexts/Branding/BrandingProvider'; - -export {default as useBrandingContext} from './contexts/Branding/useBrandingContext'; - export {default as useBrowserUrl} from './hooks/useBrowserUrl'; export * from './hooks/useBrowserUrl'; @@ -80,8 +72,6 @@ export * from './hooks/useTranslation'; export {default as useForm} from './hooks/useForm'; -export {default as useBranding} from './hooks/useBranding'; - export {default as BaseSignInButton} from './components/actions/SignInButton/BaseSignInButton'; export * from './components/actions/SignInButton/BaseSignInButton'; @@ -176,6 +166,12 @@ export * from './components/presentation/UserProfile/BaseUserProfile'; export {default as UserProfile} from './components/presentation/UserProfile/UserProfile'; export * from './components/presentation/UserProfile/UserProfile'; +export {default as BaseUserAvatar} from './components/presentation/UserAvatar/BaseUserAvatar'; +export * from './components/presentation/UserAvatar/BaseUserAvatar'; + +export {default as UserAvatar} from './components/presentation/UserAvatar/UserAvatar'; +export * from './components/presentation/UserAvatar/UserAvatar'; + export {default as BaseUserDropdown} from './components/presentation/UserDropdown/BaseUserDropdown'; export type {BaseUserDropdownProps} from './components/presentation/UserDropdown/BaseUserDropdown'; diff --git a/packages/vue/src/__tests__/composables/secondary-composables.test.ts b/packages/vue/src/__tests__/composables/secondary-composables.test.ts index 4d43a06..8fe73f1 100644 --- a/packages/vue/src/__tests__/composables/secondary-composables.test.ts +++ b/packages/vue/src/__tests__/composables/secondary-composables.test.ts @@ -20,17 +20,15 @@ import {mount} from '@vue/test-utils'; import {describe, expect, it, vi} from 'vitest'; import {defineComponent, h, ref} from 'vue'; -import useBranding from '../../composables/useBranding'; import useFlow from '../../composables/useFlow'; import useFlowMeta from '../../composables/useFlowMeta'; import useI18n from '../../composables/useI18n'; import useTheme from '../../composables/useTheme'; -import {FLOW_KEY, FLOW_META_KEY, THEME_KEY, BRANDING_KEY, I18N_KEY} from '../../keys'; +import {FLOW_KEY, FLOW_META_KEY, THEME_KEY, I18N_KEY} from '../../keys'; import type { FlowContextValue, FlowMetaContextValue, ThemeContextValue, - BrandingContextValue, I18nContextValue, } from '../../models/contexts'; @@ -160,49 +158,6 @@ describe('useTheme', () => { }); }); -describe('useBranding', () => { - it('should return the BrandingContextValue when called inside a provider', () => { - const mockContext: Partial = { - brandingPreference: ref(null) as any, - theme: ref(null) as any, - isLoading: ref(false) as any, - error: ref(null) as any, - refetch: vi.fn(), - }; - let result: BrandingContextValue | undefined; - - const TestChild = defineComponent({ - setup() { - result = useBranding(); - return () => h('div', 'test'); - }, - }); - - mount(TestChild, { - global: { - provide: { - [BRANDING_KEY as symbol]: mockContext, - }, - }, - }); - - expect(result).toBeDefined(); - }); - - it('should throw an error when called outside of ThunderIDProvider', () => { - const TestChild = defineComponent({ - setup() { - useBranding(); - return () => h('div', 'test'); - }, - }); - - expect(() => { - mount(TestChild); - }).toThrow('[ThunderID] useBranding() was called outside of '); - }); -}); - describe('useI18n', () => { it('should return the I18nContextValue when called inside a provider', () => { const mockContext: Partial = { diff --git a/packages/vue/src/composables/useBranding.ts b/packages/vue/src/composables/useBranding.ts deleted file mode 100644 index 1022632..0000000 --- a/packages/vue/src/composables/useBranding.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {inject} from 'vue'; -import {BRANDING_KEY} from '../keys'; -import type {BrandingContextValue} from '../models/contexts'; - -/** - * Composable for accessing branding preference data. - * - * Must be called inside a component that is a descendant of ``. - * - * @returns {BrandingContextValue} The branding context with preferences, theme, and fetch operations. - * @throws {Error} If called outside of ``. - * - * @example - * ```vue - * - * - * - * ``` - */ -const useBranding = (): BrandingContextValue => { - const context: unknown = inject(BRANDING_KEY); - - if (!context) { - throw new Error( - '[ThunderID] useBranding() was called outside of . ' + - 'Make sure to install the ThunderIDPlugin or wrap your app with .', - ); - } - - return context as BrandingContextValue; -}; - -export default useBranding; diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index b6cf034..ef7e1e5 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -24,7 +24,6 @@ export type {ThunderIDPluginOptions} from './plugins/ThunderIDPlugin'; export {default as ThunderIDProvider} from './providers/ThunderIDProvider'; // ── Providers ── -export {default as BrandingProvider} from './providers/BrandingProvider'; export {default as FlowMetaProvider} from './providers/FlowMetaProvider'; export {default as FlowProvider} from './providers/FlowProvider'; export {default as I18nProvider} from './providers/I18nProvider'; @@ -34,7 +33,6 @@ export {default as UserProvider} from './providers/UserProvider'; // ── Composables ── export {default as useThunderID} from './composables/useThunderID'; -export {default as useBranding} from './composables/useBranding'; export {default as useFlow} from './composables/useFlow'; export {default as useFlowMeta} from './composables/useFlowMeta'; export {default as useI18n} from './composables/useI18n'; @@ -50,7 +48,6 @@ export {default as ThunderIDVueClient} from './ThunderIDVueClient'; // ── Keys ── export { THUNDERID_KEY, - BRANDING_KEY, FLOW_KEY, FLOW_META_KEY, I18N_KEY, @@ -63,7 +60,6 @@ export { export type {ThunderIDVueConfig} from './models/config'; export type { ThunderIDContext, - BrandingContextValue, FlowContextValue, FlowMessage, FlowMetaContextValue, diff --git a/packages/vue/src/keys.ts b/packages/vue/src/keys.ts index 2aa14d5..406672f 100644 --- a/packages/vue/src/keys.ts +++ b/packages/vue/src/keys.ts @@ -19,7 +19,6 @@ import type {InjectionKey} from 'vue'; import type { ThunderIDContext, - BrandingContextValue, FlowContextValue, FlowMetaContextValue, I18nContextValue, @@ -58,11 +57,6 @@ export const FLOW_META_KEY: InjectionKey = Symbol('thunder */ export const THEME_KEY: InjectionKey = Symbol('thunderid-theme'); -/** - * Injection key for the Branding context (branding preferences from server). - */ -export const BRANDING_KEY: InjectionKey = Symbol('thunderid-branding'); - /** * Injection key for the I18n context (translation function, language switching). */ diff --git a/packages/vue/src/models/contexts.ts b/packages/vue/src/models/contexts.ts index 14d0a95..8f80fc5 100644 --- a/packages/vue/src/models/contexts.ts +++ b/packages/vue/src/models/contexts.ts @@ -18,7 +18,6 @@ import type { AllOrganizationsApiResponse, - BrandingPreference, CreateOrganizationPayload, FlowMetadataResponse, HttpRequestConfig, @@ -252,46 +251,16 @@ export interface FlowMetaContextValue { * Shape of the Theme context exposed by `useTheme()`. */ export interface ThemeContextValue { - /** Error from the branding theme fetch, if any. */ - brandingError: Readonly>; /** The current color scheme ('light' | 'dark'). */ colorScheme: Readonly>; /** The text direction for the UI. */ direction: Readonly>; - /** Whether the theme inherits from ThunderID branding preferences. */ - inheritFromBranding: boolean; - /** Whether the branding theme is currently loading. */ - isBrandingLoading: Readonly>; /** The resolved Theme object used by all styled components. */ theme: Readonly>; /** Toggle between light and dark mode. */ toggleTheme: () => void; } -// ───────────────────────────────────────────────────────────────────────────── -// Branding Context -// ───────────────────────────────────────────────────────────────────────────── - -/** - * Shape of the Branding context exposed by `useBranding()`. - */ -export interface BrandingContextValue { - /** The active theme from the branding preference ('light' | 'dark'), or null. */ - activeTheme: Readonly>; - /** The raw branding preference data from the server. */ - brandingPreference: Readonly>; - /** Error from the branding fetch, if any. */ - error: Readonly>; - /** Trigger a branding preference fetch (deduplicated). */ - fetchBranding: () => Promise; - /** Whether the branding preference is currently loading. */ - isLoading: Readonly>; - /** Force a fresh branding preference fetch (bypasses dedup). */ - refetch: () => Promise; - /** The transformed `Theme` object derived from the branding preference. */ - theme: Readonly>; -} - // ───────────────────────────────────────────────────────────────────────────── // I18n Context // ───────────────────────────────────────────────────────────────────────────── diff --git a/packages/vue/src/providers/BrandingProvider.ts b/packages/vue/src/providers/BrandingProvider.ts deleted file mode 100644 index 913ab9c..0000000 --- a/packages/vue/src/providers/BrandingProvider.ts +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {BrandingPreference, Theme, transformBrandingPreferenceToTheme} from '@thunderid/browser'; -import { - computed, - defineComponent, - h, - provide, - readonly, - shallowReadonly, - ref, - watch, - type Component, - type PropType, - type Ref, - type SetupContext, - type VNode, -} from 'vue'; -import {BRANDING_KEY} from '../keys'; -import type {BrandingContextValue} from '../models/contexts'; - -interface BrandingProviderProps { - brandingPreference: BrandingPreference | null; - enabled: boolean; - error: Error | null; - forceTheme: 'light' | 'dark' | undefined; - isLoading: boolean; - refetch: (() => Promise) | undefined; -} - -/** - * BrandingProvider manages branding preference state and makes branding data - * available to child components via `useBranding()`. - * - * It receives branding preferences from a parent component (typically - * ``) and transforms them into `Theme` objects. - * - * @internal — This provider is mounted automatically by ``. - */ -const BrandingProvider: Component = defineComponent({ - name: 'BrandingProvider', - props: { - /** Whether branding is enabled. When false the context provides null. */ - brandingPreference: { - default: null, - type: Object as PropType, - }, - /** Loading state from the parent. */ - enabled: { - default: true, - type: Boolean, - }, - error: { - default: null, - type: Object as PropType, - }, - /** Force a specific theme mode, overriding the one declared in branding. */ - forceTheme: { - default: undefined, - type: String as PropType<'light' | 'dark'>, - }, - /** Re-fetch callback from the parent (bypasses dedup). */ - isLoading: { - default: false, - type: Boolean, - }, - refetch: { - default: undefined, - type: Function as PropType<() => Promise>, - }, - }, - setup(props: BrandingProviderProps, {slots}: SetupContext): () => VNode { - const theme: Ref = ref(null); - const activeTheme: Ref<'light' | 'dark' | null> = ref(null); - - // Process branding preference whenever it changes - const processBranding = (): void => { - if (!props.enabled || !props.brandingPreference) { - theme.value = null; - activeTheme.value = null; - return; - } - - const activeThemeFromBranding: string | undefined = (props.brandingPreference as any)?.preference?.theme - ?.activeTheme; - if (activeThemeFromBranding) { - const mode: string = activeThemeFromBranding.toLowerCase(); - activeTheme.value = mode === 'light' || mode === 'dark' ? mode : null; - } else { - activeTheme.value = null; - } - - const transformedTheme: Theme | null = transformBrandingPreferenceToTheme( - props.brandingPreference, - props.forceTheme, - ); - theme.value = transformedTheme; - }; - - watch(() => [props.brandingPreference, props.forceTheme, props.enabled], processBranding, {immediate: true}); - - const fetchBranding = async (): Promise => { - if (props.refetch) { - await props.refetch(); - } - }; - - const context: BrandingContextValue = { - activeTheme: readonly(activeTheme), - brandingPreference: readonly(computed(() => props.brandingPreference)) as Readonly< - Ref - >, - error: readonly(computed(() => props.error)) as Readonly>, - fetchBranding, - isLoading: readonly(computed(() => props.isLoading)) as Readonly>, - refetch: props.refetch ?? fetchBranding, - theme: shallowReadonly(theme), - }; - - provide(BRANDING_KEY, context); - - return (): VNode => h('div', {style: 'display:contents'}, slots['default']?.()); - }, -}); - -export default BrandingProvider; diff --git a/packages/vue/src/providers/ThemeProvider.ts b/packages/vue/src/providers/ThemeProvider.ts index 12180ab..c7c5875 100644 --- a/packages/vue/src/providers/ThemeProvider.ts +++ b/packages/vue/src/providers/ThemeProvider.ts @@ -33,7 +33,6 @@ import { computed, defineComponent, h, - inject, onBeforeUnmount, onMounted, provide, @@ -47,11 +46,8 @@ import { type SetupContext, type VNode, } from 'vue'; -import {BRANDING_KEY, THEME_KEY} from '../keys'; -import type {BrandingContextValue, ThemeContextValue} from '../models/contexts'; -import {createVueLogger} from '../utils/logger'; - -const logger: ReturnType = createVueLogger('ThemeProvider'); +import {THEME_KEY} from '../keys'; +import type {ThemeContextValue} from '../models/contexts'; /** * ThemeProvider manages theme state and provides it to child components via `useTheme()`. @@ -60,20 +56,17 @@ const logger: ReturnType = createVueLogger('ThemeProvide * - Fixed color schemes (`light` | `dark`) * - System preference detection (`system`) * - CSS-class-based detection (`class`) - * - Branding-driven mode (`branding`) — inherits the active theme from `BrandingProvider` - * - Merging server branding theme with local overrides * - CSS variable injection onto `document.documentElement` * * @example * ```vue - * + * * * * ``` */ interface ThemeProviderProps { detection: BrowserThemeDetection; - inheritFromBranding: boolean; mode: ThemeMode | 'branding'; theme: RecursivePartial | undefined; } @@ -83,8 +76,6 @@ const ThemeProvider: Component = defineComponent({ props: { /** Theme detection configuration (for 'class' or 'system' mode). */ detection: {default: () => ({}), type: Object as PropType}, - /** Whether to inherit theme from ThunderID branding preference. */ - inheritFromBranding: {default: true as ThemePreferences['inheritFromBranding'], type: Boolean}, /** * The theme mode: * - `'light'` | `'dark'`: Fixed color scheme. @@ -100,9 +91,6 @@ const ThemeProvider: Component = defineComponent({ theme: {default: undefined, type: Object as PropType>}, }, setup(props: ThemeProviderProps, {slots}: SetupContext): () => VNode { - // Try to consume branding context – it is optional (BrandingProvider may not be mounted) - const brandingContext: BrandingContextValue | null = inject(BRANDING_KEY, null); - const initColorScheme = (): 'light' | 'dark' => { if (props.mode === 'light' || props.mode === 'dark') return props.mode; if (props.mode === 'branding') return detectThemeMode('system', props.detection); @@ -111,58 +99,9 @@ const ThemeProvider: Component = defineComponent({ const colorScheme: Ref<'light' | 'dark'> = ref(initColorScheme()); - // Update color scheme when branding's active theme is available - watch( - () => (brandingContext as any)?.activeTheme.value, - (brandingActiveTheme: string | 'light' | 'dark' | undefined): void => { - if (!props.inheritFromBranding || !brandingActiveTheme) return; - if (props.mode === 'branding') { - colorScheme.value = brandingActiveTheme as 'light' | 'dark'; - } else if (props.mode === 'system' && !(brandingContext as any)?.isLoading.value) { - colorScheme.value = brandingActiveTheme as 'light' | 'dark'; - } - }, - ); - - // Warn if inheritFromBranding is true but no BrandingProvider is present - if (props.inheritFromBranding && !brandingContext) { - logger.warn( - 'ThemeProvider: inheritFromBranding is enabled but BrandingProvider is not available. ' + - 'Make sure to wrap your app with BrandingProvider or ThunderIDProvider.', - ); - } - - // Merge branding theme with user-provided overrides const finalThemeConfig: Ref | undefined> = computed< RecursivePartial | undefined - >(() => { - const themeConfig: RecursivePartial | undefined = props.theme; - const brandingTheme: RecursivePartial | null | undefined = props.inheritFromBranding - ? (brandingContext as any)?.theme.value - : null; - - if (!brandingTheme) return themeConfig; - - const brandingThemeConfig: RecursivePartial = { - borderRadius: brandingTheme.borderRadius, - colors: brandingTheme.colors, - components: brandingTheme.components, - images: brandingTheme.images, - shadows: brandingTheme.shadows, - spacing: brandingTheme.spacing, - }; - - return { - ...brandingThemeConfig, - ...themeConfig, - borderRadius: {...brandingThemeConfig.borderRadius, ...themeConfig?.borderRadius}, - colors: {...brandingThemeConfig.colors, ...themeConfig?.colors}, - components: {...brandingThemeConfig.components, ...themeConfig?.components}, - images: {...brandingThemeConfig.images, ...themeConfig?.images}, - shadows: {...brandingThemeConfig.shadows, ...themeConfig?.shadows}, - spacing: {...brandingThemeConfig.spacing, ...themeConfig?.spacing}, - }; - }); + >(() => props.theme); const resolvedTheme: Ref = computed(() => createTheme(finalThemeConfig.value, colorScheme.value === 'dark'), @@ -217,9 +156,7 @@ const ThemeProvider: Component = defineComponent({ classObserver = createClassObserver(targetElement, handleThemeChange, props.detection); } } else if (props.mode === 'system') { - if (!props.inheritFromBranding || !(brandingContext as any)?.activeTheme.value) { - mediaQuery = createMediaQueryListener(handleThemeChange); - } + mediaQuery = createMediaQueryListener(handleThemeChange); } }); @@ -231,11 +168,8 @@ const ThemeProvider: Component = defineComponent({ }); const context: ThemeContextValue = { - brandingError: brandingContext?.error ?? readonly(ref(null)), colorScheme: readonly(colorScheme), direction: readonly(direction) as Readonly>, - inheritFromBranding: props.inheritFromBranding, - isBrandingLoading: brandingContext?.isLoading ?? readonly(ref(false)), theme: shallowReadonly(resolvedTheme), toggleTheme, }; diff --git a/packages/vue/src/providers/ThunderIDProvider.ts b/packages/vue/src/providers/ThunderIDProvider.ts index 6053eba..11dbd06 100644 --- a/packages/vue/src/providers/ThunderIDProvider.ts +++ b/packages/vue/src/providers/ThunderIDProvider.ts @@ -47,7 +47,6 @@ import { type PropType, type VNode, } from 'vue'; -import BrandingProvider from './BrandingProvider'; import FlowMetaProvider from './FlowMetaProvider'; import FlowProvider from './FlowProvider'; import I18nProvider from './I18nProvider'; @@ -484,70 +483,67 @@ const ThunderIDProvider: Component = defineComponent({ {enabled: true}, { default: (): any => - h(BrandingProvider, null, { + h(ThemeProvider, null, { default: (): any => - h(ThemeProvider, null, { + h(FlowProvider, null, { default: (): any => - h(FlowProvider, null, { - default: (): any => - h( - UserProvider, - { - onUpdateProfile: (updatedUser: User): void => { - user.value = updatedUser; - userProfile.value = { - flattenedProfile: generateFlattenedUserProfile( - updatedUser, - userProfile.value?.schemas ?? [], - ), - profile: updatedUser, - schemas: userProfile.value?.schemas ?? [], - }; + h( + UserProvider, + { + onUpdateProfile: (updatedUser: User): void => { + user.value = updatedUser; + userProfile.value = { + flattenedProfile: generateFlattenedUserProfile( + updatedUser, + userProfile.value?.schemas ?? [], + ), + profile: updatedUser, + schemas: userProfile.value?.schemas ?? [], + }; + }, + profile: userProfile.value, + revalidateProfile: async (): Promise => { + try { + const decodedToken: IdToken = await client.getDecodedIdToken(); + const claims: User = extractUserClaimsFromIdToken(decodedToken); + user.value = claims; + userProfile.value = { + flattenedProfile: claims, + profile: claims, + schemas: [], + }; + } catch { + // silent + } + }, + }, + { + default: (): any => + h( + OrganizationProvider, + { + currentOrganization: currentOrganization.value, + getAllOrganizations: async (): Promise => + client.getAllOrganizations({baseUrl: resolvedBaseUrl.value}), + myOrganizations: myOrganizations.value, + onOrganizationSwitch: switchOrganization, + revalidateMyOrganizations: async (): Promise => { + const baseUrl: string = resolvedBaseUrl.value; + try { + const orgs: Organization[] = await client.getMyOrganizations({baseUrl}); + myOrganizations.value = orgs || []; + return orgs || []; + } catch { + return []; + } + }, }, - profile: userProfile.value, - revalidateProfile: async (): Promise => { - try { - const decodedToken: IdToken = await client.getDecodedIdToken(); - const claims: User = extractUserClaimsFromIdToken(decodedToken); - user.value = claims; - userProfile.value = { - flattenedProfile: claims, - profile: claims, - schemas: [], - }; - } catch { - // silent - } + { + default: (): any => slots['default']?.(), }, - }, - { - default: (): any => - h( - OrganizationProvider, - { - currentOrganization: currentOrganization.value, - getAllOrganizations: async (): Promise => - client.getAllOrganizations({baseUrl: resolvedBaseUrl.value}), - myOrganizations: myOrganizations.value, - onOrganizationSwitch: switchOrganization, - revalidateMyOrganizations: async (): Promise => { - const baseUrl: string = resolvedBaseUrl.value; - try { - const orgs: Organization[] = await client.getMyOrganizations({baseUrl}); - myOrganizations.value = orgs || []; - return orgs || []; - } catch { - return []; - } - }, - }, - { - default: (): any => slots['default']?.(), - }, - ), - }, - ), - }), + ), + }, + ), }), }), }, From 37fced3abd45ae519c4faa6671c825f86b70dd3f Mon Sep 17 00:00:00 2001 From: Brion Date: Sat, 27 Jun 2026 00:17:40 +0530 Subject: [PATCH 2/5] Remove deriveOrganizationHandleFromBaseUrl --- packages/javascript/src/errors/exception.ts | 8 +- ...eriveOrganizationHandleFromBaseUrl.test.ts | 163 ---------- ...transformBrandingPreferenceToTheme.test.ts | 291 ------------------ .../deriveOrganizationHandleFromBaseUrl.ts | 99 ------ .../transformBrandingPreferenceToTheme.ts | 242 --------------- .../nextjs/src/server/actions/signInAction.ts | 10 +- packages/react/src/ThunderIDReactClient.ts | 8 - packages/vue/src/ThunderIDVueClient.ts | 11 +- 8 files changed, 11 insertions(+), 821 deletions(-) delete mode 100644 packages/javascript/src/utils/__tests__/deriveOrganizationHandleFromBaseUrl.test.ts delete mode 100644 packages/javascript/src/utils/__tests__/transformBrandingPreferenceToTheme.test.ts delete mode 100644 packages/javascript/src/utils/deriveOrganizationHandleFromBaseUrl.ts delete mode 100644 packages/javascript/src/utils/transformBrandingPreferenceToTheme.ts diff --git a/packages/javascript/src/errors/exception.ts b/packages/javascript/src/errors/exception.ts index d2af3e1..47bead2 100644 --- a/packages/javascript/src/errors/exception.ts +++ b/packages/javascript/src/errors/exception.ts @@ -19,15 +19,11 @@ /** * @deprecated Use `ThunderIDRuntimeError` for runtime errors and `ThunderIDAPIError` for API errors. */ -export class ThunderIDAuthException { - public name: string; - +export class ThunderIDAuthException extends Error { public code: string | undefined; - public message: string; - public constructor(code: string, name: string, message: string) { - this.message = message; + super(message); this.name = name; this.code = code; Object.setPrototypeOf(this, new.target.prototype); diff --git a/packages/javascript/src/utils/__tests__/deriveOrganizationHandleFromBaseUrl.test.ts b/packages/javascript/src/utils/__tests__/deriveOrganizationHandleFromBaseUrl.test.ts deleted file mode 100644 index ec57a23..0000000 --- a/packages/javascript/src/utils/__tests__/deriveOrganizationHandleFromBaseUrl.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import ThunderIDRuntimeError from '../../errors/ThunderIDRuntimeError'; -import deriveOrganizationHandleFromBaseUrl from '../deriveOrganizationHandleFromBaseUrl'; - -describe('deriveOrganizationHandleFromBaseUrl', () => { - describe('Valid ThunderID URLs', () => { - it('should extract organization handle from URL with /t/{org} pattern', () => { - const result: string = deriveOrganizationHandleFromBaseUrl('https://localhost:8090/t/dxlab'); - expect(result).toBe('dxlab'); - }); - - it('should extract organization handle with trailing slash', () => { - const result: string = deriveOrganizationHandleFromBaseUrl('https://localhost:8090/t/dxlab/'); - expect(result).toBe('dxlab'); - }); - - it('should extract organization handle with additional path segments', () => { - const result: string = deriveOrganizationHandleFromBaseUrl('https://localhost:8090/t/dxlab/api/v1'); - expect(result).toBe('dxlab'); - }); - - it('should handle different organization handles', () => { - expect(deriveOrganizationHandleFromBaseUrl('https://localhost:8090/t/myorg')).toBe('myorg'); - expect(deriveOrganizationHandleFromBaseUrl('https://localhost:8090/t/test-org')).toBe('test-org'); - expect(deriveOrganizationHandleFromBaseUrl('https://localhost:8090/t/org123')).toBe('org123'); - }); - }); - - describe('Invalid URLs - Custom Domains', () => { - let warnSpy: ReturnType; - - beforeEach(() => { - warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - }); - - afterEach(() => { - warnSpy.mockRestore(); - }); - - it('should return empty string and warn for URLs without /t/{org} pattern', () => { - const result: string = deriveOrganizationHandleFromBaseUrl('https://custom.example.com/auth'); - - expect(result).toBe(''); - expect(warnSpy).toHaveBeenCalled(); - expect(warnSpy.mock.calls[0][0]).toContain( - 'Organization handle is required since a custom domain is configured.', - ); - warnSpy.mockRestore(); - }); - - it('should return empty string and warn for URLs without /t/ pattern', () => { - const result: string = deriveOrganizationHandleFromBaseUrl('https://localhost:8090/oauth2/token'); - - expect(result).toBe(''); - expect(warnSpy).toHaveBeenCalled(); - expect(warnSpy.mock.calls[0][0]).toContain( - 'Organization handle is required since a custom domain is configured.', - ); - }); - - it('should return empty string and warn for URLs with malformed /t/ pattern', () => { - const result1: string = deriveOrganizationHandleFromBaseUrl('https://localhost:8090/t/'); - const result2: string = deriveOrganizationHandleFromBaseUrl('https://localhost:8090/t'); - - expect(result1).toBe(''); - expect(result2).toBe(''); - expect(warnSpy).toHaveBeenCalled(); - }); - - it('should return empty string and warn for URLs with empty organization handle', () => { - const result: string = deriveOrganizationHandleFromBaseUrl('https://localhost:8090/t//'); - - expect(result).toBe(''); - expect(warnSpy).toHaveBeenCalled(); - }); - }); - - describe('Invalid Input', () => { - it('should throw error for undefined baseUrl', () => { - expect(() => { - deriveOrganizationHandleFromBaseUrl(undefined); - }).toThrow(ThunderIDRuntimeError); - - expect(() => { - deriveOrganizationHandleFromBaseUrl(undefined); - }).toThrow('Base URL is required to derive organization handle.'); - }); - - it('should throw error for empty baseUrl', () => { - expect(() => { - deriveOrganizationHandleFromBaseUrl(''); - }).toThrow(ThunderIDRuntimeError); - - expect(() => { - deriveOrganizationHandleFromBaseUrl(''); - }).toThrow('Base URL is required to derive organization handle.'); - }); - - it('should throw error for invalid URL format', () => { - expect(() => { - deriveOrganizationHandleFromBaseUrl('not-a-valid-url'); - }).toThrow(ThunderIDRuntimeError); - - expect(() => { - deriveOrganizationHandleFromBaseUrl('not-a-valid-url'); - }).toThrow('Invalid base URL format'); - }); - }); - - describe('Error Details', () => { - it('should surface correct error codes for missing/invalid baseUrl and warn for custom domains', () => { - // 1) Missing baseUrl -> throws with *-ValidationError-001 - expect(() => { - deriveOrganizationHandleFromBaseUrl(undefined as any); - }).toThrow( - expect.objectContaining({ - code: 'javascript-deriveOrganizationHandleFromBaseUrl-ValidationError-001', - origin: '@thunderid/javascript', - }), - ); - - // 2) Invalid baseUrl -> throws with *-ValidationError-002 - expect(() => { - deriveOrganizationHandleFromBaseUrl('invalid-url'); - }).toThrow( - expect.objectContaining({ - code: 'javascript-deriveOrganizationHandleFromBaseUrl-ValidationError-002', - origin: '@thunderid/javascript', - }), - ); - - // 3) Custom domain (no /t/{org}) -> DOES NOT throw; warns and returns '' - const warnSpy: ReturnType = vi.spyOn(console, 'warn').mockImplementation(() => {}); - const res: string = deriveOrganizationHandleFromBaseUrl('https://custom.domain.com/auth'); - - expect(res).toBe(''); - expect(warnSpy).toHaveBeenCalled(); - - const warned = String(warnSpy.mock.calls[0][0]); - expect(warned).toContain('ThunderIDRuntimeError'); - expect(warned).toContain('javascript-deriveOrganizationHandleFromBaseUrl-CustomDomainError-002'); - - warnSpy.mockRestore(); - }); - }); -}); diff --git a/packages/javascript/src/utils/__tests__/transformBrandingPreferenceToTheme.test.ts b/packages/javascript/src/utils/__tests__/transformBrandingPreferenceToTheme.test.ts deleted file mode 100644 index ac94af0..0000000 --- a/packages/javascript/src/utils/__tests__/transformBrandingPreferenceToTheme.test.ts +++ /dev/null @@ -1,291 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest'; -import type {BrandingPreference, ThemeVariant} from '../../models/branding-preference'; - -import createTheme from '../../theme/createTheme'; -import {transformBrandingPreferenceToTheme} from '../transformBrandingPreferenceToTheme'; - -vi.mock('../../theme/createTheme', () => ({ - default: vi.fn((config: any, isDark: boolean) => ({__config: config, __isDark: isDark})), -})); - -const lightVariant = (overrides?: Partial): ThemeVariant => - ({ - buttons: undefined, - colors: { - background: { - body: {main: '#fbfbfb'}, - surface: {main: '#ffffff'}, - }, - primary: {contrastText: '#fff', main: '#FF7300'}, - secondary: {contrastText: '#000', main: '#E0E1E2'}, - text: {primary: '#000000de', secondary: '#00000066'}, - }, - images: undefined, - inputs: undefined, - ...(overrides || {}), - }) as any; - -const darkVariant = (overrides?: Partial): ThemeVariant => - ({ - colors: { - background: { - body: {main: '#17191a'}, - surface: {main: '#242627'}, - }, - primary: {contrastText: '#fff', dark: '#222222', main: '#111111'}, - text: {primary: '#EBEBEF', secondary: '#B9B9C6'}, - }, - ...(overrides || {}), - }) as any; - -const basePref = (pref: Partial): BrandingPreference => ({ - locale: 'en-US', - name: 'dxlab', - preference: pref as any, - type: 'ORG', -}); - -describe('transformBrandingPreferenceToTheme', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - it('should return default light theme when theme config is missing', () => { - const bp: BrandingPreference = basePref({} as any); - - const out: Record = transformBrandingPreferenceToTheme(bp); - - expect(createTheme).toHaveBeenCalledWith({}, false); - - expect(out).toEqual({__config: {}, __isDark: false}); - }); - - it('should use activeTheme from branding preference when forceTheme is not provided', () => { - const bp: BrandingPreference = basePref({ - theme: { - DARK: darkVariant(), - LIGHT: lightVariant(), - activeTheme: 'LIGHT', - }, - }); - - const out: Record = transformBrandingPreferenceToTheme(bp); - - expect((createTheme as any).mock.calls[0][1]).toBe(false); - - const cfg: Record = (out as any).__config; - expect(cfg.colors.primary.main).toBe('#FF7300'); - expect(cfg.colors.secondary.main).toBe('#E0E1E2'); - expect(cfg.colors.background.surface).toBe('#ffffff'); - expect(cfg.colors.background.body.main).toBe('#fbfbfb'); - }); - - it('should respect forceTheme=dark and passes isDark=true', () => { - const bp: BrandingPreference = basePref({ - theme: { - DARK: darkVariant(), - LIGHT: lightVariant(), - activeTheme: 'LIGHT', - }, - }); - - const out: Record = transformBrandingPreferenceToTheme(bp, 'dark'); - - expect((createTheme as any).mock.calls[0][1]).toBe(true); - - const cfg: Record = (out as any).__config; - - expect(cfg.colors.primary.main).toBe('#222222'); - expect(cfg.colors.background.surface).toBe('#242627'); - }); - - it('should fall back to LIGHT config when requested variant missing, but preserves isDark from activeTheme', () => { - const bp: BrandingPreference = basePref({ - theme: { - LIGHT: lightVariant(), - activeTheme: 'DARK', - } as any, - }); - - const out: Record = transformBrandingPreferenceToTheme(bp); - - expect((createTheme as any).mock.calls[0][1]).toBe(true); - - const cfg: Record = (out as any).__config; - - expect(cfg.colors.primary.main).toBe('#FF7300'); - }); - - it('should return default light theme when no variants exist', () => { - const bp: BrandingPreference = basePref({ - theme: {} as any, - }); - - const out: Record = transformBrandingPreferenceToTheme(bp); - - expect(createTheme).toHaveBeenCalledWith({}, false); - - expect(out).toEqual({__config: {}, __isDark: false}); - }); - - it('should map images (logo & favicon) into config correctly', () => { - const bp: BrandingPreference = basePref({ - theme: { - LIGHT: lightVariant({ - images: { - favicon: { - altText: 'App Icon', - imgURL: 'https://example.com/favicon.ico', - title: 'My App Favicon', - }, - logo: { - altText: 'Company Brand Logo', - imgURL: 'https://example.com/logo.png', - title: 'Company Logo', - }, - }, - }), - activeTheme: 'LIGHT', - }, - }); - - const out: Record = transformBrandingPreferenceToTheme(bp); - - const cfg: Record = (out as any).__config; - - expect(cfg.images.favicon).toEqual({ - alt: 'App Icon', - title: 'My App Favicon', - url: 'https://example.com/favicon.ico', - }); - - expect(cfg.images.logo).toEqual({ - alt: 'Company Brand Logo', - title: 'Company Logo', - url: 'https://example.com/logo.png', - }); - }); - - it('should apply component borderRadius overrides for Button and Field when present', () => { - const bp: BrandingPreference = basePref({ - theme: { - LIGHT: lightVariant({ - buttons: { - primary: { - base: { - border: {borderRadius: 12}, - }, - }, - } as any, - inputs: { - base: { - border: {borderRadius: 6}, - }, - } as any, - }), - activeTheme: 'LIGHT', - }, - }); - - const out: Record = transformBrandingPreferenceToTheme(bp); - - const cfg: Record = (out as any).__config; - - expect(cfg.components.Button.styleOverrides.root.borderRadius).toBe(12); - expect(cfg.components.Field.styleOverrides.root.borderRadius).toBe(6); - }); - - it('should omit components section when no button/field borderRadius provided', () => { - const bp: BrandingPreference = basePref({ - theme: { - LIGHT: lightVariant(), - activeTheme: 'LIGHT', - }, - }); - - const out: Record = transformBrandingPreferenceToTheme(bp); - - const cfg: Record = (out as any).__config; - - expect(cfg.components).toBeUndefined(); - }); - - it('should resolve dark color selection correctly for primary when both main and dark are provided', () => { - const bp: BrandingPreference = basePref({ - theme: { - DARK: darkVariant({ - colors: { - background: { - body: {main: '#111'}, - surface: {main: '#222'}, - }, - primary: {contrastText: '#fff', dark: '#010101', main: '#999999'}, - text: {primary: '#eee', secondary: '#aaa'}, - } as any, - }), - activeTheme: 'DARK', - }, - }); - - const out: Record = transformBrandingPreferenceToTheme(bp); - - const cfg: Record = (out as any).__config; - - expect(cfg.colors.primary.main).toBe('#010101'); - expect(cfg.colors.primary.dark).toBe('#010101'); - expect((createTheme as any).mock.calls[0][1]).toBe(true); - }); - - it('should use contrastText if provided on color variants', () => { - const bp: BrandingPreference = basePref({ - theme: { - LIGHT: lightVariant({ - colors: { - alerts: { - error: {contrastText: '#fff', main: '#ff0000'}, - info: {contrastText: '#111', main: '#0000ff'}, - neutral: {contrastText: '#222', main: '#00ff00'}, - warning: {contrastText: '#333', main: '#ffff00'}, - } as any, - primary: {contrastText: '#abcdef', main: '#123456'}, - secondary: {contrastText: '#fefefe', main: '#222222'}, - } as any, - }), - activeTheme: 'LIGHT', - }, - }); - - const out: Record = transformBrandingPreferenceToTheme(bp); - - const cfg: Record = (out as any).__config; - - expect(cfg.colors.primary.contrastText).toBe('#abcdef'); - expect(cfg.colors.secondary.contrastText).toBe('#fefefe'); - expect(cfg.colors.error.contrastText).toBe('#fff'); - expect(cfg.colors.info.contrastText).toBe('#111'); - expect(cfg.colors.success.contrastText).toBe('#222'); - expect(cfg.colors.warning.contrastText).toBe('#333'); - }); -}); diff --git a/packages/javascript/src/utils/deriveOrganizationHandleFromBaseUrl.ts b/packages/javascript/src/utils/deriveOrganizationHandleFromBaseUrl.ts deleted file mode 100644 index 3c630e2..0000000 --- a/packages/javascript/src/utils/deriveOrganizationHandleFromBaseUrl.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import logger from './logger'; -import ThunderIDRuntimeError from '../errors/ThunderIDRuntimeError'; - -/** - * Extracts the organization handle from a ThunderID base URL. - * - * Parses URLs following the `/t/{orgHandle}` pattern. - * - * @param baseUrl - The base URL of the ThunderID identity server - * @returns The extracted organization handle - * @throws {ThunderIDRuntimeError} When the URL doesn't match the expected ThunderID pattern, - * indicating a custom domain is configured and organizationHandle must be provided explicitly - * - * @example - * ```typescript - * const handle = deriveOrganizationHandleFromBaseUrl('https://localhost:8090/t/dxlab'); - * // Returns: 'dxlab' - * - * // Custom domain - returns empty string with a warning - * const handle2 = deriveOrganizationHandleFromBaseUrl('https://custom.example.com/auth'); - * // Returns: '' and logs a warning - * ``` - */ -const deriveOrganizationHandleFromBaseUrl = (baseUrl?: string): string => { - if (!baseUrl) { - throw new ThunderIDRuntimeError( - 'Base URL is required to derive organization handle.', - 'javascript-deriveOrganizationHandleFromBaseUrl-ValidationError-001', - 'javascript', - 'A valid base URL must be provided to extract the organization handle.', - ); - } - - let parsedUrl: URL; - - try { - parsedUrl = new URL(baseUrl); - } catch (error) { - throw new ThunderIDRuntimeError( - `Invalid base URL format: ${baseUrl}`, - 'javascript-deriveOrganizationHandleFromBaseUrl-ValidationError-002', - 'javascript', - 'The provided base URL does not conform to valid URL syntax.', - ); - } - - // Extract the organization handle from the path pattern: /t/{orgHandle} - const pathSegments: string[] = parsedUrl.pathname?.split('/')?.filter((segment: string) => segment?.length > 0); - - if (pathSegments.length < 2 || pathSegments[0] !== 't') { - logger.warn( - new ThunderIDRuntimeError( - 'Organization handle is required since a custom domain is configured.', - 'javascript-deriveOrganizationHandleFromBaseUrl-CustomDomainError-002', - 'javascript', - 'The provided base URL does not follow the expected URL pattern (/t/{orgHandle}). Please provide the organizationHandle explicitly in the configuration.', - ).toString(), - ); - - return ''; - } - - const organizationHandle: string = pathSegments[1]; - - if (!organizationHandle || organizationHandle.trim().length === 0) { - logger.warn( - new ThunderIDRuntimeError( - 'Organization handle is required since a custom domain is configured.', - 'javascript-deriveOrganizationHandleFromBaseUrl-CustomDomainError-003', - 'javascript', - 'The organization handle could not be extracted from the base URL. Please provide the organizationHandle explicitly in the configuration.', - ).toString(), - ); - - return ''; - } - - return organizationHandle; -}; - -export default deriveOrganizationHandleFromBaseUrl; diff --git a/packages/javascript/src/utils/transformBrandingPreferenceToTheme.ts b/packages/javascript/src/utils/transformBrandingPreferenceToTheme.ts deleted file mode 100644 index 85c2115..0000000 --- a/packages/javascript/src/utils/transformBrandingPreferenceToTheme.ts +++ /dev/null @@ -1,242 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {BrandingPreference, BrandingTheme, ThemeVariant} from '../models/branding-preference'; -import createTheme from '../theme/createTheme'; -import {Theme, ThemeConfig} from '../theme/types'; - -/** - * Safely extracts a color value from the branding preference structure - */ -interface ColorVariant { - contrastText?: string; - dark?: string; - main?: string; -} -interface TextColors { - dark?: string; - primary?: string; - secondary?: string; -} - -const extractColorValue = (colorVariant?: ColorVariant, preferDark = false): string | undefined => { - if (preferDark && colorVariant?.dark?.trim()) { - return colorVariant.dark; - } - return colorVariant?.main; -}; - -/** - * Safely extracts contrast text color from the branding preference structure - */ -const extractContrastText = (colorVariant?: {contrastText?: string; main?: string}): string | undefined => - colorVariant?.contrastText; - -/** - * Transforms a ThemeVariant from branding preference to ThemeConfig - */ -const transformThemeVariant = (themeVariant: ThemeVariant, isDark = false): Partial => { - const {buttons} = themeVariant; - const {colors} = themeVariant; - const {images} = themeVariant; - const {inputs} = themeVariant; - - const config: Partial = { - colors: { - action: { - activatedOpacity: 0.12, - active: isDark ? 'rgba(255, 255, 255, 0.70)' : 'rgba(0, 0, 0, 0.54)', - disabled: isDark ? 'rgba(255, 255, 255, 0.26)' : 'rgba(0, 0, 0, 0.26)', - disabledBackground: isDark ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.12)', - disabledOpacity: 0.38, - focus: isDark ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.12)', - focusOpacity: 0.12, - hover: isDark ? 'rgba(255, 255, 255, 0.04)' : 'rgba(0, 0, 0, 0.04)', - hoverOpacity: 0.04, - selected: isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.08)', - selectedOpacity: 0.08, - }, - background: { - body: { - dark: (colors?.background?.body as ColorVariant)?.dark || (colors?.background?.body as ColorVariant)?.main, - main: extractColorValue(colors?.background?.body as ColorVariant, isDark) ?? '', - }, - dark: - (colors?.background?.surface as ColorVariant)?.dark || (colors?.background?.surface as ColorVariant)?.main, - disabled: extractColorValue(colors?.background?.surface as ColorVariant, isDark) ?? '', - surface: extractColorValue(colors?.background?.surface as ColorVariant, isDark) ?? '', - }, - border: colors?.outlined?.default ?? '', - error: { - contrastText: extractContrastText(colors?.alerts?.error) ?? '', - dark: (colors?.alerts?.error as ColorVariant)?.dark || (colors?.alerts?.error as ColorVariant)?.main, - main: extractColorValue(colors?.alerts?.error as ColorVariant, isDark) ?? '', - }, - info: { - contrastText: extractContrastText(colors?.alerts?.info) ?? '', - dark: (colors?.alerts?.info as ColorVariant)?.dark || (colors?.alerts?.info as ColorVariant)?.main, - main: extractColorValue(colors?.alerts?.info as ColorVariant, isDark) ?? '', - }, - primary: { - contrastText: extractContrastText(colors?.primary) ?? '', - dark: colors?.primary?.dark || (colors?.primary as ColorVariant)?.main, - main: extractColorValue(colors?.primary as ColorVariant, isDark) ?? '', - }, - secondary: { - contrastText: extractContrastText(colors?.secondary) ?? '', - dark: colors?.secondary?.dark || (colors?.secondary as ColorVariant)?.main, - main: extractColorValue(colors?.secondary as ColorVariant, isDark) ?? '', - }, - success: { - contrastText: extractContrastText(colors?.alerts?.neutral) ?? '', - dark: (colors?.alerts?.neutral as ColorVariant)?.dark || (colors?.alerts?.neutral as ColorVariant)?.main, - main: extractColorValue(colors?.alerts?.neutral as ColorVariant, isDark) ?? '', - }, - text: { - dark: (colors?.text as TextColors)?.dark || (colors?.text as TextColors)?.primary, - primary: (colors?.text as TextColors)?.primary ?? '', - secondary: (colors?.text as TextColors)?.secondary ?? '', - }, - warning: { - contrastText: extractContrastText(colors?.alerts?.warning) ?? '', - dark: (colors?.alerts?.warning as ColorVariant)?.dark || (colors?.alerts?.warning as ColorVariant)?.main, - main: extractColorValue(colors?.alerts?.warning as ColorVariant, isDark) ?? '', - }, - }, - images: { - favicon: images?.favicon - ? { - alt: images.favicon.altText, - title: images.favicon.title, - url: images.favicon.imgURL, - } - : undefined, - logo: images?.logo - ? { - alt: images.logo.altText, - title: images.logo.title, - url: images.logo.imgURL, - } - : undefined, - }, - }; - - /* |---------------------------------------------------------------| */ - /* | Components | */ - /* |---------------------------------------------------------------| */ - - const buttonBorderRadius: string | undefined = buttons?.primary?.base?.border?.borderRadius; - const fieldBorderRadius: string | undefined = inputs?.base?.border?.borderRadius; - - if (buttonBorderRadius || fieldBorderRadius) { - config.components = { - ...(buttonBorderRadius && { - Button: { - styleOverrides: { - root: { - borderRadius: buttonBorderRadius, - }, - }, - }, - }), - ...(fieldBorderRadius && { - Field: { - styleOverrides: { - root: { - borderRadius: fieldBorderRadius, - }, - }, - }, - }), - }; - } - - return config; -}; - -/** - * Transforms branding preference response to Theme object - * - * @param brandingPreference - The branding preference response from getBrandingPreference - * @param forceTheme - Optional parameter to force a specific theme ('light' or 'dark'), - * if not provided, will use the activeTheme from branding preference - * @returns Theme object that can be used with the theme system - * - * The function extracts the following from branding preference: - * - Colors (primary, secondary, background, text, alerts, etc.) - * - Border radius from buttons and inputs - * - Images (logo and favicon with their URLs, titles, and alt text) - * - Typography settings - * - * @example - * ```typescript - * const brandingPreference = await getBrandingPreference({ baseUrl: "..." }); - * const theme = transformBrandingPreferenceToTheme(brandingPreference); - * - * // Access image URLs via CSS variables - * // Logo: var(--wso2-image-logo-url) - * // Favicon: var(--wso2-image-favicon-url) - * - * // Force light theme regardless of branding preference activeTheme - * const lightTheme = transformBrandingPreferenceToTheme(brandingPreference, 'light'); - * ``` - */ -export const transformBrandingPreferenceToTheme = ( - brandingPreference: BrandingPreference, - forceTheme?: 'light' | 'dark', -): Theme => { - // Extract theme configuration - const themeConfig: BrandingTheme | undefined = brandingPreference?.preference?.theme; - - if (!themeConfig) { - // If no theme config is provided, return default light theme - return createTheme({}, false); - } - - // Determine which theme variant to use - let activeThemeKey: string; - if (forceTheme) { - activeThemeKey = forceTheme.toUpperCase(); - } else { - activeThemeKey = themeConfig.activeTheme || 'LIGHT'; - } - - // Get the theme variant (LIGHT or DARK) - const themeVariant: ThemeVariant | undefined = themeConfig[ - activeThemeKey as keyof typeof themeConfig - ] as ThemeVariant; - - if (!themeVariant) { - // If the specified theme variant doesn't exist, fallback to light theme - const fallbackVariant: ThemeVariant | undefined = themeConfig.LIGHT || themeConfig.DARK; - if (fallbackVariant) { - const transformedConfig: Partial = transformThemeVariant(fallbackVariant, activeThemeKey === 'DARK'); - return createTheme(transformedConfig, activeThemeKey === 'DARK'); - } - // If no theme variants exist, return default theme - return createTheme({}, activeThemeKey === 'DARK'); - } - - // Transform the theme variant to ThemeConfig - const transformedConfig: Partial = transformThemeVariant(themeVariant, activeThemeKey === 'DARK'); - - // Create the theme using the transformed config - return createTheme(transformedConfig, activeThemeKey === 'DARK'); -}; - -export default transformBrandingPreferenceToTheme; diff --git a/packages/nextjs/src/server/actions/signInAction.ts b/packages/nextjs/src/server/actions/signInAction.ts index fe97285..4cc9e50 100644 --- a/packages/nextjs/src/server/actions/signInAction.ts +++ b/packages/nextjs/src/server/actions/signInAction.ts @@ -163,8 +163,14 @@ const signInAction = async ( return {data: response as Record, success: true}; } catch (error) { - logger.error(`[signInAction] Error during sign-in: ${error instanceof Error ? error.message : String(error)}`); - return {error: String(error), success: false}; + const message = + error instanceof Error + ? error.message + : typeof (error as any)?.message === 'string' + ? (error as any).message + : String(error); + logger.error(`[signInAction] Error during sign-in: ${message}`); + return {error: message, success: false}; } }; diff --git a/packages/react/src/ThunderIDReactClient.ts b/packages/react/src/ThunderIDReactClient.ts index 71127d6..179d274 100644 --- a/packages/react/src/ThunderIDReactClient.ts +++ b/packages/react/src/ThunderIDReactClient.ts @@ -26,7 +26,6 @@ import { executeEmbeddedSignInFlow, Organization, IdToken, - deriveOrganizationHandleFromBaseUrl, AllOrganizationsApiResponse, extractUserClaimsFromIdToken, TokenResponse, @@ -68,16 +67,9 @@ class ThunderIDReactClient { - let resolvedOrganizationHandle: string | undefined = config?.organizationHandle; - - if (!resolvedOrganizationHandle) { - resolvedOrganizationHandle = deriveOrganizationHandleFromBaseUrl(config?.baseUrl); - } - return this.withLoading(async () => { this._initializeConfig = { ...config, - organizationHandle: resolvedOrganizationHandle, periodicTokenRefresh: config?.tokenLifecycle?.refreshToken?.autoRefresh ?? (config as any)?.periodicTokenRefresh, } as any; diff --git a/packages/vue/src/ThunderIDVueClient.ts b/packages/vue/src/ThunderIDVueClient.ts index 8617a8c..fbb4696 100644 --- a/packages/vue/src/ThunderIDVueClient.ts +++ b/packages/vue/src/ThunderIDVueClient.ts @@ -39,7 +39,6 @@ import { EmbeddedSignInFlowResponse, EmbeddedSignInFlowStatus, EmbeddedSignUpFlowStatus, - deriveOrganizationHandleFromBaseUrl, StorageManager, } from '@thunderid/browser'; import getAllOrganizations from './api/getAllOrganizations'; @@ -76,15 +75,7 @@ class ThunderIDVueClient exte } override initialize(config: ThunderIDVueConfig): Promise { - let resolvedOrganizationHandle: string | undefined = config?.organizationHandle; - - if (!resolvedOrganizationHandle) { - resolvedOrganizationHandle = deriveOrganizationHandleFromBaseUrl(config?.baseUrl); - } - - return this.withLoading(async () => - super.initialize({...config, organizationHandle: resolvedOrganizationHandle} as unknown as T), - ); + return this.withLoading(async () => super.initialize(config as unknown as T)); } override reInitialize(config: Partial): Promise { From ac1152627c09ea66da0f1dfa1179eda828adac3a Mon Sep 17 00:00:00 2001 From: Brion Date: Sat, 27 Jun 2026 00:23:05 +0530 Subject: [PATCH 3/5] Refactor Next.js SDK: streamline package.json exports, enhance rolldown config, remove deprecated middleware, and introduce new proxy functionalities --- packages/nextjs/package.json | 16 ++----- packages/nextjs/rolldown.config.js | 48 +++++-------------- packages/nextjs/src/ThunderIDNextClient.ts | 18 +++---- packages/nextjs/src/middleware.ts | 37 -------------- packages/nextjs/src/server/index.ts | 6 +++ .../createRouteMatcher.ts | 0 .../thunderIDProxy.ts} | 38 +++++++-------- packages/nextjs/tsconfig.lib.json | 1 - 8 files changed, 48 insertions(+), 116 deletions(-) delete mode 100644 packages/nextjs/src/middleware.ts rename packages/nextjs/src/server/{middleware => proxy}/createRouteMatcher.ts (100%) rename packages/nextjs/src/server/{middleware/thunderIDMiddleware.ts => proxy/thunderIDProxy.ts} (92%) diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 98404f9..6b7eac4 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -15,24 +15,14 @@ "author": "WSO2", "license": "Apache-2.0", "type": "module", - "main": "dist/cjs/index.cjs", - "module": "dist/index.js", "exports": { ".": { "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "require": "./dist/cjs/index.cjs" + "import": "./dist/index.js" }, "./server": { "types": "./dist/server/index.d.ts", - "import": "./dist/server/index.js", - "require": "./dist/cjs/server/index.cjs" - }, - "./middleware": { - "types": "./dist/middleware.d.ts", - "edge-light": "./dist/middleware.js", - "import": "./dist/middleware.js", - "require": "./dist/cjs/middleware.cjs" + "import": "./dist/server/index.js" } }, "files": [ @@ -47,7 +37,7 @@ "directory": "packages/nextjs" }, "scripts": { - "build": "pnpm clean:dist && rolldown -c rolldown.config.js && tsc -p tsconfig.lib.json --emitDeclarationOnly --outDir dist", + "build": "pnpm clean:dist && rolldown -c rolldown.config.js && tsc -p tsconfig.lib.json --emitDeclarationOnly --outDir dist", "clean": "pnpm clean:node_modules && pnpm clean:dist", "clean:dist": "rimraf dist", "clean:node_modules": "rimraf node_modules", diff --git a/packages/nextjs/rolldown.config.js b/packages/nextjs/rolldown.config.js index da2abba..eb865e0 100644 --- a/packages/nextjs/rolldown.config.js +++ b/packages/nextjs/rolldown.config.js @@ -22,62 +22,36 @@ import {defineConfig} from 'rolldown'; const pkg = JSON.parse(readFileSync('./package.json', 'utf8')); -const external = [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})]; +const externalPackages = [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})]; +const external = id => externalPackages.some(name => id === name || id.startsWith(`${name}/`)); -const nodeOptions = { +const commonOptions = { input: [join('src', 'index.ts'), join('src', 'server', 'index.ts')], - preserveModules: true, external, platform: 'node', - target: 'es2020', - sourcemap: true, -}; - -const edgeOptions = { - input: [join('src', 'middleware.ts')], - preserveModules: true, - external, - platform: 'browser', - target: 'es2020', - sourcemap: true, }; export default defineConfig([ - // ESM build (node) - { - ...nodeOptions, - output: { - dir: 'dist', - format: 'esm', - preserveModulesRoot: 'src', - }, - }, - // CommonJS build (node) - { - ...nodeOptions, - output: { - dir: join('dist', 'cjs'), - entryFileNames: '[name].cjs', - format: 'cjs', - preserveModulesRoot: 'src', - }, - }, - // Edge/middleware ESM build (browser) + // ESM build { - ...edgeOptions, + ...commonOptions, output: { dir: 'dist', format: 'esm', + sourcemap: true, + preserveModules: true, preserveModulesRoot: 'src', }, }, - // Edge/middleware CommonJS build (browser) + // CommonJS build { - ...edgeOptions, + ...commonOptions, output: { dir: join('dist', 'cjs'), entryFileNames: '[name].cjs', format: 'cjs', + sourcemap: true, + preserveModules: true, preserveModulesRoot: 'src', }, }, diff --git a/packages/nextjs/src/ThunderIDNextClient.ts b/packages/nextjs/src/ThunderIDNextClient.ts index f84d055..494a624 100644 --- a/packages/nextjs/src/ThunderIDNextClient.ts +++ b/packages/nextjs/src/ThunderIDNextClient.ts @@ -36,7 +36,6 @@ import { User, UserProfile, createOrganization, - deriveOrganizationHandleFromBaseUrl, extractUserClaimsFromIdToken, flattenUserSchema, generateFlattenedUserProfile, @@ -61,6 +60,13 @@ class ThunderIDNextClient e } private async ensureInitialized(): Promise { + if (!this.isInitialized) { + // Server actions may run in a module context that hasn't been through + // ThunderIDServerProvider (e.g. a different worker). Try to initialize + // from environment variables before giving up. + await this.initialize({} as T); + } + if (!this.isInitialized) { throw new Error( '[ThunderIDNextClient] Client is not initialized. Make sure you have wrapped your app with ThunderIDProvider and provided the required configuration (baseUrl, clientId, etc.).', @@ -85,12 +91,6 @@ class ThunderIDNextClient e ...rest } = decorateConfigWithNextEnv(config); - let resolvedOrganizationHandle: string | undefined = organizationHandle; - - if (!resolvedOrganizationHandle) { - resolvedOrganizationHandle = deriveOrganizationHandleFromBaseUrl(baseUrl); - } - const origin: string = await getClientOrigin(); const initialized: boolean = await super.initialize( @@ -101,8 +101,8 @@ class ThunderIDNextClient e baseUrl, clientId, clientSecret, - enablePKCE: clientSecret == null, - organizationHandle: resolvedOrganizationHandle, + enablePKCE: (rest as any).enablePKCE ?? true, + organizationHandle, signInUrl, signUpUrl, } as any, diff --git a/packages/nextjs/src/middleware.ts b/packages/nextjs/src/middleware.ts deleted file mode 100644 index 6d228a7..0000000 --- a/packages/nextjs/src/middleware.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * Edge Runtime entry point — safe for use in Next.js middleware.ts. - * - * This file must only import modules whose full transitive dependency graph - * contains zero Node.js-only APIs (process.versions, fs, crypto, etc.). - * Permitted dependencies: jose, fetch, next/server, and local utilities - * that themselves satisfy the same constraint. - * - * Do NOT import from: - * - ThunderIDNextClient (depends on @thunderid/node → @thunderid/javascript) - * - server/ThunderIDProvider (depends on @thunderid/node) - * - server/actions/* (depend on @thunderid/node) - * - client/* (depend on @thunderid/javascript via @thunderid/react) - */ - -export {default as thunderIDMiddleware} from './server/middleware/thunderIDMiddleware'; -export * from './server/middleware/thunderIDMiddleware'; - -export {default as createRouteMatcher} from './server/middleware/createRouteMatcher'; diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 287ae23..8fb8585 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -22,3 +22,9 @@ export {default as thunderid} from './thunderid'; export {default as ThunderIDProvider} from './ThunderIDProvider.js'; export * from './ThunderIDProvider.js'; + +export {default as thunderIDProxy} from './proxy/thunderIDProxy'; +export * from './proxy/thunderIDProxy'; + +export {default as createRouteMatcher} from './proxy/createRouteMatcher'; + diff --git a/packages/nextjs/src/server/middleware/createRouteMatcher.ts b/packages/nextjs/src/server/proxy/createRouteMatcher.ts similarity index 100% rename from packages/nextjs/src/server/middleware/createRouteMatcher.ts rename to packages/nextjs/src/server/proxy/createRouteMatcher.ts diff --git a/packages/nextjs/src/server/middleware/thunderIDMiddleware.ts b/packages/nextjs/src/server/proxy/thunderIDProxy.ts similarity index 92% rename from packages/nextjs/src/server/middleware/thunderIDMiddleware.ts rename to packages/nextjs/src/server/proxy/thunderIDProxy.ts index 0dc3473..aad40f5 100644 --- a/packages/nextjs/src/server/middleware/thunderIDMiddleware.ts +++ b/packages/nextjs/src/server/proxy/thunderIDProxy.ts @@ -24,9 +24,9 @@ import handleRefreshToken from '../../utils/handleRefreshToken'; import SessionManager, {SessionTokenPayload} from '../../utils/SessionManager'; import {getSessionFromRequest, getSessionIdFromRequest} from '../../utils/sessionUtils'; -export type ThunderIDMiddlewareOptions = Partial; +export type ThunderIDProxyOptions = Partial; -export interface ThunderIDMiddlewareContext { +export interface ThunderIDProxyContext { /** Get the session payload from JWT session if available */ getSession: () => Promise; /** Get the session ID from the current request */ @@ -45,8 +45,8 @@ export interface ThunderIDMiddlewareContext { protectRoute: (routeOptions?: {redirect?: string}) => Promise; } -type ThunderIDMiddlewareHandler = ( - thunderid: ThunderIDMiddlewareContext, +type ThunderIDProxyHandler = ( + thunderid: ThunderIDProxyContext, req: NextRequest, ) => Promise | NextResponse | void; @@ -93,7 +93,7 @@ const replaceCookieInHeader = (cookieHeader: string, name: string, value: string }; /** - * ThunderID middleware that integrates authentication into your Next.js application. + * ThunderID proxy that integrates authentication into your Next.js application. * Similar to Clerk's clerkMiddleware pattern. * * Proactively refreshes the access token when it is within REFRESH_BUFFER_SECONDS of @@ -110,16 +110,16 @@ const replaceCookieInHeader = (cookieHeader: string, name: string, value: string * (NEXT_PUBLIC_THUNDERID_BASE_URL, NEXT_PUBLIC_THUNDERID_CLIENT_ID, * THUNDERID_CLIENT_SECRET). If none are available the refresh step is skipped silently. * - * @param handler - Optional handler function to customize middleware behavior - * @param options - Configuration options for the middleware + * @param handler - Optional handler function to customize proxy behavior + * @param options - Configuration options for the proxy * @returns Next.js middleware function * * @example * ```typescript * // middleware.ts - Basic usage (config read from env vars automatically) - * import { thunderIDMiddleware } from '@thunderid/nextjs'; + * import { thunderIDProxy } from '@thunderid/nextjs/server'; * - * export default thunderIDMiddleware(); + * export default thunderIDProxy(); * * export const config = { * matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'], @@ -129,24 +129,24 @@ const replaceCookieInHeader = (cookieHeader: string, name: string, value: string * @example * ```typescript * // With route protection - * import { thunderIDMiddleware, createRouteMatcher } from '@thunderid/nextjs'; + * import { thunderIDProxy, createRouteMatcher } from '@thunderid/nextjs/server'; * * const isProtectedRoute = createRouteMatcher(['/dashboard(.*)']); * - * export default thunderIDMiddleware(async (thunderid, req) => { + * export default thunderIDProxy(async (thunderid, req) => { * if (isProtectedRoute(req)) { * await thunderid.protectRoute(); * } * }); * ``` */ -const thunderIDMiddleware = +const thunderIDProxy = ( - handler?: ThunderIDMiddlewareHandler, - options?: ThunderIDMiddlewareOptions | ((req: NextRequest) => ThunderIDMiddlewareOptions), + handler?: ThunderIDProxyHandler, + options?: ThunderIDProxyOptions | ((req: NextRequest) => ThunderIDProxyOptions), ): ((request: NextRequest) => Promise) => async (request: NextRequest): Promise => { - const resolvedOptions: ThunderIDMiddlewareOptions = + const resolvedOptions: ThunderIDProxyOptions = typeof options === 'function' ? options(request) : options || {}; // Resolve full config from passed options + environment variable fallbacks. @@ -176,7 +176,7 @@ const thunderIDMiddleware = const verifiedSession: SessionTokenPayload | undefined = await getSessionFromRequest(request); // Step 2: If no verified session exists, verify the raw cookie's signature - // without enforcing expiry. This allows the middleware to recover from an + // without enforcing expiry. This allows the proxy to recover from an // expired access token as long as the JWT is authentic and a refresh token // is present. Skipping the signature check here would let a tampered cookie // drive identity-confusion attacks since handleRefreshToken reuses `sub`, @@ -249,8 +249,8 @@ const thunderIDMiddleware = const sessionId: string | undefined = activeSession?.sessionId ?? (await getSessionIdFromRequest(request)); const isAuthenticated = !!activeSession; - // ── Middleware context ──────────────────────────────────────────────────── - const thunderid: ThunderIDMiddlewareContext = { + // ── Proxy context ───────────────────────────────────────────────────────── + const thunderid: ThunderIDProxyContext = { getSession: async (): Promise => activeSession, getSessionId: (): string | undefined => sessionId, isSignedIn: (): boolean => isAuthenticated, @@ -343,4 +343,4 @@ const thunderIDMiddleware = return response; }; -export default thunderIDMiddleware; +export default thunderIDProxy; diff --git a/packages/nextjs/tsconfig.lib.json b/packages/nextjs/tsconfig.lib.json index 0e89616..30cb881 100644 --- a/packages/nextjs/tsconfig.lib.json +++ b/packages/nextjs/tsconfig.lib.json @@ -4,7 +4,6 @@ "declaration": true, "declarationMap": true, "outDir": "dist", - "declarationDir": "dist/types", "types": ["node"] }, "exclude": [ From fe15a523c46369b2267d7d45efd30e0e91de8128 Mon Sep 17 00:00:00 2001 From: Brion Date: Sat, 27 Jun 2026 01:40:08 +0530 Subject: [PATCH 4/5] Clean up B2B components --- .../src/ThunderIDJavaScriptClient.ts | 17 - .../api/__tests__/createOrganization.test.ts | 295 ---------- .../api/__tests__/getAllOrganizations.test.ts | 222 ------- .../api/__tests__/getMeOrganizations.test.ts | 222 ------- .../src/api/__tests__/getOrganization.test.ts | 204 ------- .../api/__tests__/updateOrganization.test.ts | 341 ----------- .../javascript/src/api/createOrganization.ts | 214 ------- .../javascript/src/api/getAllOrganizations.ts | 189 ------ .../javascript/src/api/getMeOrganizations.ts | 205 ------- .../javascript/src/api/getOrganization.ts | 187 ------ .../src/api/getOrganizationUnitChildren.ts | 157 +++-- .../javascript/src/api/updateOrganization.ts | 225 -------- packages/javascript/src/index.ts | 22 +- packages/javascript/src/models/client.ts | 29 - packages/javascript/src/models/config.ts | 8 - .../src/models/organization-unit.ts | 112 ---- .../javascript/src/models/organization.ts | 35 -- packages/nextjs/src/ThunderIDNextClient.ts | 143 ----- .../CreateOrganization/CreateOrganization.tsx | 159 ----- .../Organization/Organization.tsx | 83 --- .../OrganizationList/OrganizationList.tsx | 136 ----- .../OrganizationProfile.tsx | 221 ------- .../OrganizationSwitcher.tsx | 199 ------- .../contexts/ThunderID/ThunderIDProvider.tsx | 28 +- packages/nextjs/src/client/index.ts | 16 +- .../nextjs/src/server/ThunderIDProvider.tsx | 41 +- .../__tests__/createOrganization.test.ts | 128 ----- .../__tests__/getAllOrganizations.test.ts | 120 ---- .../getCurrentOrganizationAction.test.ts | 112 ---- .../__tests__/getMyOrganizations.test.ts | 211 ------- .../__tests__/getOrganizationAction.test.ts | 126 ---- .../src/server/actions/createOrganization.ts | 42 -- .../src/server/actions/getAllOrganizations.ts | 42 -- .../actions/getCurrentOrganizationAction.ts | 49 -- .../src/server/actions/getMyOrganizations.ts | 82 --- .../server/actions/getOrganizationAction.ts | 50 -- .../src/server/actions/switchOrganization.ts | 90 --- packages/nuxt/src/index.ts | 2 +- packages/nuxt/src/module.ts | 53 +- .../src/runtime/components/ThunderIDRoot.ts | 87 +-- .../organization/CreateOrganization.ts | 71 --- .../components/organization/Organization.ts | 58 -- .../organization/OrganizationList.ts | 67 --- .../organization/OrganizationProfile.ts | 74 --- .../organization/OrganizationSwitcher.ts | 64 --- .../nuxt/src/runtime/errors/error-codes.ts | 3 - .../nuxt/src/runtime/plugins/thunderid.ts | 14 +- .../src/runtime/server/ThunderIDNuxtClient.ts | 94 --- .../runtime/server/plugins/thunderid-ssr.ts | 18 +- .../routes/auth/organizations/current.get.ts | 56 -- .../routes/auth/organizations/id.get.ts | 60 -- .../routes/auth/organizations/index.get.ts | 55 -- .../routes/auth/organizations/index.post.ts | 64 --- .../routes/auth/organizations/me.get.ts | 55 -- .../routes/auth/organizations/switch.post.ts | 91 --- packages/nuxt/src/runtime/types.ts | 7 - packages/react/src/ThunderIDReactClient.ts | 107 ---- packages/react/src/api/createOrganization.ts | 123 ---- packages/react/src/api/getAllOrganizations.ts | 114 ---- packages/react/src/api/getMeOrganizations.ts | 118 ---- packages/react/src/api/getOrganization.ts | 110 ---- packages/react/src/api/getSchemas.ts | 104 ---- packages/react/src/api/updateOrganization.ts | 121 ---- .../OrganizationContext.tsx | 84 --- .../OrganizationContextController.tsx | 106 ---- .../BaseCreateOrganization.styles.ts | 180 ------ .../BaseCreateOrganization.tsx | 279 --------- .../CreateOrganization/CreateOrganization.tsx | 147 ----- .../Organization/BaseOrganization.tsx | 84 --- .../Organization/Organization.tsx | 82 --- .../BaseOrganizationList.styles.ts | 281 --------- .../OrganizationList/BaseOrganizationList.tsx | 432 -------------- .../OrganizationList.styles.ts | 89 --- .../OrganizationList/OrganizationList.tsx | 143 ----- .../BaseOrganizationProfile.styles.ts | 182 ------ .../BaseOrganizationProfile.tsx | 541 ------------------ .../OrganizationProfile.tsx | 216 ------- .../BaseOrganizationSwitcher.styles.ts | 275 --------- .../BaseOrganizationSwitcher.test.tsx | 279 --------- .../BaseOrganizationSwitcher.tsx | 522 ----------------- .../OrganizationSwitcher.tsx | 201 ------- .../UserAvatar/BaseUserAvatar.tsx | 90 +++ .../presentation/UserAvatar/UserAvatar.tsx | 55 ++ .../UserDropdown/BaseUserDropdown.tsx | 14 +- .../presentation/auth/AuthOptionFactory.tsx | 30 +- .../OrganizationUnitPicker.styles.ts | 153 ----- .../OrganizationUnitPicker.tsx | 245 -------- .../auth/OrganizationUnitPicker/index.ts | 20 - .../Organization/OrganizationContext.ts | 59 -- .../Organization/OrganizationProvider.tsx | 178 ------ .../contexts/Organization/useOrganization.ts | 116 ---- .../contexts/ThunderID/ThunderIDContext.ts | 7 +- .../contexts/ThunderID/ThunderIDProvider.tsx | 49 +- .../ThunderID/__tests__/useThunderID.test.tsx | 3 - packages/react/src/index.ts | 53 -- packages/vue/src/ThunderIDVueClient.ts | 107 ---- .../composables/useOrganization.test.ts | 100 ---- packages/vue/src/api/getAllOrganizations.ts | 62 -- packages/vue/src/api/getMeOrganizations.ts | 62 -- .../BaseCreateOrganization.ts | 118 ---- .../CreateOrganization.css.ts | 67 --- .../create-organization/CreateOrganization.ts | 64 --- .../organization-list/BaseOrganizationList.ts | 82 --- .../organization-list/OrganizationList.css.ts | 89 --- .../organization-list/OrganizationList.ts | 61 -- .../BaseOrganizationProfile.ts | 412 ------------- .../OrganizationProfile.css.ts | 219 ------- .../OrganizationProfile.ts | 67 --- .../BaseOrganizationSwitcher.ts | 131 ----- .../OrganizationSwitcher.css.ts | 154 ----- .../OrganizationSwitcher.ts | 53 -- .../presentation/organization/Organization.ts | 57 -- .../vue/src/composables/useOrganization.ts | 57 -- packages/vue/src/index.ts | 15 - packages/vue/src/keys.ts | 6 - packages/vue/src/models/contexts.ts | 35 -- .../vue/src/providers/OrganizationProvider.ts | 148 ----- .../vue/src/providers/ThunderIDProvider.ts | 57 +- packages/vue/src/styles/injectStyles.ts | 8 - 119 files changed, 294 insertions(+), 13684 deletions(-) delete mode 100644 packages/javascript/src/api/__tests__/createOrganization.test.ts delete mode 100644 packages/javascript/src/api/__tests__/getAllOrganizations.test.ts delete mode 100644 packages/javascript/src/api/__tests__/getMeOrganizations.test.ts delete mode 100644 packages/javascript/src/api/__tests__/getOrganization.test.ts delete mode 100644 packages/javascript/src/api/__tests__/updateOrganization.test.ts delete mode 100644 packages/javascript/src/api/createOrganization.ts delete mode 100644 packages/javascript/src/api/getAllOrganizations.ts delete mode 100644 packages/javascript/src/api/getMeOrganizations.ts delete mode 100644 packages/javascript/src/api/getOrganization.ts delete mode 100644 packages/javascript/src/api/updateOrganization.ts delete mode 100644 packages/javascript/src/models/organization-unit.ts delete mode 100644 packages/javascript/src/models/organization.ts delete mode 100644 packages/nextjs/src/client/components/presentation/CreateOrganization/CreateOrganization.tsx delete mode 100644 packages/nextjs/src/client/components/presentation/Organization/Organization.tsx delete mode 100644 packages/nextjs/src/client/components/presentation/OrganizationList/OrganizationList.tsx delete mode 100644 packages/nextjs/src/client/components/presentation/OrganizationProfile/OrganizationProfile.tsx delete mode 100644 packages/nextjs/src/client/components/presentation/OrganizationSwitcher/OrganizationSwitcher.tsx delete mode 100644 packages/nextjs/src/server/actions/__tests__/createOrganization.test.ts delete mode 100644 packages/nextjs/src/server/actions/__tests__/getAllOrganizations.test.ts delete mode 100644 packages/nextjs/src/server/actions/__tests__/getCurrentOrganizationAction.test.ts delete mode 100644 packages/nextjs/src/server/actions/__tests__/getMyOrganizations.test.ts delete mode 100644 packages/nextjs/src/server/actions/__tests__/getOrganizationAction.test.ts delete mode 100644 packages/nextjs/src/server/actions/createOrganization.ts delete mode 100644 packages/nextjs/src/server/actions/getAllOrganizations.ts delete mode 100644 packages/nextjs/src/server/actions/getCurrentOrganizationAction.ts delete mode 100644 packages/nextjs/src/server/actions/getMyOrganizations.ts delete mode 100644 packages/nextjs/src/server/actions/getOrganizationAction.ts delete mode 100644 packages/nextjs/src/server/actions/switchOrganization.ts delete mode 100644 packages/nuxt/src/runtime/components/organization/CreateOrganization.ts delete mode 100644 packages/nuxt/src/runtime/components/organization/Organization.ts delete mode 100644 packages/nuxt/src/runtime/components/organization/OrganizationList.ts delete mode 100644 packages/nuxt/src/runtime/components/organization/OrganizationProfile.ts delete mode 100644 packages/nuxt/src/runtime/components/organization/OrganizationSwitcher.ts delete mode 100644 packages/nuxt/src/runtime/server/routes/auth/organizations/current.get.ts delete mode 100644 packages/nuxt/src/runtime/server/routes/auth/organizations/id.get.ts delete mode 100644 packages/nuxt/src/runtime/server/routes/auth/organizations/index.get.ts delete mode 100644 packages/nuxt/src/runtime/server/routes/auth/organizations/index.post.ts delete mode 100644 packages/nuxt/src/runtime/server/routes/auth/organizations/me.get.ts delete mode 100644 packages/nuxt/src/runtime/server/routes/auth/organizations/switch.post.ts delete mode 100644 packages/react/src/api/createOrganization.ts delete mode 100644 packages/react/src/api/getAllOrganizations.ts delete mode 100644 packages/react/src/api/getMeOrganizations.ts delete mode 100644 packages/react/src/api/getOrganization.ts delete mode 100644 packages/react/src/api/getSchemas.ts delete mode 100644 packages/react/src/api/updateOrganization.ts delete mode 100644 packages/react/src/components/control/OrganizationContext/OrganizationContext.tsx delete mode 100644 packages/react/src/components/control/OrganizationContext/OrganizationContextController.tsx delete mode 100644 packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.styles.ts delete mode 100644 packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.tsx delete mode 100644 packages/react/src/components/presentation/CreateOrganization/CreateOrganization.tsx delete mode 100644 packages/react/src/components/presentation/Organization/BaseOrganization.tsx delete mode 100644 packages/react/src/components/presentation/Organization/Organization.tsx delete mode 100644 packages/react/src/components/presentation/OrganizationList/BaseOrganizationList.styles.ts delete mode 100644 packages/react/src/components/presentation/OrganizationList/BaseOrganizationList.tsx delete mode 100644 packages/react/src/components/presentation/OrganizationList/OrganizationList.styles.ts delete mode 100644 packages/react/src/components/presentation/OrganizationList/OrganizationList.tsx delete mode 100644 packages/react/src/components/presentation/OrganizationProfile/BaseOrganizationProfile.styles.ts delete mode 100644 packages/react/src/components/presentation/OrganizationProfile/BaseOrganizationProfile.tsx delete mode 100644 packages/react/src/components/presentation/OrganizationProfile/OrganizationProfile.tsx delete mode 100644 packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.styles.ts delete mode 100644 packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.test.tsx delete mode 100644 packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.tsx delete mode 100644 packages/react/src/components/presentation/OrganizationSwitcher/OrganizationSwitcher.tsx create mode 100644 packages/react/src/components/presentation/UserAvatar/BaseUserAvatar.tsx create mode 100644 packages/react/src/components/presentation/UserAvatar/UserAvatar.tsx delete mode 100644 packages/react/src/components/presentation/auth/OrganizationUnitPicker/OrganizationUnitPicker.styles.ts delete mode 100644 packages/react/src/components/presentation/auth/OrganizationUnitPicker/OrganizationUnitPicker.tsx delete mode 100644 packages/react/src/components/presentation/auth/OrganizationUnitPicker/index.ts delete mode 100644 packages/react/src/contexts/Organization/OrganizationContext.ts delete mode 100644 packages/react/src/contexts/Organization/OrganizationProvider.tsx delete mode 100644 packages/react/src/contexts/Organization/useOrganization.ts delete mode 100644 packages/vue/src/__tests__/composables/useOrganization.test.ts delete mode 100644 packages/vue/src/api/getAllOrganizations.ts delete mode 100644 packages/vue/src/api/getMeOrganizations.ts delete mode 100644 packages/vue/src/components/presentation/create-organization/BaseCreateOrganization.ts delete mode 100644 packages/vue/src/components/presentation/create-organization/CreateOrganization.css.ts delete mode 100644 packages/vue/src/components/presentation/create-organization/CreateOrganization.ts delete mode 100644 packages/vue/src/components/presentation/organization-list/BaseOrganizationList.ts delete mode 100644 packages/vue/src/components/presentation/organization-list/OrganizationList.css.ts delete mode 100644 packages/vue/src/components/presentation/organization-list/OrganizationList.ts delete mode 100644 packages/vue/src/components/presentation/organization-profile/BaseOrganizationProfile.ts delete mode 100644 packages/vue/src/components/presentation/organization-profile/OrganizationProfile.css.ts delete mode 100644 packages/vue/src/components/presentation/organization-profile/OrganizationProfile.ts delete mode 100644 packages/vue/src/components/presentation/organization-switcher/BaseOrganizationSwitcher.ts delete mode 100644 packages/vue/src/components/presentation/organization-switcher/OrganizationSwitcher.css.ts delete mode 100644 packages/vue/src/components/presentation/organization-switcher/OrganizationSwitcher.ts delete mode 100644 packages/vue/src/components/presentation/organization/Organization.ts delete mode 100644 packages/vue/src/composables/useOrganization.ts delete mode 100644 packages/vue/src/providers/OrganizationProvider.ts diff --git a/packages/javascript/src/ThunderIDJavaScriptClient.ts b/packages/javascript/src/ThunderIDJavaScriptClient.ts index 415bafa..b4358dd 100644 --- a/packages/javascript/src/ThunderIDJavaScriptClient.ts +++ b/packages/javascript/src/ThunderIDJavaScriptClient.ts @@ -33,7 +33,6 @@ import {Crypto} from './models/crypto'; import {ExtendedAuthorizeRequestUrlParams} from './models/oauth-request'; import {OIDCDiscoveryApiResponse} from './models/oidc-discovery'; import {OIDCEndpoints} from './models/oidc-endpoints'; -import {AllOrganizationsApiResponse, Organization} from './models/organization'; import {SessionData, UserSession} from './models/session'; import {Storage, TemporaryStore} from './models/store'; import {IdToken, TokenExchangeRequestConfig, TokenResponse} from './models/token'; @@ -312,22 +311,6 @@ class ThunderIDJavaScriptClient implements ThunderIDClient { throw new Error('Method not implemented.'); } - public switchOrganization(_organization: Organization, _sessionId?: string): Promise { - throw new Error('Method not implemented.'); - } - - public getCurrentOrganization(_sessionId?: string): Promise { - throw new Error('Method not implemented.'); - } - - public getAllOrganizations(_options?: any, _sessionId?: string): Promise { - throw new Error('Method not implemented.'); - } - - public getMyOrganizations(_options?: any, _sessionId?: string): Promise { - throw new Error('Method not implemented.'); - } - public getUserProfile(_options?: any): Promise { throw new Error('Method not implemented.'); } diff --git a/packages/javascript/src/api/__tests__/createOrganization.test.ts b/packages/javascript/src/api/__tests__/createOrganization.test.ts deleted file mode 100644 index cd1bd38..0000000 --- a/packages/javascript/src/api/__tests__/createOrganization.test.ts +++ /dev/null @@ -1,295 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {describe, it, expect, vi, beforeEach} from 'vitest'; -import ThunderIDAPIError from '../../errors/ThunderIDAPIError'; -import {Organization} from '../../models/organization'; -import createOrganization, {CreateOrganizationPayload} from '../createOrganization'; - -describe('createOrganization', (): void => { - beforeEach((): void => { - vi.resetAllMocks(); - }); - - it('should create organization successfully with default fetch', async (): Promise => { - const mockOrg: Organization = { - id: 'org-001', - name: 'Team Viewer', - orgHandle: 'team-viewer', - }; - - global.fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mockOrg), - ok: true, - }); - - const payload: CreateOrganizationPayload = { - description: 'Screen sharing organization', - name: 'Team Viewer', - orgHandle: 'team-viewer', - parentId: 'parent-123', - type: 'TENANT', - }; - - const baseUrl = 'https://localhost:8090'; - const result: Organization = await createOrganization({baseUrl, payload}); - - expect(fetch).toHaveBeenCalledWith(`${baseUrl}/api/server/v1/organizations`, { - body: JSON.stringify(payload), - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - method: 'POST', - }); - expect(result).toEqual(mockOrg); - }); - - it('should use custom fetcher when provided', async (): Promise => { - const mockOrg: Organization = { - id: 'org-002', - name: 'Demo Org', - orgHandle: 'demo-org', - }; - - const customFetcher: typeof fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mockOrg), - ok: true, - }); - - const payload: CreateOrganizationPayload = { - description: 'Example org', - name: 'Demo Org', - parentId: 'p123', - type: 'TENANT', - }; - - const baseUrl = 'https://localhost:8090'; - const result: Organization = await createOrganization({ - baseUrl, - fetcher: customFetcher, - payload, - }); - - expect(result).toEqual(mockOrg); - expect(customFetcher).toHaveBeenCalledWith( - `${baseUrl}/api/server/v1/organizations`, - expect.objectContaining({ - headers: expect.objectContaining({ - Accept: 'application/json', - 'Content-Type': 'application/json', - }), - method: 'POST', - }), - ); - }); - - it('should handle errors thrown directly by custom fetcher', async (): Promise => { - const customFetcher: typeof fetch = vi.fn().mockImplementation(() => { - throw new Error('Custom fetcher failure'); - }); - - const payload: CreateOrganizationPayload = { - description: 'Error via fetcher', - name: 'Fetcher Org', - parentId: 'p222', - type: 'TENANT', - }; - - const baseUrl = 'https://localhost:8090'; - - await expect(createOrganization({baseUrl, fetcher: customFetcher, payload})).rejects.toThrow( - 'Network or parsing error: Custom fetcher failure', - ); - }); - - it('should throw ThunderIDAPIError for invalid base URL', async (): Promise => { - const payload: CreateOrganizationPayload = { - description: 'Invalid test', - name: 'Broken Org', - parentId: 'p1', - type: 'TENANT', - }; - - await expect(createOrganization({baseUrl: 'invalid-url', payload})).rejects.toThrow(ThunderIDAPIError); - await expect(createOrganization({baseUrl: 'invalid-url', payload})).rejects.toThrow('Invalid base URL provided.'); - }); - - it('should throw ThunderIDAPIError for undefined baseUrl', async (): Promise => { - const payload: CreateOrganizationPayload = { - description: 'No URL test', - name: 'Broken Org', - parentId: 'p1', - type: 'TENANT', - }; - - await expect(createOrganization({baseUrl: undefined, payload})).rejects.toThrow(ThunderIDAPIError); - await expect(createOrganization({baseUrl: undefined, payload})).rejects.toThrow('Invalid base URL provided.'); - }); - - it('should throw ThunderIDAPIError for empty string baseUrl', async (): Promise => { - const payload: CreateOrganizationPayload = { - description: 'Empty URL test', - name: 'Broken Org', - parentId: 'p1', - type: 'TENANT', - }; - await expect(createOrganization({baseUrl: '', payload})).rejects.toThrow(ThunderIDAPIError); - await expect(createOrganization({baseUrl: '', payload})).rejects.toThrow('Invalid base URL provided.'); - }); - - it('should throw ThunderIDAPIError when payload is missing', async (): Promise => { - const baseUrl = 'https://localhost:8090'; - - await expect(createOrganization({baseUrl} as any)).rejects.toThrow(ThunderIDAPIError); - await expect(createOrganization({baseUrl} as any)).rejects.toThrow('Organization payload is required'); - }); - - it("should always set type to 'TENANT' in payload", async (): Promise => { - const mockOrg: Organization = { - id: 'org-002', - name: 'Demo Org', - orgHandle: 'demo-org', - }; - - global.fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mockOrg), - ok: true, - }); - - const payload: CreateOrganizationPayload = { - description: 'Example org', - name: 'Demo Org', - parentId: 'p123', - type: 'GROUP', // Intentionally incorrect to test override - }; - - const baseUrl = 'https://localhost:8090'; - await createOrganization({baseUrl, payload}); - - expect(fetch).toHaveBeenCalledWith(`${baseUrl}/api/server/v1/organizations`, { - body: JSON.stringify({ - ...payload, - type: 'TENANT', - }), - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - method: 'POST', - }); - }); - - it('should handle HTTP error responses', async (): Promise => { - global.fetch = vi.fn().mockResolvedValue({ - ok: false, - status: 400, - statusText: 'Bad Request', - text: () => Promise.resolve('Invalid organization data'), - }); - - const payload: CreateOrganizationPayload = { - description: 'Error test', - name: 'Bad Org', - parentId: 'p99', - type: 'TENANT', - }; - - const baseUrl = 'https://localhost:8090'; - - await expect(createOrganization({baseUrl, payload})).rejects.toThrow(ThunderIDAPIError); - await expect(createOrganization({baseUrl, payload})).rejects.toThrow( - 'Failed to create organization: Invalid organization data', - ); - }); - - it('should handle network or parsing errors', async (): Promise => { - global.fetch = vi.fn().mockRejectedValue(new Error('Network error')); - - const payload: CreateOrganizationPayload = { - description: 'Network issue', - name: 'Fail Org', - parentId: 'p404', - type: 'TENANT', - }; - - const baseUrl = 'https://localhost:8090'; - - await expect(createOrganization({baseUrl, payload})).rejects.toThrow(ThunderIDAPIError); - await expect(createOrganization({baseUrl, payload})).rejects.toThrow('Network or parsing error: Network error'); - }); - - it('should handle non-Error rejections', async (): Promise => { - global.fetch = vi.fn().mockRejectedValue('unexpected failure'); - - const payload: CreateOrganizationPayload = { - description: 'Unknown error org', - name: 'Unknown Org', - parentId: 'p000', - type: 'TENANT', - }; - - const baseUrl = 'https://localhost:8090'; - - await expect(createOrganization({baseUrl, payload})).rejects.toThrow('Network or parsing error: Unknown error'); - }); - - it('should pass through custom headers', async (): Promise => { - const mockOrg: Organization = { - id: 'org-003', - name: 'Header Org', - orgHandle: 'header-org', - }; - - global.fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mockOrg), - ok: true, - }); - - const payload: CreateOrganizationPayload = { - description: 'Header test org', - name: 'Header Org', - parentId: 'p456', - type: 'TENANT', - }; - - const customHeaders: Record = { - Authorization: 'Bearer token', - 'X-Custom-Header': 'custom-value', - }; - - const baseUrl = 'https://localhost:8090'; - - await createOrganization({ - baseUrl, - headers: customHeaders, - payload, - }); - - expect(fetch).toHaveBeenCalledWith(`${baseUrl}/api/server/v1/organizations`, { - body: JSON.stringify(payload), - headers: { - Accept: 'application/json', - Authorization: 'Bearer token', - 'Content-Type': 'application/json', - 'X-Custom-Header': 'custom-value', - }, - method: 'POST', - }); - }); -}); diff --git a/packages/javascript/src/api/__tests__/getAllOrganizations.test.ts b/packages/javascript/src/api/__tests__/getAllOrganizations.test.ts deleted file mode 100644 index 9a52161..0000000 --- a/packages/javascript/src/api/__tests__/getAllOrganizations.test.ts +++ /dev/null @@ -1,222 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {describe, it, expect, vi, beforeEach} from 'vitest'; -import ThunderIDAPIError from '../../errors/ThunderIDAPIError'; -import {AllOrganizationsApiResponse} from '../../models/organization'; -import getAllOrganizations from '../getAllOrganizations'; - -describe('getAllOrganizations', (): void => { - beforeEach((): void => { - vi.resetAllMocks(); - }); - - it('should fetch all organizations successfully with default fetch', async (): Promise => { - const mockResponse: AllOrganizationsApiResponse = { - hasMore: false, - nextCursor: null, - organizations: [ - {id: 'org1', name: 'Org One', orgHandle: 'org-one'}, - {id: 'org2', name: 'Org Two', orgHandle: 'org-two'}, - ], - totalCount: 2, - }; - - global.fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mockResponse), - ok: true, - }); - - const baseUrl = 'https://localhost:8090'; - const result: AllOrganizationsApiResponse = await getAllOrganizations({baseUrl}); - - expect(fetch).toHaveBeenCalledWith(`${baseUrl}/api/server/v1/organizations?limit=10&recursive=false`, { - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - method: 'GET', - }); - expect(result).toEqual(mockResponse); - }); - - it('should append query parameters when provided', async (): Promise => { - const mockResponse: AllOrganizationsApiResponse = { - hasMore: true, - nextCursor: 'abc123', - organizations: [], - totalCount: 5, - }; - - global.fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mockResponse), - ok: true, - }); - - const baseUrl = 'https://localhost:8090'; - await getAllOrganizations({ - baseUrl, - filter: 'type eq TENANT', - limit: 20, - recursive: true, - }); - - expect(fetch).toHaveBeenCalledWith( - `${baseUrl}/api/server/v1/organizations?filter=type+eq+TENANT&limit=20&recursive=true`, - expect.any(Object), - ); - }); - - it('should use custom fetcher when provided', async (): Promise => { - const mockResponse: AllOrganizationsApiResponse = { - hasMore: false, - nextCursor: null, - organizations: [{id: 'org1', name: 'Custom Org', orgHandle: 'custom-org'}], - totalCount: 1, - }; - - const customFetcher: typeof fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mockResponse), - ok: true, - }); - - const baseUrl = 'https://localhost:8090'; - const result: AllOrganizationsApiResponse = await getAllOrganizations({baseUrl, fetcher: customFetcher}); - - expect(result).toEqual(mockResponse); - expect(customFetcher).toHaveBeenCalledWith( - `${baseUrl}/api/server/v1/organizations?limit=10&recursive=false`, - expect.objectContaining({ - method: 'GET', - }), - ); - }); - - it('should handle errors thrown directly by custom fetcher', async (): Promise => { - const customFetcher: typeof fetch = vi.fn().mockImplementation(() => { - throw new Error('Custom fetcher failure'); - }); - - const baseUrl = 'https://localhost:8090'; - - await expect(getAllOrganizations({baseUrl, fetcher: customFetcher})).rejects.toThrow( - 'Network or parsing error: Custom fetcher failure', - ); - }); - - it('should throw ThunderIDAPIError for invalid base URL', async (): Promise => { - await expect(getAllOrganizations({baseUrl: 'invalid-url'})).rejects.toThrow(ThunderIDAPIError); - await expect(getAllOrganizations({baseUrl: 'invalid-url'})).rejects.toThrow('Invalid base URL provided.'); - }); - - it('should throw ThunderIDAPIError for undefined baseUrl', async (): Promise => { - await expect(getAllOrganizations({baseUrl: undefined} as any)).rejects.toThrow(ThunderIDAPIError); - await expect(getAllOrganizations({baseUrl: undefined} as any)).rejects.toThrow('Invalid base URL provided.'); - }); - - it('should throw ThunderIDAPIError for empty string baseUrl', async (): Promise => { - await expect(getAllOrganizations({baseUrl: ''})).rejects.toThrow(ThunderIDAPIError); - await expect(getAllOrganizations({baseUrl: ''})).rejects.toThrow('Invalid base URL provided.'); - }); - - it('should handle HTTP error responses', async (): Promise => { - global.fetch = vi.fn().mockResolvedValue({ - ok: false, - status: 400, - statusText: 'Bad Request', - text: () => Promise.resolve('Invalid query'), - }); - - const baseUrl = 'https://localhost:8090'; - - await expect(getAllOrganizations({baseUrl})).rejects.toThrow(ThunderIDAPIError); - await expect(getAllOrganizations({baseUrl})).rejects.toThrow('Failed to get organizations: Invalid query'); - }); - - it('should handle network or parsing errors', async (): Promise => { - global.fetch = vi.fn().mockRejectedValue(new Error('Network error')); - - const baseUrl = 'https://localhost:8090'; - - await expect(getAllOrganizations({baseUrl})).rejects.toThrow(ThunderIDAPIError); - await expect(getAllOrganizations({baseUrl})).rejects.toThrow('Network or parsing error: Network error'); - }); - - it('should handle non-Error rejections', async (): Promise => { - global.fetch = vi.fn().mockRejectedValue('unexpected failure'); - - const baseUrl = 'https://localhost:8090'; - - await expect(getAllOrganizations({baseUrl})).rejects.toThrow('Network or parsing error: Unknown error'); - }); - - it('should pass through custom headers (and enforces content-type & accept)', async (): Promise => { - const mockResponse: AllOrganizationsApiResponse = { - hasMore: false, - nextCursor: null, - organizations: [], - totalCount: 1, - }; - - global.fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mockResponse), - ok: true, - }); - - const baseUrl = 'https://localhost:8090'; - const customHeaders: Record = { - Accept: 'text/plain', - Authorization: 'Bearer token', - 'Content-Type': 'text/plain', - 'X-Custom-Header': 'custom-value', - }; - - await getAllOrganizations({ - baseUrl, - headers: customHeaders, - }); - - expect(fetch).toHaveBeenCalledWith(`${baseUrl}/api/server/v1/organizations?limit=10&recursive=false`, { - headers: { - Accept: 'application/json', - Authorization: 'Bearer token', - 'Content-Type': 'application/json', - 'X-Custom-Header': 'custom-value', - }, - method: 'GET', - }); - }); - - it('should return an empty organization list if none exist', async (): Promise => { - global.fetch = vi.fn().mockResolvedValue({ - json: () => - Promise.resolve({ - hasMore: false, - nextCursor: null, - totalCount: 0, // missing organizations - }), - ok: true, - }); - - const baseUrl = 'https://localhost:8090'; - const result: AllOrganizationsApiResponse = await getAllOrganizations({baseUrl}); - - expect(result.organizations).toEqual([]); - expect(result.totalCount).toBe(0); - }); -}); diff --git a/packages/javascript/src/api/__tests__/getMeOrganizations.test.ts b/packages/javascript/src/api/__tests__/getMeOrganizations.test.ts deleted file mode 100644 index a169ec4..0000000 --- a/packages/javascript/src/api/__tests__/getMeOrganizations.test.ts +++ /dev/null @@ -1,222 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {Mock, beforeEach, describe, expect, it, vi} from 'vitest'; -import ThunderIDAPIError from '../../errors/ThunderIDAPIError'; -import type {Organization} from '../../models/organization'; -import getMeOrganizations from '../getMeOrganizations'; - -describe('getMeOrganizations', (): void => { - beforeEach((): void => { - vi.resetAllMocks(); - }); - - it('should fetch associated orgs successfully (default fetch)', async (): Promise => { - const mock: {organizations: Organization[]} = { - organizations: [ - {id: 'o1', name: 'One', orgHandle: 'one'}, - {id: 'o2', name: 'Two', orgHandle: 'two'}, - ], - }; - - global.fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mock), - ok: true, - }); - - const baseUrl = 'https://localhost:8090'; - const result: Organization[] = await getMeOrganizations({baseUrl}); - - expect(fetch).toHaveBeenCalledWith(`${baseUrl}/api/users/v1/me/organizations?limit=10&recursive=false`, { - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - method: 'GET', - }); - expect(result).toEqual(mock.organizations); - }); - - it('should append query params when provided', async (): Promise => { - const mock: {organizations: Organization[]} = {organizations: [] as Organization[]}; - - global.fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mock), - ok: true, - }); - - const baseUrl = 'https://localhost:8090'; - await getMeOrganizations({ - after: 'YWZ0', - authorizedAppName: 'my-app', - baseUrl, - before: 'YmZy', - filter: 'name co "acme"', - limit: 25, - recursive: true, - }); - - expect(fetch).toHaveBeenCalledWith( - `${baseUrl}/api/users/v1/me/organizations?after=YWZ0&authorizedAppName=my-app&before=YmZy&filter=name+co+%22acme%22&limit=25&recursive=true`, - expect.any(Object), - ); - }); - - it('should use custom fetcher when provided', async (): Promise => { - const mock: {organizations: Organization[]} = {organizations: [{id: 'o1', name: 'C', orgHandle: 'c'}]}; - - const customFetcher: Mock = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mock), - ok: true, - }); - - const baseUrl = 'https://localhost:8090'; - const result: Organization[] = await getMeOrganizations({baseUrl, fetcher: customFetcher}); - - expect(result).toEqual(mock.organizations); - expect(customFetcher).toHaveBeenCalledWith( - `${baseUrl}/api/users/v1/me/organizations?limit=10&recursive=false`, - expect.objectContaining({method: 'GET'}), - ); - }); - - it('should handle errors thrown directly by custom fetcher', async (): Promise => { - const customFetcher: Mock = vi.fn().mockImplementation(() => { - throw new Error('Custom fetcher failure'); - }); - - const baseUrl = 'https://localhost:8090'; - - await expect(getMeOrganizations({baseUrl, fetcher: customFetcher})).rejects.toThrow( - 'Network or parsing error: Custom fetcher failure', - ); - }); - - it('should throw ThunderIDAPIError for invalid base URL', async (): Promise => { - await expect(getMeOrganizations({baseUrl: 'invalid-url' as any})).rejects.toThrow(ThunderIDAPIError); - await expect(getMeOrganizations({baseUrl: 'invalid-url' as any})).rejects.toThrow('Invalid base URL provided.'); - }); - - it('should throw ThunderIDAPIError for undefined baseUrl', async (): Promise => { - await expect(getMeOrganizations({baseUrl: undefined as any})).rejects.toThrow(ThunderIDAPIError); - await expect(getMeOrganizations({baseUrl: undefined as any})).rejects.toThrow('Invalid base URL provided.'); - }); - - it('should throw ThunderIDAPIError for empty string baseUrl', async (): Promise => { - await expect(getMeOrganizations({baseUrl: ''})).rejects.toThrow(ThunderIDAPIError); - await expect(getMeOrganizations({baseUrl: ''})).rejects.toThrow('Invalid base URL provided.'); - }); - - it('should handle HTTP error responses', async (): Promise => { - global.fetch = vi.fn().mockResolvedValue({ - ok: false, - status: 403, - statusText: 'Forbidden', - text: () => Promise.resolve('Not authorized'), - }); - - const baseUrl = 'https://localhost:8090'; - - await expect(getMeOrganizations({baseUrl})).rejects.toThrow(ThunderIDAPIError); - await expect(getMeOrganizations({baseUrl})).rejects.toThrow( - 'Failed to fetch associated organizations of the user: Not authorized', - ); - }); - - it('should handle network or parsing errors', async (): Promise => { - global.fetch = vi.fn().mockRejectedValue(new Error('Network error')); - - const baseUrl = 'https://localhost:8090'; - - await expect(getMeOrganizations({baseUrl})).rejects.toThrow(ThunderIDAPIError); - await expect(getMeOrganizations({baseUrl})).rejects.toThrow('Network or parsing error: Network error'); - }); - - it('should handle non-Error rejections', async (): Promise => { - global.fetch = vi.fn().mockRejectedValue('unexpected failure'); - - const baseUrl = 'https://localhost:8090'; - - await expect(getMeOrganizations({baseUrl})).rejects.toThrow('Network or parsing error: Unknown error'); - }); - - it('should include custom headers when provided', async (): Promise => { - const mock: {organizations: Organization[]} = {organizations: [] as Organization[]}; - - global.fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mock), - ok: true, - }); - - const baseUrl = 'https://localhost:8090'; - const customHeaders: Record = { - Authorization: 'Bearer token', - 'X-Custom-Header': 'custom-value', - }; - - await getMeOrganizations({baseUrl, headers: customHeaders}); - - expect(fetch).toHaveBeenCalledWith(`${baseUrl}/api/users/v1/me/organizations?limit=10&recursive=false`, { - headers: { - Accept: 'application/json', - Authorization: 'Bearer token', - 'Content-Type': 'application/json', - 'X-Custom-Header': 'custom-value', - }, - method: 'GET', - }); - }); - - it('should return [] if response has no organizations property', async (): Promise => { - global.fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve({}), // missing organizations - ok: true, - }); - - const baseUrl = 'https://localhost:8090'; - const result: Organization[] = await getMeOrganizations({baseUrl}); - - expect(result).toEqual([]); - }); - - it('should pass custom headers to fetch correctly', async (): Promise => { - const mock: {organizations: Organization[]} = {organizations: [] as Organization[]}; - - global.fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mock), - ok: true, - }); - - const baseUrl = 'https://localhost:8090'; - const customHeaders: Record = { - Authorization: 'Bearer token', - 'X-Custom-Header': 'custom-value', - }; - - await getMeOrganizations({baseUrl, headers: customHeaders}); - expect(fetch).toHaveBeenCalledWith(`${baseUrl}/api/users/v1/me/organizations?limit=10&recursive=false`, { - headers: { - Accept: 'application/json', - Authorization: 'Bearer token', - 'Content-Type': 'application/json', - 'X-Custom-Header': 'custom-value', - }, - method: 'GET', - }); - }); -}); diff --git a/packages/javascript/src/api/__tests__/getOrganization.test.ts b/packages/javascript/src/api/__tests__/getOrganization.test.ts deleted file mode 100644 index eacb092..0000000 --- a/packages/javascript/src/api/__tests__/getOrganization.test.ts +++ /dev/null @@ -1,204 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {describe, it, expect, vi, beforeEach} from 'vitest'; -import ThunderIDAPIError from '../../errors/ThunderIDAPIError'; -import getOrganization from '../getOrganization'; -import type {OrganizationDetails} from '../getOrganization'; - -describe('getOrganization', (): void => { - beforeEach((): void => { - vi.resetAllMocks(); - }); - - it('should fetch organization details successfully (default fetch)', async (): Promise => { - const mockOrg: OrganizationDetails = { - description: 'Demo org', - id: '0d5e071b-d3d3-475d-b3c6-1a20ee2fa9b1', - name: 'DX Lab', - orgHandle: 'dxlab', - }; - - global.fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mockOrg), - ok: true, - }); - - const baseUrl = 'https://localhost:8090'; - const organizationId: string = mockOrg.id; - const result: OrganizationDetails = await getOrganization({baseUrl, organizationId}); - - expect(fetch).toHaveBeenCalledWith(`${baseUrl}/api/server/v1/organizations/${organizationId}`, { - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - method: 'GET', - }); - expect(result).toEqual(mockOrg); - }); - - it('should use custom fetcher when provided', async (): Promise => { - const mockOrg: OrganizationDetails = { - id: 'org-123', - name: 'Custom Org', - orgHandle: 'custom-org', - }; - - const customFetcher: typeof fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mockOrg), - ok: true, - }); - - const baseUrl = 'https://localhost:8090'; - const organizationId = 'org-123'; - const result: OrganizationDetails = await getOrganization({ - baseUrl, - fetcher: customFetcher, - organizationId, - }); - - expect(result).toEqual(mockOrg); - expect(customFetcher).toHaveBeenCalledWith( - `${baseUrl}/api/server/v1/organizations/${organizationId}`, - expect.objectContaining({ - headers: expect.objectContaining({ - Accept: 'application/json', - 'Content-Type': 'application/json', - }), - method: 'GET', - }), - ); - }); - - it('should handle errors thrown directly by custom fetcher', async (): Promise => { - const customFetcher: typeof fetch = vi.fn().mockImplementation(() => { - throw new Error('Custom fetcher failure'); - }); - - const baseUrl = 'https://localhost:8090'; - const organizationId = 'org-1'; - - await expect(getOrganization({baseUrl, fetcher: customFetcher, organizationId})).rejects.toThrow( - 'Network or parsing error: Custom fetcher failure', - ); - }); - - it('should throw ThunderIDAPIError for invalid base URL', async (): Promise => { - await expect(getOrganization({baseUrl: 'invalid-url' as any, organizationId: 'org-1'})).rejects.toThrow( - ThunderIDAPIError, - ); - // Substring match is fine because the implementation appends the native error text - await expect(getOrganization({baseUrl: 'invalid-url' as any, organizationId: 'org-1'})).rejects.toThrow( - 'Invalid base URL provided.', - ); - }); - - it('should throw ThunderIDAPIError for undefined baseUrl', async (): Promise => { - await expect(getOrganization({baseUrl: undefined as any, organizationId: 'org-1'})).rejects.toThrow( - ThunderIDAPIError, - ); - await expect(getOrganization({baseUrl: undefined as any, organizationId: 'org-1'})).rejects.toThrow( - 'Invalid base URL provided.', - ); - }); - - it('should throw ThunderIDAPIError for empty string baseUrl', async (): Promise => { - await expect(getOrganization({baseUrl: '', organizationId: 'org-1'})).rejects.toThrow(ThunderIDAPIError); - await expect(getOrganization({baseUrl: '', organizationId: 'org-1'})).rejects.toThrow('Invalid base URL provided.'); - }); - - it('should throw ThunderIDAPIError when organizationId is missing', async (): Promise => { - const baseUrl = 'https://localhost:8090'; - - await expect(getOrganization({baseUrl, organizationId: '' as any})).rejects.toThrow(ThunderIDAPIError); - await expect(getOrganization({baseUrl, organizationId: '' as any})).rejects.toThrow('Organization ID is required'); - }); - - it('should handle HTTP error responses', async (): Promise => { - global.fetch = vi.fn().mockResolvedValue({ - ok: false, - status: 404, - statusText: 'Not Found', - text: () => Promise.resolve('Organization not found'), - }); - - const baseUrl = 'https://localhost:8090'; - const organizationId = 'missing-org'; - - await expect(getOrganization({baseUrl, organizationId})).rejects.toThrow(ThunderIDAPIError); - await expect(getOrganization({baseUrl, organizationId})).rejects.toThrow( - 'Failed to fetch organization details: Organization not found', - ); - }); - - it('should handle network or parsing errors', async (): Promise => { - global.fetch = vi.fn().mockRejectedValue(new Error('Network error')); - - const baseUrl = 'https://localhost:8090'; - const organizationId = 'org-1'; - - await expect(getOrganization({baseUrl, organizationId})).rejects.toThrow(ThunderIDAPIError); - await expect(getOrganization({baseUrl, organizationId})).rejects.toThrow('Network or parsing error: Network error'); - }); - - it('should handle non-Error rejections', async (): Promise => { - global.fetch = vi.fn().mockRejectedValue('unexpected failure'); - - const baseUrl = 'https://localhost:8090'; - const organizationId = 'org-1'; - - await expect(getOrganization({baseUrl, organizationId})).rejects.toThrow('Network or parsing error: Unknown error'); - }); - - it('should include custom headers when provided', async (): Promise => { - const mockOrg: OrganizationDetails = { - id: 'org-003', - name: 'Header Org', - orgHandle: 'header-org', - }; - - global.fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mockOrg), - ok: true, - }); - - const baseUrl = 'https://localhost:8090'; - const organizationId = 'org-003'; - const customHeaders: Record = { - Authorization: 'Bearer token', - 'X-Custom-Header': 'custom-value', - }; - - await getOrganization({ - baseUrl, - headers: customHeaders, - organizationId, - }); - - expect(fetch).toHaveBeenCalledWith(`${baseUrl}/api/server/v1/organizations/${organizationId}`, { - headers: { - Accept: 'application/json', - Authorization: 'Bearer token', - 'Content-Type': 'application/json', - 'X-Custom-Header': 'custom-value', - }, - method: 'GET', - }); - }); -}); diff --git a/packages/javascript/src/api/__tests__/updateOrganization.test.ts b/packages/javascript/src/api/__tests__/updateOrganization.test.ts deleted file mode 100644 index 025d1a6..0000000 --- a/packages/javascript/src/api/__tests__/updateOrganization.test.ts +++ /dev/null @@ -1,341 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {describe, it, expect, vi, beforeEach} from 'vitest'; -import ThunderIDAPIError from '../../errors/ThunderIDAPIError'; -import type {OrganizationDetails} from '../getOrganization'; -import updateOrganization, {createPatchOperations} from '../updateOrganization'; - -interface PatchOp { - operation: 'REPLACE' | 'ADD' | 'REMOVE'; - path: string; - value?: unknown; -} - -describe('updateOrganization', (): void => { - const baseUrl = 'https://localhost:8090'; - const organizationId = '0d5e071b-d3d3-475d-b3c6-1a20ee2fa9b1'; - - beforeEach((): void => { - vi.resetAllMocks(); - }); - - it('should update organization successfully with default fetch', async (): Promise => { - const mockOrg: OrganizationDetails = { - description: 'Updated', - id: organizationId, - name: 'Updated Name', - orgHandle: 'demo', - }; - - global.fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mockOrg), - ok: true, - }); - - const operations: PatchOp[] = [ - {operation: 'REPLACE' as const, path: '/name', value: 'Updated Name'}, - {operation: 'REPLACE' as const, path: '/description', value: 'Updated'}, - ]; - - const result: OrganizationDetails = await updateOrganization({ - baseUrl, - operations, - organizationId, - }); - - expect(fetch).toHaveBeenCalledWith(`${baseUrl}/api/server/v1/organizations/${organizationId}`, { - body: JSON.stringify(operations), - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - method: 'PATCH', - }); - expect(result).toEqual(mockOrg); - }); - - it('should use custom fetcher when provided', async (): Promise => { - const mockOrg: OrganizationDetails = { - id: organizationId, - name: 'Custom', - orgHandle: 'custom', - }; - - const customFetcher: ReturnType = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mockOrg), - ok: true, - }); - - const operations: PatchOp[] = [{operation: 'REPLACE' as const, path: '/name', value: 'Custom'}]; - - const result: OrganizationDetails = await updateOrganization({ - baseUrl, - fetcher: customFetcher, - operations, - organizationId, - }); - - expect(result).toEqual(mockOrg); - expect(customFetcher).toHaveBeenCalledWith( - `${baseUrl}/api/server/v1/organizations/${organizationId}`, - expect.objectContaining({ - body: JSON.stringify(operations), - headers: expect.objectContaining({ - Accept: 'application/json', - 'Content-Type': 'application/json', - }), - method: 'PATCH', - }), - ); - }); - - it('should handle errors thrown directly by custom fetcher', async (): Promise => { - const customFetcher: ReturnType = vi.fn().mockImplementation(() => { - throw new Error('Custom fetcher failure'); - }); - - const operations: PatchOp[] = [{operation: 'REPLACE' as const, path: '/name', value: 'X'}]; - - await expect(updateOrganization({baseUrl, fetcher: customFetcher, operations, organizationId})).rejects.toThrow( - 'Network or parsing error: Custom fetcher failure', - ); - }); - - it('should throw ThunderIDAPIError for invalid base URL', async (): Promise => { - const operations: PatchOp[] = [{operation: 'REPLACE' as const, path: '/name', value: 'X'}]; - - await expect(updateOrganization({baseUrl: 'invalid-url' as any, operations, organizationId})).rejects.toThrow( - ThunderIDAPIError, - ); - - await expect(updateOrganization({baseUrl: 'invalid-url' as any, operations, organizationId})).rejects.toThrow( - 'Invalid base URL provided.', - ); - }); - - it('should throw ThunderIDAPIError for undefined baseUrl', async (): Promise => { - const operations: PatchOp[] = [{operation: 'REPLACE' as const, path: '/name', value: 'X'}]; - - await expect(updateOrganization({baseUrl: undefined as any, operations, organizationId})).rejects.toThrow( - ThunderIDAPIError, - ); - await expect(updateOrganization({baseUrl: undefined as any, operations, organizationId})).rejects.toThrow( - 'Invalid base URL provided.', - ); - }); - - it('should throw ThunderIDAPIError for empty string baseUrl', async (): Promise => { - const operations: PatchOp[] = [{operation: 'REPLACE' as const, path: '/name', value: 'X'}]; - - await expect(updateOrganization({baseUrl: '', operations, organizationId})).rejects.toThrow(ThunderIDAPIError); - await expect(updateOrganization({baseUrl: '', operations, organizationId})).rejects.toThrow( - 'Invalid base URL provided.', - ); - }); - - it('should throw ThunderIDAPIError when organizationId is missing', async (): Promise => { - const operations: PatchOp[] = [{operation: 'REPLACE' as const, path: '/name', value: 'X'}]; - - await expect(updateOrganization({baseUrl, operations, organizationId: '' as any})).rejects.toThrow( - ThunderIDAPIError, - ); - await expect(updateOrganization({baseUrl, operations, organizationId: '' as any})).rejects.toThrow( - 'Organization ID is required', - ); - }); - - it('should throw ThunderIDAPIError when operations is missing/empty', async (): Promise => { - await expect(updateOrganization({baseUrl, operations: undefined as any, organizationId})).rejects.toThrow( - 'Operations array is required and cannot be empty', - ); - - await expect(updateOrganization({baseUrl, operations: [], organizationId})).rejects.toThrow( - 'Operations array is required and cannot be empty', - ); - - await expect(updateOrganization({baseUrl, operations: 'not-array' as any, organizationId})).rejects.toThrow( - 'Operations array is required and cannot be empty', - ); - }); - - it('should handle HTTP error responses', async (): Promise => { - global.fetch = vi.fn().mockResolvedValue({ - ok: false, - status: 400, - statusText: 'Bad Request', - text: () => Promise.resolve('Invalid operations'), - }); - - const operations: PatchOp[] = [{operation: 'REPLACE' as const, path: '/name', value: 'X'}]; - - await expect(updateOrganization({baseUrl, operations, organizationId})).rejects.toThrow(ThunderIDAPIError); - await expect(updateOrganization({baseUrl, operations, organizationId})).rejects.toThrow( - 'Failed to update organization: Invalid operations', - ); - }); - - it('should handle network or parsing errors', async (): Promise => { - global.fetch = vi.fn().mockRejectedValue(new Error('Network error')); - - const operations: PatchOp[] = [{operation: 'REPLACE' as const, path: '/name', value: 'X'}]; - - await expect(updateOrganization({baseUrl, operations, organizationId})).rejects.toThrow(ThunderIDAPIError); - await expect(updateOrganization({baseUrl, operations, organizationId})).rejects.toThrow( - 'Network or parsing error: Network error', - ); - }); - - it('should handle non-Error rejections', async (): Promise => { - global.fetch = vi.fn().mockRejectedValue('unexpected failure'); - - const operations: PatchOp[] = [{operation: 'REPLACE' as const, path: '/name', value: 'X'}]; - - await expect(updateOrganization({baseUrl, operations, organizationId})).rejects.toThrow( - 'Network or parsing error: Unknown error', - ); - }); - - it('should include custom headers when provided', async (): Promise => { - const mockOrg: OrganizationDetails = { - id: organizationId, - name: 'Header Org', - orgHandle: 'header-org', - }; - - global.fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mockOrg), - ok: true, - }); - - const operations: PatchOp[] = [{operation: 'REPLACE' as const, path: '/name', value: 'Header Org'}]; - - const customHeaders: Record = { - Authorization: 'Bearer token', - 'X-Custom-Header': 'custom-value', - }; - - await updateOrganization({ - baseUrl, - headers: customHeaders, - operations, - organizationId, - }); - - expect(fetch).toHaveBeenCalledWith(`${baseUrl}/api/server/v1/organizations/${organizationId}`, { - body: JSON.stringify(operations), - headers: { - Accept: 'application/json', - Authorization: 'Bearer token', - 'Content-Type': 'application/json', - 'X-Custom-Header': 'custom-value', - }, - method: 'PATCH', - }); - }); - - it('should always use HTTP PATCH even if a different method is passed in requestConfig', async (): Promise => { - const mockOrg: OrganizationDetails = { - id: organizationId, - name: 'A', - orgHandle: 'a', - }; - - global.fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mockOrg), - ok: true, - }); - - const operations: PatchOp[] = [{operation: 'REPLACE' as const, path: '/name', value: 'A'}]; - - await updateOrganization({ - baseUrl, - method: 'PUT', - operations, - organizationId, - }); - - expect(fetch).toHaveBeenCalledWith( - `${baseUrl}/api/server/v1/organizations/${organizationId}`, - expect.objectContaining({method: 'PATCH'}), - ); - }); - - it('should send the exact operations array as body (no mutation)', async (): Promise => { - const mockOrg: OrganizationDetails = { - id: organizationId, - name: 'B', - orgHandle: 'b', - }; - - global.fetch = vi.fn().mockResolvedValue({ - json: () => Promise.resolve(mockOrg), - ok: true, - }); - - const operations: PatchOp[] = [ - {operation: 'REPLACE' as const, path: 'name', value: 'B'}, - {operation: 'REMOVE' as const, path: 'description'}, - ]; - - await updateOrganization({baseUrl, operations, organizationId}); - - const [, init] = (fetch as any).mock.calls[0]; - expect(JSON.parse(init.body)).toEqual(operations); - }); -}); - -describe('createPatchOperations', (): void => { - it('should generate REPLACE for non-empty values and REMOVE for empty', (): void => { - const payload: Record = { - description: '', - extra: 'value', - name: 'Updated Organization', - note: null, - }; - - const ops: PatchOp[] = createPatchOperations(payload); - - expect(ops).toEqual( - expect.arrayContaining([ - {operation: 'REPLACE', path: '/name', value: 'Updated Organization'}, - {operation: 'REPLACE', path: '/extra', value: 'value'}, - {operation: 'REMOVE', path: '/description'}, - {operation: 'REMOVE', path: '/note'}, - ]), - ); - }); - - it('should prefix all paths with a slash', (): void => { - const ops: PatchOp[] = createPatchOperations({ - summary: '', - title: 'A', - }); - - expect(ops.find((o: PatchOp) => o.path === '/title')).toBeDefined(); - expect(ops.find((o: PatchOp) => o.path === '/summary')).toBeDefined(); - }); - - it('should handle undefined payload values as REMOVE', (): void => { - const ops: PatchOp[] = createPatchOperations({ - something: undefined, - }); - - expect(ops).toEqual([{operation: 'REMOVE', path: '/something'}]); - }); -}); diff --git a/packages/javascript/src/api/createOrganization.ts b/packages/javascript/src/api/createOrganization.ts deleted file mode 100644 index 7c438de..0000000 --- a/packages/javascript/src/api/createOrganization.ts +++ /dev/null @@ -1,214 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import ThunderIDAPIError from '../errors/ThunderIDAPIError'; -import {Organization} from '../models/organization'; - -/** - * Interface for organization creation payload. - */ -export interface CreateOrganizationPayload { - /** - * Organization description. - */ - description: string; - /** - * Organization name. - */ - name: string; - /** - * Organization handle/slug. - */ - orgHandle?: string; - /** - * Parent organization ID. - */ - parentId: string; - /** - * Organization type. - */ - type: 'TENANT'; -} - -/** - * Configuration for the createOrganization request - */ -export interface CreateOrganizationConfig extends Omit { - /** - * The base URL for the API endpoint. - */ - baseUrl: string; - /** - * Optional custom fetcher function. - * If not provided, native fetch will be used - */ - fetcher?: (url: string, config: RequestInit) => Promise; - /** - * Organization creation payload - */ - payload: CreateOrganizationPayload; -} - -/** - * Creates a new organization. - * - * @param config - Configuration object containing baseUrl, payload and optional request config. - * @returns A promise that resolves with the created organization information. - * @example - * ```typescript - * // Using default fetch - * try { - * const organization = await createOrganization({ - * baseUrl: "https://localhost:8090", - * payload: { - * description: "Share your screens", - * name: "Team Viewer", - * orgHandle: "team-viewer", - * parentId: "f4825104-4948-40d9-ab65-a960eee3e3d5", - * type: "TENANT" - * } - * }); - * console.log(organization); - * } catch (error) { - * if (error instanceof ThunderIDAPIError) { - * console.error('Failed to create organization:', error.message); - * } - * } - * ``` - * - * @example - * ```typescript - * // Using custom fetcher (e.g., axios-based httpClient) - * try { - * const organization = await createOrganization({ - * baseUrl: "https://localhost:8090", - * payload: { - * description: "Share your screens", - * name: "Team Viewer", - * orgHandle: "team-viewer", - * parentId: "f4825104-4948-40d9-ab65-a960eee3e3d5", - * type: "TENANT" - * }, - * fetcher: async (url, config) => { - * const response = await httpClient({ - * url, - * method: config.method, - * headers: config.headers, - * data: config.body, - * ...config - * }); - * // Convert axios-like response to fetch-like Response - * return { - * ok: response.status >= 200 && response.status < 300, - * status: response.status, - * statusText: response.statusText, - * json: () => Promise.resolve(response.data), - * text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)) - * } as Response; - * } - * }); - * console.log(organization); - * } catch (error) { - * if (error instanceof ThunderIDAPIError) { - * console.error('Failed to create organization:', error.message); - * } - * } - * ``` - */ -const createOrganization = async ({ - baseUrl, - payload, - fetcher, - ...requestConfig -}: CreateOrganizationConfig): Promise => { - try { - // eslint-disable-next-line no-new - new URL(baseUrl); - } catch (error) { - throw new ThunderIDAPIError( - `Invalid base URL provided. ${error?.toString()}`, - 'createOrganization-ValidationError-001', - 'javascript', - 400, - 'The provided `baseUrl` does not adhere to the URL schema.', - ); - } - - if (!payload) { - throw new ThunderIDAPIError( - 'Organization payload is required', - 'createOrganization-ValidationError-002', - 'javascript', - 400, - 'Invalid Request', - ); - } - - // Always set type to TENANT for now - const organizationPayload: CreateOrganizationPayload = { - ...payload, - type: 'TENANT' as const, - }; - - const fetchFn: typeof fetch = fetcher || fetch; - const resolvedUrl = `${baseUrl}/api/server/v1/organizations`; - - const requestInit: RequestInit = { - ...requestConfig, - body: JSON.stringify(organizationPayload), - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - ...requestConfig.headers, - }, - method: 'POST', - }; - - try { - const response: Response = await fetchFn(resolvedUrl, requestInit); - - if (!response?.ok) { - const errorText: string = await response.text(); - - throw new ThunderIDAPIError( - errorText, - 'createOrganization-ResponseError-001', - 'javascript', - response.status, - response.statusText, - 'Failed to create organization', - ); - } - - return (await response.json()) as Organization; - } catch (error) { - if (error instanceof ThunderIDAPIError) { - throw error; - } - - throw new ThunderIDAPIError( - `Network or parsing error: ${error instanceof Error ? error.message : 'Unknown error'}`, - 'createOrganization-NetworkError-001', - 'javascript', - 0, - 'Network Error', - ); - } -}; - -export default createOrganization; diff --git a/packages/javascript/src/api/getAllOrganizations.ts b/packages/javascript/src/api/getAllOrganizations.ts deleted file mode 100644 index 3af34f6..0000000 --- a/packages/javascript/src/api/getAllOrganizations.ts +++ /dev/null @@ -1,189 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import ThunderIDAPIError from '../errors/ThunderIDAPIError'; -import {AllOrganizationsApiResponse} from '../models/organization'; - -/** - * Configuration for the getAllOrganizations request - */ -export interface GetAllOrganizationsConfig extends Omit { - /** - * The base URL for the API endpoint. - */ - baseUrl: string; - /** - * Optional custom fetcher function. - * If not provided, native fetch will be used - */ - fetcher?: (url: string, config: RequestInit) => Promise; - /** - * Filter expression for organizations - */ - filter?: string; - /** - * Maximum number of organizations to return - */ - limit?: number; - /** - * Whether to include child organizations recursively - */ - recursive?: boolean; -} - -/** - * Retrieves all organizations with pagination support. - * - * @param config - Configuration object containing baseUrl, optional query parameters, and request config. - * @returns A promise that resolves with the paginated organizations information. - * @example - * ```typescript - * // Using default fetch - * try { - * const response = await getAllOrganizations({ - * baseUrl: "https://localhost:8090", - * filter: "", - * limit: 10, - * recursive: false - * }); - * console.log(response.organizations); - * } catch (error) { - * if (error instanceof ThunderIDAPIError) { - * console.error('Failed to get organizations:', error.message); - * } - * } - * ``` - * - * @example - * ```typescript - * // Using custom fetcher (e.g., axios-based httpClient) - * try { - * const response = await getAllOrganizations({ - * baseUrl: "https://localhost:8090", - * filter: "", - * limit: 10, - * recursive: false, - * fetcher: async (url, config) => { - * const response = await httpClient({ - * url, - * method: config.method, - * headers: config.headers, - * ...config - * }); - * // Convert axios-like response to fetch-like Response - * return { - * ok: response.status >= 200 && response.status < 300, - * status: response.status, - * statusText: response.statusText, - * json: () => Promise.resolve(response.data), - * text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)) - * } as Response; - * } - * }); - * console.log(response.organizations); - * } catch (error) { - * if (error instanceof ThunderIDAPIError) { - * console.error('Failed to get organizations:', error.message); - * } - * } - * ``` - */ -const getAllOrganizations = async ({ - baseUrl, - filter = '', - limit = 10, - recursive = false, - fetcher, - ...requestConfig -}: GetAllOrganizationsConfig): Promise => { - try { - // eslint-disable-next-line no-new - new URL(baseUrl); - } catch (error) { - throw new ThunderIDAPIError( - `Invalid base URL provided. ${error?.toString()}`, - 'getAllOrganizations-ValidationError-001', - 'javascript', - 400, - 'The provided `baseUrl` does not adhere to the URL schema.', - ); - } - - const queryParams: URLSearchParams = new URLSearchParams( - Object.fromEntries( - Object.entries({ - filter, - limit: limit.toString(), - recursive: recursive.toString(), - }).filter(([, value]: [string, string]) => Boolean(value)), - ), - ); - - const fetchFn: typeof fetch = fetcher || fetch; - const resolvedUrl = `${baseUrl}/api/server/v1/organizations?${queryParams.toString()}`; - - const requestInit: RequestInit = { - ...requestConfig, - headers: { - ...requestConfig.headers, - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - method: 'GET', - }; - - try { - const response: Response = await fetchFn(resolvedUrl, requestInit); - - if (!response?.ok) { - const errorText: string = await response.text(); - - throw new ThunderIDAPIError( - errorText, - 'getAllOrganizations-ResponseError-001', - 'javascript', - response.status, - response.statusText, - 'Failed to get organizations', - ); - } - - const data: AllOrganizationsApiResponse = await response.json(); - - return { - hasMore: data.hasMore, - nextCursor: data.nextCursor, - organizations: data.organizations || [], - totalCount: data.totalCount, - }; - } catch (error) { - if (error instanceof ThunderIDAPIError) { - throw error; - } - - throw new ThunderIDAPIError( - `Network or parsing error: ${error instanceof Error ? error.message : 'Unknown error'}`, - 'getAllOrganizations-NetworkError-001', - 'javascript', - 0, - 'Network Error', - ); - } -}; - -export default getAllOrganizations; diff --git a/packages/javascript/src/api/getMeOrganizations.ts b/packages/javascript/src/api/getMeOrganizations.ts deleted file mode 100644 index be7e08a..0000000 --- a/packages/javascript/src/api/getMeOrganizations.ts +++ /dev/null @@ -1,205 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import ThunderIDAPIError from '../errors/ThunderIDAPIError'; -import {Organization} from '../models/organization'; - -/** - * Configuration for the getMeOrganizations request - */ -export interface GetMeOrganizationsConfig extends Omit { - /** - * Base64 encoded cursor value for forward pagination - */ - after?: string; - /** - * Authorized application name filter - */ - authorizedAppName?: string; - /** - * The base URL for the API endpoint. - */ - baseUrl: string; - /** - * Base64 encoded cursor value for backward pagination - */ - before?: string; - /** - * Optional custom fetcher function. - * If not provided, native fetch will be used - */ - fetcher?: (url: string, config: RequestInit) => Promise; - /** - * Filter expression for organizations - */ - filter?: string; - /** - * Maximum number of organizations to return - */ - limit?: number; - /** - * Whether to include child organizations recursively - */ - recursive?: boolean; -} - -/** - * Retrieves the organizations associated with the current user. - * - * @param config - Configuration object containing baseUrl, optional query parameters, and request config. - * @returns A promise that resolves with the organizations information. - * @example - * ```typescript - * // Using default fetch - * try { - * const organizations = await getMeOrganizations({ - * baseUrl: "https://localhost:8090", - * after: "", - * before: "", - * filter: "", - * limit: 10, - * recursive: false - * }); - * console.log(organizations); - * } catch (error) { - * if (error instanceof ThunderIDAPIError) { - * console.error('Failed to get organizations:', error.message); - * } - * } - * ``` - * - * @example - * ```typescript - * // Using custom fetcher (e.g., axios-based httpClient) - * try { - * const organizations = await getMeOrganizations({ - * baseUrl: "https://localhost:8090", - * after: "", - * before: "", - * filter: "", - * limit: 10, - * recursive: false, - * fetcher: async (url, config) => { - * const response = await httpClient({ - * url, - * method: config.method, - * headers: config.headers, - * ...config - * }); - * // Convert axios-like response to fetch-like Response - * return { - * ok: response.status >= 200 && response.status < 300, - * status: response.status, - * statusText: response.statusText, - * json: () => Promise.resolve(response.data), - * text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)) - * } as Response; - * } - * }); - * console.log(organizations); - * } catch (error) { - * if (error instanceof ThunderIDAPIError) { - * console.error('Failed to get organizations:', error.message); - * } - * } - * ``` - */ -const getMeOrganizations = async ({ - baseUrl, - after = '', - authorizedAppName = '', - before = '', - filter = '', - limit = 10, - recursive = false, - fetcher, - ...requestConfig -}: GetMeOrganizationsConfig): Promise => { - try { - // eslint-disable-next-line no-new - new URL(baseUrl); - } catch (error) { - throw new ThunderIDAPIError( - `Invalid base URL provided. ${error?.toString()}`, - 'getMeOrganizations-ValidationError-001', - 'javascript', - 400, - 'The provided `baseUrl` does not adhere to the URL schema.', - ); - } - - const queryParams: URLSearchParams = new URLSearchParams( - Object.fromEntries( - Object.entries({ - after, - authorizedAppName, - before, - filter, - limit: limit.toString(), - recursive: recursive.toString(), - }).filter(([, value]: [string, string]) => Boolean(value)), - ), - ); - - const fetchFn: typeof fetch = fetcher || fetch; - const resolvedUrl = `${baseUrl}/api/users/v1/me/organizations?${queryParams.toString()}`; - - const requestInit: RequestInit = { - ...requestConfig, - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - ...requestConfig.headers, - }, - method: 'GET', - }; - - try { - const response: Response = await fetchFn(resolvedUrl, requestInit); - - if (!response?.ok) { - const errorText: string = await response.text(); - - throw new ThunderIDAPIError( - errorText, - 'getMeOrganizations-ResponseError-001', - 'javascript', - response.status, - response.statusText, - 'Failed to fetch associated organizations of the user', - ); - } - - const data: Record = await response.json(); - return (data['organizations'] as Organization[]) || []; - } catch (error) { - if (error instanceof ThunderIDAPIError) { - throw error; - } - - throw new ThunderIDAPIError( - `Network or parsing error: ${error instanceof Error ? error.message : 'Unknown error'}`, - 'getMeOrganizations-NetworkError-001', - 'javascript', - 0, - 'Network Error', - ); - } -}; - -export default getMeOrganizations; diff --git a/packages/javascript/src/api/getOrganization.ts b/packages/javascript/src/api/getOrganization.ts deleted file mode 100644 index 7b246b3..0000000 --- a/packages/javascript/src/api/getOrganization.ts +++ /dev/null @@ -1,187 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import ThunderIDAPIError from '../errors/ThunderIDAPIError'; - -/** - * Extended organization interface with additional properties - */ -export interface OrganizationDetails { - attributes?: Record; - created?: string; - description?: string; - id: string; - lastModified?: string; - name: string; - orgHandle: string; - parent?: { - id: string; - ref: string; - }; - permissions?: string[]; - status?: string; - type?: string; -} - -/** - * Configuration for the getOrganization request - */ -export interface GetOrganizationConfig extends Omit { - /** - * The base URL for the API endpoint. - */ - baseUrl: string; - /** - * Optional custom fetcher function. - * If not provided, native fetch will be used - */ - fetcher?: (url: string, config: RequestInit) => Promise; - /** - * The ID of the organization to retrieve - */ - organizationId: string; -} - -/** - * Retrieves detailed information for a specific organization. - * - * @param config - Configuration object containing baseUrl, organizationId, and request config. - * @returns A promise that resolves with the organization details. - * @example - * ```typescript - * // Using default fetch - * try { - * const organization = await getOrganization({ - * baseUrl: "https://localhost:8090", - * organizationId: "0d5e071b-d3d3-475d-b3c6-1a20ee2fa9b1" - * }); - * console.log(organization); - * } catch (error) { - * if (error instanceof ThunderIDAPIError) { - * console.error('Failed to get organization:', error.message); - * } - * } - * ``` - * - * @example - * ```typescript - * // Using custom fetcher (e.g., axios-based httpClient) - * try { - * const organization = await getOrganization({ - * baseUrl: "https://localhost:8090", - * organizationId: "0d5e071b-d3d3-475d-b3c6-1a20ee2fa9b1", - * fetcher: async (url, config) => { - * const response = await httpClient({ - * url, - * method: config.method, - * headers: config.headers, - * ...config - * }); - * // Convert axios-like response to fetch-like Response - * return { - * ok: response.status >= 200 && response.status < 300, - * status: response.status, - * statusText: response.statusText, - * json: () => Promise.resolve(response.data), - * text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)) - * } as Response; - * } - * }); - * console.log(organization); - * } catch (error) { - * if (error instanceof ThunderIDAPIError) { - * console.error('Failed to get organization:', error.message); - * } - * } - * ``` - */ -const getOrganization = async ({ - baseUrl, - organizationId, - fetcher, - ...requestConfig -}: GetOrganizationConfig): Promise => { - try { - // eslint-disable-next-line no-new - new URL(baseUrl); - } catch (error) { - throw new ThunderIDAPIError( - `Invalid base URL provided. ${error?.toString()}`, - 'getOrganization-ValidationError-001', - 'javascript', - 400, - 'The provided `baseUrl` does not adhere to the URL schema.', - ); - } - - if (!organizationId) { - throw new ThunderIDAPIError( - 'Organization ID is required', - 'getOrganization-ValidationError-002', - 'javascript', - 400, - 'Invalid Request', - ); - } - - const fetchFn: typeof fetch = fetcher || fetch; - const resolvedUrl = `${baseUrl}/api/server/v1/organizations/${organizationId}`; - - const requestInit: RequestInit = { - ...requestConfig, - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - ...requestConfig.headers, - }, - method: 'GET', - }; - - try { - const response: Response = await fetchFn(resolvedUrl, requestInit); - - if (!response?.ok) { - const errorText: string = await response.text(); - - throw new ThunderIDAPIError( - errorText, - 'getOrganization-ResponseError-001', - 'javascript', - response.status, - response.statusText, - 'Failed to fetch organization details', - ); - } - - return (await response.json()) as OrganizationDetails; - } catch (error) { - if (error instanceof ThunderIDAPIError) { - throw error; - } - - throw new ThunderIDAPIError( - `Network or parsing error: ${error instanceof Error ? error.message : 'Unknown error'}`, - 'getOrganization-NetworkError-001', - 'javascript', - 0, - 'Network Error', - ); - } -}; - -export default getOrganization; diff --git a/packages/javascript/src/api/getOrganizationUnitChildren.ts b/packages/javascript/src/api/getOrganizationUnitChildren.ts index 0d8f22e..2e0d4c2 100644 --- a/packages/javascript/src/api/getOrganizationUnitChildren.ts +++ b/packages/javascript/src/api/getOrganizationUnitChildren.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -17,80 +17,149 @@ */ import ThunderIDAPIError from '../errors/ThunderIDAPIError'; -import {GetOrganizationUnitChildrenConfig, OrganizationUnitListResponse} from '../models/organization-unit'; /** - * Retrieves the child organization units of a given parent OU. - * - * @param config - Request configuration including `baseUrl`/`url`, `organizationUnitId`, - * and optional `limit`/`offset` pagination parameters. - * @returns A promise that resolves with the paginated list of child organization units. - * - * @throws {ThunderIDAPIError} When the server returns a non-OK response. - * - * @example - * ```typescript - * const children = await getOrganizationUnitChildren({ - * baseUrl: 'https://localhost:8090', - * organizationUnitId: '0d5e071b-d3d3-475d-b3c6-1a20ee2fa9b1', - * limit: 10, - * offset: 0, - * }); - * console.log(children.organizationUnits); - * ``` + * Represents a single organization unit entry. + */ +export interface OrganizationUnit { + /** Optional description of the organization unit. */ + description?: string; + /** Unique identifier of the organization unit. */ + id: string; + /** Human-readable name of the organization unit. */ + name: string; + /** Identifier of the parent organization unit, if any. */ + parentId?: string; + /** Status of the organization unit (e.g. ACTIVE). */ + status?: string; +} + +/** + * Response shape for the organization unit children list endpoint. + */ +export interface OrganizationUnitListResponse { + /** Number of items returned in this page. */ + count?: number; + /** List of child organization units. */ + organizationUnits: OrganizationUnit[]; + /** Pagination start index (1-based). */ + startIndex?: number; + /** Total number of matching organization units. */ + totalResults?: number; +} + +/** + * Configuration for the getOrganizationUnitChildren request. + */ +export interface GetOrganizationUnitChildrenConfig extends Omit { + /** + * The base URL of the ThunderID server. + * Used to derive the endpoint when `url` is not provided. + */ + baseUrl?: string; + /** + * Optional custom fetcher function. + * If not provided, native fetch will be used. + */ + fetcher?: (url: string, config: RequestInit) => Promise; + /** + * Maximum number of results to return. + */ + limit?: number; + /** + * Zero-based offset for pagination. + */ + offset?: number; + /** + * The identifier of the parent organization unit whose children to list. + */ + organizationUnitId: string; + /** + * The absolute API endpoint URL. + * When provided, `baseUrl` and `organizationUnitId` are not used for URL construction. + */ + url?: string; +} + +/** + * Retrieves the child organization units of a given organization unit. * - * @experimental This function targets the ThunderID V2 platform API + * @param config - Request configuration object. + * @returns A promise that resolves with the list of child organization units. */ const getOrganizationUnitChildren = async ({ url, baseUrl, organizationUnitId, - limit = 10, - offset = 0, + limit, + offset, + fetcher, ...requestConfig }: GetOrganizationUnitChildrenConfig): Promise => { - if (!organizationUnitId) { + try { + // eslint-disable-next-line no-new + new URL((url ?? baseUrl)!); + } catch (error) { throw new ThunderIDAPIError( - 'Organization Unit ID is required', + `Invalid URL provided. ${error?.toString()}`, 'getOrganizationUnitChildren-ValidationError-001', 'javascript', 400, - 'If an organization unit ID is not provided, the request cannot be constructed correctly.', + 'The provided `url` or `baseUrl` path does not adhere to the URL schema.', ); } - const queryParams: URLSearchParams = new URLSearchParams({ - limit: String(limit), - offset: String(offset), - }); + const fetchFn: typeof fetch = fetcher || fetch; - const endpoint: string = url ?? `${baseUrl}/organization-units/${organizationUnitId}/ous?${queryParams.toString()}`; + const queryParams = new URLSearchParams(); + if (limit !== undefined) queryParams.set('limit', String(limit)); + if (offset !== undefined) queryParams.set('offset', String(offset)); + const query = queryParams.toString(); - const response: Response = await fetch(endpoint, { + const resolvedBase = + url ?? `${baseUrl}/api/server/v1/organization-units/${organizationUnitId}/children`; + const resolvedUrl = query ? `${resolvedBase}?${query}` : resolvedBase; + + const requestInit: RequestInit = { ...requestConfig, headers: { Accept: 'application/json', + 'Content-Type': 'application/json', ...requestConfig.headers, }, method: 'GET', - }); + }; + + try { + const response: Response = await fetchFn(resolvedUrl, requestInit); - if (!response.ok) { - const errorText: string = await response.text(); + if (!response?.ok) { + const errorText: string = await response.text(); + + throw new ThunderIDAPIError( + errorText, + 'getOrganizationUnitChildren-ResponseError-001', + 'javascript', + response.status, + response.statusText, + 'Failed to fetch organization unit children', + ); + } + + return (await response.json()) as OrganizationUnitListResponse; + } catch (error) { + if (error instanceof ThunderIDAPIError) { + throw error; + } throw new ThunderIDAPIError( - errorText, - 'getOrganizationUnitChildren-ResponseError-001', + `Network or parsing error: ${error instanceof Error ? error.message : 'Unknown error'}`, + 'getOrganizationUnitChildren-NetworkError-001', 'javascript', - response.status, - response.statusText, - 'Failed to fetch organization unit children', + 0, + 'Network Error', ); } - - const listResponse: OrganizationUnitListResponse = await response.json(); - - return listResponse; }; export default getOrganizationUnitChildren; diff --git a/packages/javascript/src/api/updateOrganization.ts b/packages/javascript/src/api/updateOrganization.ts deleted file mode 100644 index ddeffd9..0000000 --- a/packages/javascript/src/api/updateOrganization.ts +++ /dev/null @@ -1,225 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {OrganizationDetails} from './getOrganization'; -import ThunderIDAPIError from '../errors/ThunderIDAPIError'; -import isEmpty from '../utils/isEmpty'; - -/** - * Configuration for the updateOrganization request - */ -export interface UpdateOrganizationConfig extends Omit { - /** - * The base URL for the API endpoint. - */ - baseUrl: string; - /** - * Optional custom fetcher function. - * If not provided, native fetch will be used - */ - fetcher?: (url: string, config: RequestInit) => Promise; - /** - * Array of patch operations to apply - */ - operations: { - operation: 'REPLACE' | 'ADD' | 'REMOVE'; - path: string; - value?: any; - }[]; - /** - * The ID of the organization to update - */ - organizationId: string; -} - -/** - * Updates the organization information using the Organizations Management API. - * - * @param config - Configuration object with baseUrl, organizationId, operations and optional request config. - * @returns A promise that resolves with the updated organization information. - * @example - * ```typescript - * // Using the helper function to create operations automatically - * const operations = createPatchOperations({ - * name: "Updated Organization Name", // Will use REPLACE - * description: "", // Will use REMOVE (empty string) - * customField: "Some value" // Will use REPLACE - * }); - * - * await updateOrganization({ - * baseUrl: "https://localhost:8090", - * organizationId: "0d5e071b-d3d3-475d-b3c6-1a20ee2fa9b1", - * operations - * }); - * - * // Or manually specify operations - * await updateOrganization({ - * baseUrl: "https://localhost:8090", - * organizationId: "0d5e071b-d3d3-475d-b3c6-1a20ee2fa9b1", - * operations: [ - * { operation: "REPLACE", path: "/name", value: "Updated Organization Name" }, - * { operation: "REMOVE", path: "/description" } - * ] - * }); - * ``` - * - * @example - * ```typescript - * // Using custom fetcher (e.g., axios-based httpClient) - * await updateOrganization({ - * baseUrl: "https://localhost:8090", - * organizationId: "0d5e071b-d3d3-475d-b3c6-1a20ee2fa9b1", - * operations: [ - * { operation: "REPLACE", path: "/name", value: "Updated Organization Name" } - * ], - * fetcher: async (url, config) => { - * const response = await httpClient({ - * url, - * method: config.method, - * headers: config.headers, - * data: config.body, - * ...config - * }); - * // Convert axios-like response to fetch-like Response - * return { - * ok: response.status >= 200 && response.status < 300, - * status: response.status, - * statusText: response.statusText, - * json: () => Promise.resolve(response.data), - * text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)) - * } as Response; - * } - * }); - * ``` - */ -const updateOrganization = async ({ - baseUrl, - organizationId, - operations, - fetcher, - ...requestConfig -}: UpdateOrganizationConfig): Promise => { - try { - // eslint-disable-next-line no-new - new URL(baseUrl); - } catch (error) { - throw new ThunderIDAPIError( - `Invalid base URL provided. ${error?.toString()}`, - 'updateOrganization-ValidationError-001', - 'javascript', - 400, - 'The provided `baseUrl` does not adhere to the URL schema.', - ); - } - - if (!organizationId) { - throw new ThunderIDAPIError( - 'Organization ID is required', - 'updateOrganization-ValidationError-002', - 'javascript', - 400, - 'Invalid Request', - ); - } - - if (!operations || !Array.isArray(operations) || operations.length === 0) { - throw new ThunderIDAPIError( - 'Operations array is required and cannot be empty', - 'updateOrganization-ValidationError-003', - 'javascript', - 400, - 'Invalid Request', - ); - } - - const fetchFn: typeof fetch = fetcher || fetch; - const resolvedUrl = `${baseUrl}/api/server/v1/organizations/${organizationId}`; - - const requestInit: RequestInit = { - ...requestConfig, - body: JSON.stringify(operations), - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - ...requestConfig.headers, - }, - method: 'PATCH', - }; - - try { - const response: Response = await fetchFn(resolvedUrl, requestInit); - - if (!response?.ok) { - const errorText: string = await response.text(); - - throw new ThunderIDAPIError( - errorText, - 'updateOrganization-ResponseError-001', - 'javascript', - response.status, - response.statusText, - 'Failed to update organization', - ); - } - - return (await response.json()) as OrganizationDetails; - } catch (error) { - if (error instanceof ThunderIDAPIError) { - throw error; - } - - throw new ThunderIDAPIError( - `Network or parsing error: ${error instanceof Error ? error.message : 'Unknown error'}`, - 'updateOrganization-NetworkError-001', - 'javascript', - 0, - 'Network Error', - ); - } -}; - -/** - * Helper function to convert field updates to patch operations format. - * Uses REMOVE operation when the value is empty, otherwise uses REPLACE. - * - * @param payload - Object containing field updates - * @returns Array of patch operations - */ -export const createPatchOperations = ( - payload: Record, -): { - operation: 'REPLACE' | 'REMOVE'; - path: string; - value?: any; -}[] => - Object.entries(payload).map(([key, value]: [string, any]) => { - if (isEmpty(value)) { - return { - operation: 'REMOVE' as const, - path: `/${key}`, - }; - } - - return { - operation: 'REPLACE' as const, - path: `/${key}`, - value, - }; - }); - -export default updateOrganization; diff --git a/packages/javascript/src/index.ts b/packages/javascript/src/index.ts index ad8d796..3a3a33b 100644 --- a/packages/javascript/src/index.ts +++ b/packages/javascript/src/index.ts @@ -25,21 +25,16 @@ export {default as executeEmbeddedUserOnboardingFlow} from './api/executeEmbedde export type {EmbeddedUserOnboardingFlowResponse} from './api/executeEmbeddedUserOnboardingFlow'; export {default as getFlowMeta} from './api/getFlowMeta'; export {default as getOrganizationUnitChildren} from './api/getOrganizationUnitChildren'; +export type { + GetOrganizationUnitChildrenConfig, + OrganizationUnit, + OrganizationUnitListResponse, +} from './api/getOrganizationUnitChildren'; export {default as getUserInfo} from './api/getUserInfo'; export {default as getScim2Me} from './api/getScim2Me'; export type {GetScim2MeConfig} from './api/getScim2Me'; export {default as getSchemas} from './api/getSchemas'; export type {GetSchemasConfig} from './api/getSchemas'; -export {default as getAllOrganizations} from './api/getAllOrganizations'; -export type {GetAllOrganizationsConfig} from './api/getAllOrganizations'; -export {default as createOrganization} from './api/createOrganization'; -export type {CreateOrganizationPayload, CreateOrganizationConfig} from './api/createOrganization'; -export {default as getMeOrganizations} from './api/getMeOrganizations'; -export type {GetMeOrganizationsConfig} from './api/getMeOrganizations'; -export {default as getOrganization} from './api/getOrganization'; -export type {OrganizationDetails, GetOrganizationConfig} from './api/getOrganization'; -export {default as updateOrganization, createPatchOperations} from './api/updateOrganization'; -export type {UpdateOrganizationConfig} from './api/updateOrganization'; export {default as updateMeProfile} from './api/updateMeProfile'; export type {UpdateMeProfileConfig} from './api/updateMeProfile'; @@ -55,7 +50,6 @@ export {ThunderIDAuthException} from './errors/exception'; export type {CIBAInitiateOptions, CIBAInitiateResponse, CIBAErrorCode, CIBAPollOptions} from './models/ciba'; -export type {AllOrganizationsApiResponse} from './models/organization'; export { EmbeddedFlowComponentType, EmbeddedFlowActionVariant, @@ -101,11 +95,6 @@ export type { EmbeddedRecoveryFlowRequest, EmbeddedRecoveryFlowErrorResponse, } from './models/embedded-recovery-flow'; -export type { - OrganizationUnit, - OrganizationUnitListResponse, - GetOrganizationUnitChildrenConfig, -} from './models/organization-unit'; export {FlowMetaType} from './models/flow-meta'; export type { ApplicationMetadata, @@ -163,7 +152,6 @@ export type {OIDCDiscoveryApiResponse} from './models/oidc-discovery'; export type {Storage, TemporaryStore} from './models/store'; export type {User, UserProfile} from './models/user'; export type {SessionData} from './models/session'; -export type {Organization} from './models/organization'; export type {TranslationFn} from './models/translation'; export type {ResolveFlowTemplateLiteralsOptions} from './models/vars'; export {WellKnownSchemaIds} from './models/scim2-schema'; diff --git a/packages/javascript/src/models/client.ts b/packages/javascript/src/models/client.ts index f65540c..6d8b1aa 100644 --- a/packages/javascript/src/models/client.ts +++ b/packages/javascript/src/models/client.ts @@ -18,7 +18,6 @@ import type {CIBAInitiateOptions, CIBAInitiateResponse, CIBAPollOptions} from './ciba'; import {SignInOptions, SignOutOptions, SignUpOptions} from './config'; -import {Organization, AllOrganizationsApiResponse} from './organization'; import {Storage} from './store'; import {TokenExchangeRequestConfig, TokenResponse} from './token'; import {User, UserProfile} from './user'; @@ -61,33 +60,12 @@ export interface ThunderIDClient { */ getAccessToken(sessionId?: string): Promise; - /** - * Gets all organizations available to the user. - * @param options - Optional parameters for the request. - * @param sessionId - Optional session ID to be used for the request. - */ - getAllOrganizations(options?: any, sessionId?: string): Promise; - /** * Gets the client configuration. * @returns The client configuration. */ getConfiguration(): T; - /** - * Gets the current organization of the user. - * - * @returns The current organization if available, otherwise null. - */ - getCurrentOrganization(sessionId?: string): Promise; - - /** - * Gets the current signed-in user's associated organizations. - * - * @returns Associated organizations. - */ - getMyOrganizations(options?: any, sessionId?: string): Promise; - /** * Gets user information from the session. * @@ -220,13 +198,6 @@ export interface ThunderIDClient { */ signUp(options?: SignUpOptions): Promise; - /** - * Switches the current organization to the specified one. - * @param organization - The organization to switch to. - * @returns A promise that resolves when the switch is complete. - */ - switchOrganization(organization: Organization, sessionId?: string): Promise; - /** * Updates the user profile with the provided payload. * @param payload - The new user profile data. diff --git a/packages/javascript/src/models/config.ts b/packages/javascript/src/models/config.ts index 3199396..d107a47 100644 --- a/packages/javascript/src/models/config.ts +++ b/packages/javascript/src/models/config.ts @@ -518,14 +518,6 @@ export interface I18nPreferences { } export interface UserPreferences { - /** - * Whether to automatically fetch the user's associated organizations after sign-in. - * When set to false, the SDK will not make API calls to `/api/users/v1/me/organizations`. - * @default true - * @remarks Disabling this will improve performance if you don't need organization information. - * You can manually call `getMyOrganizations()` when needed if this is disabled. - */ - fetchOrganizations?: boolean; /** * Whether to automatically fetch the user profile from SCIM2 endpoints after sign-in. * When set to false, the SDK will not make API calls to `/scim2/Me` and `/scim2/Schemas`. diff --git a/packages/javascript/src/models/organization-unit.ts b/packages/javascript/src/models/organization-unit.ts deleted file mode 100644 index 577de21..0000000 --- a/packages/javascript/src/models/organization-unit.ts +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export interface OrganizationUnit { - description?: string; - handle: string; - id: string; - logoUrl?: string; - name: string; - parent?: { - id: string; - ref?: string; - }; -} - -export interface OrganizationUnitListResponse { - count: number; - organizationUnits: OrganizationUnit[]; - startIndex: number; - totalResults: number; -} - -/** - * Request configuration for fetching a single organization unit. - * - * @example - * ```typescript - * const config: GetOrganizationUnitConfig = { - * baseUrl: 'https://localhost:8090', - * organizationUnitId: '0d5e071b-d3d3-475d-b3c6-1a20ee2fa9b1', - * }; - * ``` - * - * @experimental This API may change in future versions - */ -export interface GetOrganizationUnitConfig extends Omit, 'method' | 'body'> { - /** - * Base URL of the API server. - * Either `baseUrl` or `url` must be provided. - */ - baseUrl?: string; - - /** - * The ID of the organization unit to retrieve. - */ - organizationUnitId: string; - - /** - * Fully qualified URL of the organization unit endpoint. - * When provided, `baseUrl` is ignored. - */ - url?: string; -} - -/** - * Request configuration for fetching child organization units. - * - * @example - * ```typescript - * const config: GetOrganizationUnitChildrenConfig = { - * baseUrl: 'https://localhost:8090', - * organizationUnitId: '0d5e071b-d3d3-475d-b3c6-1a20ee2fa9b1', - * limit: 10, - * offset: 0, - * }; - * ``` - * - * @experimental This API may change in future versions - */ -export interface GetOrganizationUnitChildrenConfig extends Omit, 'method' | 'body'> { - /** - * Base URL of the API server. - * Either `baseUrl` or `url` must be provided. - */ - baseUrl?: string; - - /** - * Maximum number of child OUs to return. Defaults to 10. - */ - limit?: number; - - /** - * Pagination offset. Defaults to 0. - */ - offset?: number; - - /** - * The ID of the parent organization unit. - */ - organizationUnitId: string; - - /** - * Fully qualified URL of the organization unit children endpoint. - * When provided, `baseUrl` is ignored. - */ - url?: string; -} diff --git a/packages/javascript/src/models/organization.ts b/packages/javascript/src/models/organization.ts deleted file mode 100644 index fd166ca..0000000 --- a/packages/javascript/src/models/organization.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export interface Organization { - id: string; - name: string; - orgHandle: string; - ref?: string; - status?: string; -} - -/** - * Interface for paginated organization response. - */ -export interface AllOrganizationsApiResponse { - hasMore?: boolean; - nextCursor?: string; - organizations: Organization[]; - totalCount?: number; -} diff --git a/packages/nextjs/src/ThunderIDNextClient.ts b/packages/nextjs/src/ThunderIDNextClient.ts index 494a624..d27b18a 100644 --- a/packages/nextjs/src/ThunderIDNextClient.ts +++ b/packages/nextjs/src/ThunderIDNextClient.ts @@ -17,16 +17,12 @@ */ import { - AllOrganizationsApiResponse, ThunderIDNodeClient, ThunderIDRuntimeError, AuthClientConfig, - CreateOrganizationPayload, ExtendedAuthorizeRequestUrlParams, FlattenedSchema, IdToken, - Organization, - OrganizationDetails, Schema, SignInOptions, SignUpOptions, @@ -35,14 +31,10 @@ import { TokenResponse, User, UserProfile, - createOrganization, extractUserClaimsFromIdToken, flattenUserSchema, generateFlattenedUserProfile, generateUserProfile, - getAllOrganizations, - getMeOrganizations, - getOrganization, getScim2Me, getSchemas, updateMeProfile, @@ -217,141 +209,6 @@ class ThunderIDNextClient e } } - async createOrganization(payload: CreateOrganizationPayload, userId?: string): Promise { - try { - const configData: AuthClientConfig = await this.getStorageManager().getConfigData(); - const baseUrl: string = configData?.baseUrl!; - - return createOrganization({ - baseUrl, - headers: { - Authorization: `Bearer ${await this.getAccessToken(userId)}`, - }, - payload, - }); - } catch (error) { - throw new ThunderIDRuntimeError( - `Failed to create organization: ${error instanceof Error ? error.message : String(error)}`, - 'ThunderIDReactClient-createOrganization-RuntimeError-001', - 'nextjs', - 'An error occurred while creating the organization. Please check your configuration and network connection.', - ); - } - } - - async getOrganization(organizationId: string, userId?: string): Promise { - try { - const configData: AuthClientConfig = await this.getStorageManager().getConfigData(); - const baseUrl: string = configData?.baseUrl!; - - return getOrganization({ - baseUrl, - headers: { - Authorization: `Bearer ${await this.getAccessToken(userId)}`, - }, - organizationId, - }); - } catch (error) { - throw new ThunderIDRuntimeError( - `Failed to fetch the organization details of ${organizationId}: ${String(error)}`, - 'ThunderIDReactClient-getOrganization-RuntimeError-001', - 'nextjs', - `An error occurred while fetching the organization with the id: ${organizationId}.`, - ); - } - } - - override async getMyOrganizations(options?: any, userId?: string): Promise { - try { - const configData: AuthClientConfig = await this.getStorageManager().getConfigData(); - const baseUrl: string = configData?.baseUrl!; - - return getMeOrganizations({ - baseUrl, - headers: { - Authorization: `Bearer ${await this.getAccessToken(userId)}`, - }, - }); - } catch (error) { - throw new ThunderIDRuntimeError( - `Failed to fetch the user's associated organizations: ${ - error instanceof Error ? error.message : String(error) - }`, - 'ThunderIDNextClient-getMyOrganizations-RuntimeError-001', - 'nextjs', - 'An error occurred while fetching associated organizations of the signed-in user.', - ); - } - } - - override async getAllOrganizations(options?: any, userId?: string): Promise { - try { - const configData: AuthClientConfig = await this.getStorageManager().getConfigData(); - const baseUrl: string = configData?.baseUrl!; - - return getAllOrganizations({ - baseUrl, - headers: { - Authorization: `Bearer ${await this.getAccessToken(userId)}`, - }, - }); - } catch (error) { - throw new ThunderIDRuntimeError( - `Failed to fetch all organizations: ${error instanceof Error ? error.message : String(error)}`, - 'ThunderIDNextClient-getAllOrganizations-RuntimeError-001', - 'nextjs', - 'An error occurred while fetching all the organizations associated with the user.', - ); - } - } - - override async getCurrentOrganization(userId?: string): Promise { - const idToken: IdToken = await super.getDecodedIdToken(userId); - - return { - id: idToken?.org_id!, - name: idToken?.org_name!, - orgHandle: idToken?.org_handle!, - }; - } - - override async switchOrganization(organization: Organization, userId?: string): Promise { - try { - if (!organization.id) { - throw new ThunderIDRuntimeError( - 'Organization ID is required for switching organizations', - 'ThunderIDNextClient-switchOrganization-ValidationError-001', - 'nextjs', - 'The organization object must contain a valid ID to perform the organization switch.', - ); - } - - const exchangeConfig: TokenExchangeRequestConfig = { - attachToken: false, - data: { - client_id: '{{clientId}}', - client_secret: '{{clientSecret}}', - grant_type: 'organization_switch', - scope: '{{scopes}}', - switching_organization: organization.id, - token: '{{accessToken}}', - }, - id: 'organization-switch', - returnsSession: true, - signInRequired: true, - }; - - return super.exchangeToken(exchangeConfig, userId) as unknown as Promise; - } catch (error) { - throw new ThunderIDRuntimeError( - `Failed to switch organization: ${error instanceof Error ? error.message : String(JSON.stringify(error))}`, - 'ThunderIDReactClient-RuntimeError-003', - 'nextjs', - 'An error occurred while switching to the specified organization. Please try again.', - ); - } - } - override isLoading(): boolean { return false; } diff --git a/packages/nextjs/src/client/components/presentation/CreateOrganization/CreateOrganization.tsx b/packages/nextjs/src/client/components/presentation/CreateOrganization/CreateOrganization.tsx deleted file mode 100644 index 6b68d9b..0000000 --- a/packages/nextjs/src/client/components/presentation/CreateOrganization/CreateOrganization.tsx +++ /dev/null @@ -1,159 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -'use client'; - -import {CreateOrganizationPayload, ThunderIDRuntimeError} from '@thunderid/node'; -import {BaseCreateOrganization, BaseCreateOrganizationProps, useOrganization} from '@thunderid/react'; -import {FC, ReactElement, useState} from 'react'; -import getSessionId from '../../../../server/actions/getSessionId'; -import useThunderID from '../../../contexts/ThunderID/useThunderID'; - -/** - * Props interface for the CreateOrganization component. - */ -export interface CreateOrganizationProps extends Omit { - /** - * Fallback element to render when the user is not signed in. - */ - fallback?: ReactElement; - /** - * Custom organization creation handler (will use default API if not provided). - */ - onCreateOrganization?: (payload: CreateOrganizationPayload) => Promise; -} - -/** - * CreateOrganization component that provides organization creation functionality. - * This component automatically integrates with the ThunderID and Organization contexts. - * - * @example - * ```tsx - * import { CreateOrganization } from '@thunderid/react'; - * - * // Basic usage - uses default API and contexts - * console.log('Created:', org)} - * onCancel={() => navigate('/organizations')} - * /> - * - * // With custom organization creation handler - * { - * const result = await myCustomAPI.createOrganization(payload); - * return result; - * }} - * onSuccess={(org) => { - * console.log('Organization created:', org.name); - * // Custom success logic here - * }} - * /> - * - * // With fallback for unauthenticated users - * Please sign in to create an organization} - * /> - * ``` - */ -export const CreateOrganization: FC = ({ - onCreateOrganization, - fallback = <>, - onSuccess, - defaultParentId, - ...props -}: CreateOrganizationProps): ReactElement => { - const {isSignedIn, baseUrl} = useThunderID(); - const {currentOrganization, revalidateMyOrganizations, createOrganization} = useOrganization(); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - // Don't render if not authenticated - if (!isSignedIn && fallback) { - return fallback; - } - - if (!isSignedIn) { - return <>; - } - - // Use current organization as parent if no defaultParentId provided - const parentId: string = defaultParentId || currentOrganization?.id || ''; - - const handleSubmit = async (payload: CreateOrganizationPayload): Promise => { - setLoading(true); - setError(null); - - try { - let result: any; - - if (onCreateOrganization) { - result = await onCreateOrganization(payload); - } else { - if (!baseUrl) { - throw new Error('Base URL is required for organization creation'); - } - - if (!createOrganization) { - throw new ThunderIDRuntimeError( - `createOrganization function is not available.`, - 'CreateOrganization-handleSubmit-RuntimeError-001', - 'nextjs', - 'The createOrganization function must be provided by the Organization context.', - ); - } - - result = await createOrganization( - { - ...payload, - parentId, - }, - (await getSessionId())!, - ); - } - - // Refresh organizations list to include the new organization - if (revalidateMyOrganizations) { - await revalidateMyOrganizations(); - } - - // Call success callback if provided - if (onSuccess) { - onSuccess(result); - } - } catch (createError) { - const errorMessage: string = createError instanceof Error ? createError.message : 'Failed to create organization'; - setError(errorMessage); - throw createError; // Re-throw to allow form to handle it - } finally { - setLoading(false); - } - }; - - return ( - - ); -}; - -export default CreateOrganization; diff --git a/packages/nextjs/src/client/components/presentation/Organization/Organization.tsx b/packages/nextjs/src/client/components/presentation/Organization/Organization.tsx deleted file mode 100644 index b76815a..0000000 --- a/packages/nextjs/src/client/components/presentation/Organization/Organization.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -'use client'; - -import {Organization as IOrganization} from '@thunderid/node'; -import {BaseOrganization, BaseOrganizationProps, useOrganization} from '@thunderid/react'; -import {FC, ReactElement, ReactNode} from 'react'; - -/** - * Props for the Organization component. - * Extends BaseOrganizationProps but makes the organization prop optional since it will be obtained from useOrganization - */ -export interface OrganizationProps extends Omit { - /** - * Render prop that takes the organization object and returns a ReactNode. - * @param organization - The current organization object from Organization context. - * @returns A ReactNode to render. - */ - children: (organization: IOrganization | null) => ReactNode; - - /** - * Optional element to render when no organization is selected. - */ - fallback?: ReactNode; -} - -/** - * A component that uses render props to expose the current organization object. - * This component automatically retrieves the current organization from Organization context. - * - * @remarks This component is only supported in browser based React applications (CSR). - * - * @example - * ```tsx - * import { Organization } from '@thunderid/auth-react'; - * - * const App = () => { - * return ( - * No organization selected

}> - * {(organization) => ( - *
- *

Current Organization: {organization.name}!

- *

ID: {organization.id}

- *

Role: {organization.role}

- * {organization.memberCount && ( - *

Members: {organization.memberCount}

- * )} - *
- * )} - *
- * ); - * } - * ``` - */ -const Organization: FC = ({children, fallback = null}: OrganizationProps): ReactElement => { - const {currentOrganization} = useOrganization(); - - return ( - - {children} - - ); -}; - -Organization.displayName = 'Organization'; - -export default Organization; diff --git a/packages/nextjs/src/client/components/presentation/OrganizationList/OrganizationList.tsx b/packages/nextjs/src/client/components/presentation/OrganizationList/OrganizationList.tsx deleted file mode 100644 index f9f063d..0000000 --- a/packages/nextjs/src/client/components/presentation/OrganizationList/OrganizationList.tsx +++ /dev/null @@ -1,136 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -'use client'; - -import {AllOrganizationsApiResponse} from '@thunderid/node'; -import { - BaseOrganizationListProps, - BaseOrganizationList, - useOrganization, - OrganizationWithSwitchAccess, -} from '@thunderid/react'; -import {FC, ReactElement, useEffect, useState} from 'react'; - -/** - * Configuration options for the OrganizationList component. - */ -export interface OrganizationListConfig { - /** - * Whether to automatically fetch organizations on mount - */ - autoFetch?: boolean; - /** - * Filter string for organizations - */ - filter?: string; - /** - * Number of organizations to fetch per page - */ - limit?: number; - /** - * Whether to include recursive organizations - */ - recursive?: boolean; -} - -/** - * Props interface for the OrganizationList component. - * Uses the enhanced OrganizationContext instead of the useOrganizations hook. - */ -export interface OrganizationListProps - extends Omit< - BaseOrganizationListProps, - 'allOrganizations' | 'error' | 'fetchMore' | 'hasMore' | 'isLoading' | 'isLoadingMore' | 'myOrganizations' - >, - OrganizationListConfig { - /** - * Function called when an organization is selected/clicked - */ - onOrganizationSelect?: (organization: OrganizationWithSwitchAccess) => void; -} - -/** - * OrganizationList component that provides organization listing functionality with pagination. - * This component uses the enhanced OrganizationContext, eliminating the polling issue and - * providing better integration with the existing context system. - * - * @example - * ```tsx - * import { OrganizationList } from '@thunderid/react'; - * - * // Basic usage - * - * - * // With custom limit and filter - * { - * console.log('Selected organization:', org.name); - * }} - * /> - * - * // As a popup dialog - * - * - * // With custom organization renderer - * ( - *
- *

{org.name}

- *

Can switch: {org.canSwitch ? 'Yes' : 'No'}

- *
- * )} - * /> - * ``` - */ -export const OrganizationList: FC = ({ - onOrganizationSelect, - ...baseProps -}: OrganizationListProps): ReactElement => { - const {getAllOrganizations, error, isLoading, myOrganizations} = useOrganization(); - - const [allOrganizations, setAllOrganizations] = useState({ - organizations: [], - }); - - useEffect(() => { - (async (): Promise => { - setAllOrganizations(await getAllOrganizations()); - })(); - }, []); - - return ( - - ); -}; - -export default OrganizationList; diff --git a/packages/nextjs/src/client/components/presentation/OrganizationProfile/OrganizationProfile.tsx b/packages/nextjs/src/client/components/presentation/OrganizationProfile/OrganizationProfile.tsx deleted file mode 100644 index f8bfbc2..0000000 --- a/packages/nextjs/src/client/components/presentation/OrganizationProfile/OrganizationProfile.tsx +++ /dev/null @@ -1,221 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -'use client'; - -import {OrganizationDetails, updateOrganization, createPatchOperations} from '@thunderid/node'; -import {BaseOrganizationProfile, BaseOrganizationProfileProps, useTranslation} from '@thunderid/react'; -import {FC, ReactElement, useEffect, useState} from 'react'; -import getOrganizationAction from '../../../../server/actions/getOrganizationAction'; -import getSessionId from '../../../../server/actions/getSessionId'; -import logger from '../../../../utils/logger'; -import useThunderID from '../../../contexts/ThunderID/useThunderID'; - -/** - * Props for the OrganizationProfile component. - * Extends BaseOrganizationProfileProps but makes the organization prop optional - * since it will be fetched using the organizationId - */ -export type OrganizationProfileProps = Omit & { - /** - * Component to show when there's an error loading organization data. - */ - errorFallback?: ReactElement; - - /** - * Component to show while loading organization data. - */ - loadingFallback?: ReactElement; - - /** - * Display mode for the component. - */ - mode?: 'default' | 'popup'; - - /** - * Callback fired when the popup should be closed (only used in popup mode). - */ - onOpenChange?: (open: boolean) => void; - - /** - * Callback fired when the organization should be updated. - */ - onUpdate?: (payload: any) => Promise; - - /** - * Whether the popup is open (only used in popup mode). - */ - open?: boolean; - - /** - * The ID of the organization to fetch and display. - */ - organizationId: string; - - /** - * Custom title for the popup dialog (only used in popup mode). - */ - popupTitle?: string; -}; - -/** - * OrganizationProfile component displays organization information in a - * structured and styled format. It automatically fetches organization details - * using the provided organization ID and displays them using BaseOrganizationProfile. - * - * The component supports editing functionality, allowing users to modify organization - * fields inline. Updates are automatically synced with the backend via the SCIM2 API. - * - * This component is the React-specific implementation that automatically - * retrieves the organization data from ThunderID API. - * - * @example - * ```tsx - * // Basic usage with editing enabled (default) - * - * - * // Read-only mode - * - * - * // With card layout and custom fallbacks - * Loading organization...} - * errorFallback={
Failed to load organization
} - * fallback={
No organization data available
} - * /> - * - * // With custom fields configuration and update callback - * value || 'No description' }, - * { key: 'created', label: 'Created Date', editable: false, render: (value) => new Date(value).toLocaleDateString() }, - * { key: 'lastModified', label: 'Last Modified Date', editable: false, render: (value) => new Date(value).toLocaleDateString() }, - * { key: 'attributes', label: 'Custom Attributes', editable: true } - * ]} - * onUpdate={async (payload) => { - * console.log('Organization updated:', payload); - * // payload contains the updated field values - * // The component automatically converts these to patch operations - * }} - * /> - * - * // In popup mode - * - * ``` - */ -const OrganizationProfile: FC = ({ - organizationId, - mode = 'default', - open = false, - onOpenChange, - onUpdate, - popupTitle, - ...rest -}: OrganizationProfileProps): ReactElement => { - const {baseUrl} = useThunderID(); - const {t} = useTranslation(); - const [organization, setOrganization] = useState(null); - const [, setLoading] = useState(true); - const [, setError] = useState(false); - - const fetchOrganization = async (): Promise => { - if (!baseUrl || !organizationId) { - setLoading(false); - setError(true); - return; - } - - try { - setLoading(true); - setError(false); - const result: {data?: {organization?: OrganizationDetails}; error: string | null; success: boolean} = - await getOrganizationAction(organizationId, (await getSessionId())!); - - if (result.data?.organization) { - setOrganization(result.data.organization); - - return; - } - - setError(true); - } catch (err) { - logger.error('Failed to fetch organization:', err); - setError(true); - setOrganization(null); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - fetchOrganization(); - }, [baseUrl, organizationId]); - - const handleOrganizationUpdate = async (payload: any): Promise => { - if (!baseUrl || !organizationId) return; - - try { - // Convert payload to patch operations format - const operations: {operation: 'REPLACE' | 'REMOVE'; path: string; value?: any}[] = createPatchOperations(payload); - - await updateOrganization({ - baseUrl, - operations, - organizationId, - }); - // Refetch organization data after update - await fetchOrganization(); - - // Call the optional onUpdate callback - if (onUpdate) { - await onUpdate(payload); - } - } catch (err) { - logger.error('Failed to update organization:', err); - throw err; - } - }; - - return ( - - ); -}; - -export default OrganizationProfile; diff --git a/packages/nextjs/src/client/components/presentation/OrganizationSwitcher/OrganizationSwitcher.tsx b/packages/nextjs/src/client/components/presentation/OrganizationSwitcher/OrganizationSwitcher.tsx deleted file mode 100644 index 198d628..0000000 --- a/packages/nextjs/src/client/components/presentation/OrganizationSwitcher/OrganizationSwitcher.tsx +++ /dev/null @@ -1,199 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -'use client'; - -import {Organization} from '@thunderid/node'; -import { - BaseOrganizationSwitcher, - BaseOrganizationSwitcherProps, - BuildingAlt, - useOrganization, - useTranslation, -} from '@thunderid/react'; -import {FC, ReactElement, useState} from 'react'; -import useThunderID from '../../../contexts/ThunderID/useThunderID'; -import {CreateOrganization} from '../CreateOrganization/CreateOrganization.js'; -import OrganizationList from '../OrganizationList/OrganizationList.js'; -import OrganizationProfile from '../OrganizationProfile/OrganizationProfile.js'; - -/** - * Props interface for the OrganizationSwitcher component. - * Makes organizations optional since they'll be retrieved from OrganizationContext. - */ -export interface OrganizationSwitcherProps - extends Omit { - /** - * Optional override for current organization (will use context if not provided) - */ - currentOrganization?: Organization; - /** - * Fallback element to render when the user is not signed in. - */ - fallback?: ReactElement; - /** - * Optional callback for organization switch (will use context if not provided) - */ - onOrganizationSwitch?: (organization: Organization) => Promise | void; - /** - * Optional override for organizations list (will use context if not provided) - */ - organizations?: Organization[]; -} - -/** - * OrganizationSwitcher component that provides organization switching functionality. - * This component automatically retrieves organizations from the OrganizationContext. - * You can also override the organizations, currentOrganization, and onOrganizationSwitch - * by passing them as props. - * - * @example - * ```tsx - * import { OrganizationSwitcher } from '@thunderid/react'; - * - * // Basic usage - uses OrganizationContext - * - * - * // With custom organization switch handler - * { - * console.log('Switching to:', org.name); - * // Custom logic here - * }} - * /> - * - * // With fallback for unauthenticated users - * Please sign in to view organizations} - * /> - * ``` - */ -export const OrganizationSwitcher: FC = ({ - currentOrganization: propCurrentOrganization, - fallback = <>, - onOrganizationSwitch: propOnOrganizationSwitch, - organizations: propOrganizations, - ...props -}: OrganizationSwitcherProps): ReactElement => { - const {isSignedIn} = useThunderID(); - const { - currentOrganization: contextCurrentOrganization, - myOrganizations: contextOrganizations, - switchOrganization, - isLoading, - error, - } = useOrganization(); - const [isCreateOrgOpen, setIsCreateOrgOpen] = useState(false); - const [isProfileOpen, setIsProfileOpen] = useState(false); - const [isOrganizationListOpen, setIsOrganizationListOpen] = useState(false); - const {t} = useTranslation(); - - if (!isSignedIn && fallback) { - return fallback; - } - - if (!isSignedIn) { - return <>; - } - - const organizations: Organization[] = propOrganizations || contextOrganizations || []; - const currentOrganization: Organization = propCurrentOrganization || contextCurrentOrganization!; - const onOrganizationSwitch: (organization: Organization) => void = propOnOrganizationSwitch || switchOrganization; - - const handleManageOrganizations = (): void => { - setIsOrganizationListOpen(true); - }; - - const handleManageOrganization = (): void => { - setIsProfileOpen(true); - }; - - const defaultMenuItems: {icon?: ReactElement; label: string; onClick: () => void}[] = []; - - if (currentOrganization) { - defaultMenuItems.push({ - icon: , - label: t('organization.switcher.manage.organizations'), - onClick: handleManageOrganizations, - }); - } - - defaultMenuItems.push({ - icon: ( - - - - ), - label: t('organization.switcher.create.organization'), - onClick: (): void => setIsCreateOrgOpen(true), - }); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const menuItems: any[] = props.menuItems ? [...defaultMenuItems, ...props.menuItems] : defaultMenuItems; - - return ( - <> - - { - if (org && onOrganizationSwitch) { - onOrganizationSwitch(org); - } - setIsCreateOrgOpen(false); - }} - /> - {currentOrganization && ( - {t('organization.profile.loading')}} - errorFallback={
{t('organization.profile.error')}
} - /> - )} - { - if (onOrganizationSwitch) { - onOrganizationSwitch(organization); - } - setIsOrganizationListOpen(false); - }} - /> - - ); -}; - -export default OrganizationSwitcher; diff --git a/packages/nextjs/src/client/contexts/ThunderID/ThunderIDProvider.tsx b/packages/nextjs/src/client/contexts/ThunderID/ThunderIDProvider.tsx index 0cd013c..0624ed9 100644 --- a/packages/nextjs/src/client/contexts/ThunderID/ThunderIDProvider.tsx +++ b/packages/nextjs/src/client/contexts/ThunderID/ThunderIDProvider.tsx @@ -19,15 +19,11 @@ 'use client'; import { - AllOrganizationsApiResponse, EmbeddedFlowExecuteRequestConfig, generateFlattenedUserProfile, - Organization, UpdateMeProfileConfig, User, UserProfile, - TokenResponse, - CreateOrganizationPayload, ThunderIDRuntimeError, } from '@thunderid/node'; import { @@ -37,7 +33,6 @@ import { UserProvider, ThemeProvider, ThunderIDProviderProps, - OrganizationProvider, getActiveTheme, } from '@thunderid/react'; import {ReadonlyURLSearchParams} from 'next/dist/client/components/navigation.react-server'; @@ -55,23 +50,17 @@ export type ThunderIDClientProviderProps = Partial & { applicationId: ThunderIDContextProps['applicationId']; clearSession: () => Promise; - createOrganization: (payload: CreateOrganizationPayload, sessionId: string) => Promise; - currentOrganization: Organization; - getAllOrganizations: (options?: any, sessionId?: string) => Promise; handleOAuthCallback: ( code: string, state: string, sessionState?: string, ) => Promise<{error?: string; redirectUrl?: string; success: boolean}>; isSignedIn: boolean; - myOrganizations: Organization[]; organizationHandle: ThunderIDContextProps['organizationHandle']; refreshToken: () => Promise; - revalidateMyOrganizations?: (sessionId?: string) => Promise; signIn: ThunderIDContextProps['signIn']; signOut: ThunderIDContextProps['signOut']; signUp: ThunderIDContextProps['signUp']; - switchOrganization: (organization: Organization, sessionId?: string) => Promise; updateProfile: ( requestConfig: UpdateMeProfileConfig, sessionId?: string, @@ -89,22 +78,16 @@ const ThunderIDClientProvider: FC) => { const reRenderCheckRef: RefObject = useRef(false); const router: AppRouterInstance = useRouter(); @@ -325,16 +308,7 @@ const ThunderIDClientProvider: FC - - {children} - + {children}
diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index fde41e4..39c3a01 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -18,23 +18,11 @@ export {default as useThunderID} from './contexts/ThunderID/useThunderID'; -export {default as Organization} from './components/presentation/Organization/Organization'; -export {OrganizationProps} from './components/presentation/Organization/Organization'; - -export {default as CreateOrganization} from './components/presentation/CreateOrganization/CreateOrganization'; -export {CreateOrganizationProps} from './components/presentation/CreateOrganization/CreateOrganization'; - -export {default as OrganizationProfile} from './components/presentation/OrganizationProfile/OrganizationProfile'; -export {OrganizationProfileProps} from './components/presentation/OrganizationProfile/OrganizationProfile'; - -export {default as OrganizationSwitcher} from './components/presentation/OrganizationSwitcher/OrganizationSwitcher'; -export {OrganizationSwitcherProps} from './components/presentation/OrganizationSwitcher/OrganizationSwitcher'; - export {default as SignedIn} from './components/control/SignedIn/SignedIn'; -export {SignedInProps} from './components/control/SignedIn/SignedIn'; +export type {SignedInProps} from './components/control/SignedIn/SignedIn'; export {default as SignedOut} from './components/control/SignedOut/SignedOut'; -export {SignedOutProps} from './components/control/SignedOut/SignedOut'; +export type {SignedOutProps} from './components/control/SignedOut/SignedOut'; export {default as SignInButton} from './components/actions/SignInButton/SignInButton'; export type {SignInButtonProps} from './components/actions/SignInButton/SignInButton'; diff --git a/packages/nextjs/src/server/ThunderIDProvider.tsx b/packages/nextjs/src/server/ThunderIDProvider.tsx index 7579289..e10c6eb 100644 --- a/packages/nextjs/src/server/ThunderIDProvider.tsx +++ b/packages/nextjs/src/server/ThunderIDProvider.tsx @@ -18,14 +18,10 @@ 'use server'; -import {ThunderIDRuntimeError, IdToken, Organization, User, UserProfile} from '@thunderid/node'; +import {ThunderIDRuntimeError, IdToken, User, UserProfile} from '@thunderid/node'; import {ThunderIDProviderProps} from '@thunderid/react'; import {FC, PropsWithChildren, ReactElement} from 'react'; import clearSession from './actions/clearSession'; -import createOrganization from './actions/createOrganization'; -import getAllOrganizations from './actions/getAllOrganizations'; -import getCurrentOrganizationAction from './actions/getCurrentOrganizationAction'; -import getMyOrganizations from './actions/getMyOrganizations'; import getSessionId from './actions/getSessionId'; import getSessionPayload from './actions/getSessionPayload'; import getUserAction from './actions/getUserAction'; @@ -36,7 +32,6 @@ import refreshToken from './actions/refreshToken'; import signInAction from './actions/signInAction'; import signOutAction from './actions/signOutAction'; import signUpAction from './actions/signUpAction'; -import switchOrganization from './actions/switchOrganization'; import updateUserProfileAction from './actions/updateUserProfileAction'; import getClient from './getClient'; import ThunderIDClientProvider from '../client/contexts/ThunderID/ThunderIDProvider.js'; @@ -122,13 +117,6 @@ const ThunderIDServerProvider: FC}; - error: string | null; - success: boolean; - } = await getCurrentOrganizationAction(sessionId); - - if (sessionId) { - myOrganizations = await getMyOrganizations({}, sessionId); - } else { - logger.warn('[ThunderIDServerProvider] No session ID available, skipping organization fetch'); - } - - currentOrganization = currentOrganizationResponse?.data?.organization!; - } catch (error) { - logger.warn('[ThunderIDServerProvider] Failed to fetch organization info:', error?.toString()); - } - } } return ( @@ -209,14 +175,9 @@ const ThunderIDServerProvider: FC {children} diff --git a/packages/nextjs/src/server/actions/__tests__/createOrganization.test.ts b/packages/nextjs/src/server/actions/__tests__/createOrganization.test.ts deleted file mode 100644 index 913a549..0000000 --- a/packages/nextjs/src/server/actions/__tests__/createOrganization.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {ThunderIDAPIError, Organization, CreateOrganizationPayload} from '@thunderid/node'; -import {describe, it, expect, vi, beforeEach, afterEach, Mock} from 'vitest'; - -// Adjust these paths if your project structure is different -import getClient from '../../getClient'; -import createOrganization from '../createOrganization'; - -// Use the same class so we can assert instanceof and status code propagation - -// Pull the mocked modules so we can access their spies -import getSessionId from '../getSessionId'; - -// ---- Mocks ---- -vi.mock('../../getClient', () => ({ - default: vi.fn(), -})); - -vi.mock('../getSessionId', () => ({ - default: vi.fn(), -})); - -describe('createOrganization (Next.js server action)', () => { - const mockClient: {createOrganization: ReturnType} = { - createOrganization: vi.fn(), - }; - - const basePayload: CreateOrganizationPayload = { - description: 'Screen sharing organization', - name: 'Team Viewer', - orgHandle: 'team-viewer', - parentId: 'parent-123', - type: 'TENANT', - }; - - const mockOrg: Organization = { - id: 'org-001', - name: 'Team Viewer', - orgHandle: 'team-viewer', - }; - - beforeEach(() => { - vi.resetAllMocks(); - - // Default: getInstance returns our mock client - (getClient as unknown as Mock).mockReturnValue(mockClient); - // Default: getSessionId resolves to a session id - (getSessionId as unknown as Mock).mockResolvedValue('sess-abc'); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - it('should create an organization successfully when a sessionId is provided', async () => { - mockClient.createOrganization.mockResolvedValueOnce(mockOrg); - - const result: Organization = await createOrganization(basePayload, 'sess-123'); - - expect(getClient).toHaveBeenCalledTimes(1); - expect(getSessionId).not.toHaveBeenCalled(); - expect(mockClient.createOrganization).toHaveBeenCalledWith(basePayload, 'sess-123'); - expect(result).toEqual(mockOrg); - }); - - it('should fall back to getSessionId when sessionId is undefined', async () => { - mockClient.createOrganization.mockResolvedValueOnce(mockOrg); - - const result: Organization = await createOrganization(basePayload, undefined as unknown as string); - - expect(getSessionId).toHaveBeenCalledTimes(1); - expect(mockClient.createOrganization).toHaveBeenCalledWith(basePayload, 'sess-abc'); - expect(result).toEqual(mockOrg); - }); - - it('should fall back to getSessionId when sessionId is null', async () => { - mockClient.createOrganization.mockResolvedValueOnce(mockOrg); - - const result: Organization = await createOrganization(basePayload, null as unknown as string); - - expect(getSessionId).toHaveBeenCalledTimes(1); - expect(mockClient.createOrganization).toHaveBeenCalledWith(basePayload, 'sess-abc'); - expect(result).toEqual(mockOrg); - }); - - it('should not call getSessionId when an empty string is passed (empty string is not nullish)', async () => { - mockClient.createOrganization.mockResolvedValueOnce(mockOrg); - - const result: Organization = await createOrganization(basePayload, ''); - - expect(getSessionId).not.toHaveBeenCalled(); - expect(mockClient.createOrganization).toHaveBeenCalledWith(basePayload, ''); - expect(result).toEqual(mockOrg); - }); - - it('should wrap an ThunderIDAPIError thrown by client.createOrganization, preserving statusCode', async () => { - const original: ThunderIDAPIError = new ThunderIDAPIError( - 'Upstream validation failed', - 'ORG_CREATE_400', - 'server', - 400, - ); - mockClient.createOrganization.mockRejectedValueOnce(original); - - await expect(createOrganization(basePayload, 'sess-1')).rejects.toMatchObject({ - constructor: ThunderIDAPIError, - message: expect.stringContaining('Failed to create the organization: Upstream validation failed'), - statusCode: 400, - }); - }); -}); diff --git a/packages/nextjs/src/server/actions/__tests__/getAllOrganizations.test.ts b/packages/nextjs/src/server/actions/__tests__/getAllOrganizations.test.ts deleted file mode 100644 index 9171fb8..0000000 --- a/packages/nextjs/src/server/actions/__tests__/getAllOrganizations.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// src/server/actions/__tests__/getAllOrganizations.test.ts -import {ThunderIDAPIError, AllOrganizationsApiResponse} from '@thunderid/node'; -import {describe, it, expect, vi, beforeEach, afterEach, Mock} from 'vitest'; - -// --- Now import the SUT and mocked deps --- -import getClient from '../../getClient'; -import getAllOrganizations from '../getAllOrganizations'; -import getSessionId from '../getSessionId'; - -// --- Mocks MUST be defined before importing the SUT --- -vi.mock('../../getClient', () => ({ - default: vi.fn(), -})); - -vi.mock('../getSessionId', () => ({ - default: vi.fn(), -})); - -describe('getAllOrganizations (Next.js server action)', () => { - const mockClient: {getAllOrganizations: ReturnType} = { - getAllOrganizations: vi.fn(), - }; - - const baseOptions: {cursor: string; filter: string; limit: number} = { - cursor: 'cur-1', - filter: 'type eq "TENANT"', - limit: 50, - }; - - const mockResponse: AllOrganizationsApiResponse = { - data: [ - {id: 'org-001', name: 'Alpha', orgHandle: 'alpha'}, - {id: 'org-002', name: 'Beta', orgHandle: 'beta'}, - ], - meta: {itemsPerPage: 2, startIndex: 1, totalResults: 2}, - } as unknown as AllOrganizationsApiResponse; - - beforeEach(() => { - vi.resetAllMocks(); - - // Default: getInstance returns our mock client - (getClient as unknown as Mock).mockReturnValue(mockClient); - // Default: session id resolver returns a value - (getSessionId as unknown as Mock).mockResolvedValue('sess-abc'); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - it('returns organizations when a sessionId is provided (no getSessionId fallback)', async () => { - mockClient.getAllOrganizations.mockResolvedValueOnce(mockResponse); - - const result: AllOrganizationsApiResponse = await getAllOrganizations(baseOptions, 'sess-123'); - - expect(getClient).toHaveBeenCalledTimes(1); - expect(getSessionId).not.toHaveBeenCalled(); - expect(mockClient.getAllOrganizations).toHaveBeenCalledWith(baseOptions, 'sess-123'); - expect(result).toBe(mockResponse); - }); - - it('falls back to getSessionId when sessionId is undefined', async () => { - mockClient.getAllOrganizations.mockResolvedValueOnce(mockResponse); - - const result: AllOrganizationsApiResponse = await getAllOrganizations(baseOptions, undefined); - - expect(getSessionId).toHaveBeenCalledTimes(1); - expect(mockClient.getAllOrganizations).toHaveBeenCalledWith(baseOptions, 'sess-abc'); - expect(result).toBe(mockResponse); - }); - - it('falls back to getSessionId when sessionId is null', async () => { - mockClient.getAllOrganizations.mockResolvedValueOnce(mockResponse); - - const result: AllOrganizationsApiResponse = await getAllOrganizations(baseOptions, null as unknown as string); - - expect(getSessionId).toHaveBeenCalledTimes(1); - expect(mockClient.getAllOrganizations).toHaveBeenCalledWith(baseOptions, 'sess-abc'); - expect(result).toBe(mockResponse); - }); - - it('does not call getSessionId for an empty string sessionId (empty string is not nullish)', async () => { - mockClient.getAllOrganizations.mockResolvedValueOnce(mockResponse); - - const result: AllOrganizationsApiResponse = await getAllOrganizations(baseOptions, ''); - - expect(getSessionId).not.toHaveBeenCalled(); - expect(mockClient.getAllOrganizations).toHaveBeenCalledWith(baseOptions, ''); - expect(result).toBe(mockResponse); - }); - - it('wraps an ThunderIDAPIError thrown by client.getAllOrganizations, preserving statusCode', async () => { - const upstream: ThunderIDAPIError = new ThunderIDAPIError('Upstream failed', 'ORG_LIST_500', 'server', 503); - mockClient.getAllOrganizations.mockRejectedValueOnce(upstream); - - await expect(getAllOrganizations(baseOptions, 'sess-x')).rejects.toMatchObject({ - constructor: ThunderIDAPIError, - message: expect.stringContaining('Failed to get all the organizations for the user: Upstream failed'), - statusCode: 503, - }); - }); -}); diff --git a/packages/nextjs/src/server/actions/__tests__/getCurrentOrganizationAction.test.ts b/packages/nextjs/src/server/actions/__tests__/getCurrentOrganizationAction.test.ts deleted file mode 100644 index aa1bc92..0000000 --- a/packages/nextjs/src/server/actions/__tests__/getCurrentOrganizationAction.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// src/server/actions/__tests__/getCurrentOrganizationAction.test.ts -import {describe, it, expect, vi, beforeEach, afterEach, type Mock} from 'vitest'; - -// --- Import SUT and mocked deps --- -import getClient from '../../getClient'; -import getCurrentOrganizationAction from '../getCurrentOrganizationAction'; - -// --- Mock client factory BEFORE importing SUT --- -vi.mock('../../getClient', () => ({ - default: vi.fn(), -})); - -// A light org shape for testing (only fields we assert on) -interface Org { - id: string; - name: string; - orgHandle?: string; -} - -describe('getCurrentOrganizationAction', () => { - type ActionResult = Awaited>; - - const mockClient: {getCurrentOrganization: ReturnType} = { - getCurrentOrganization: vi.fn(), - }; - - const sessionId = 'sess-123'; - const org: Org = {id: 'org-001', name: 'Alpha', orgHandle: 'alpha'}; - - beforeEach(() => { - vi.resetAllMocks(); - (getClient as unknown as Mock).mockReturnValue(mockClient); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - it('returns success with organization when upstream succeeds', async () => { - mockClient.getCurrentOrganization.mockResolvedValueOnce(org); - - const result: ActionResult = await getCurrentOrganizationAction(sessionId); - - expect(getClient).toHaveBeenCalledTimes(1); - expect(mockClient.getCurrentOrganization).toHaveBeenCalledWith(sessionId); - - expect(result.success).toBe(true); - expect(result.error).toBeNull(); - expect(result.data.organization).toEqual(org); - }); - - it('should pass through the provided sessionId even if it is an empty string', async () => { - mockClient.getCurrentOrganization.mockResolvedValueOnce(org); - - const result: ActionResult = await getCurrentOrganizationAction(''); - - expect(mockClient.getCurrentOrganization).toHaveBeenCalledWith(''); - expect(result.success).toBe(true); - expect(result.data.organization).toEqual(org); - }); - - it('should return failure shape when client.getCurrentOrganization rejects', async () => { - mockClient.getCurrentOrganization.mockRejectedValueOnce(new Error('upstream down')); - - const result: ActionResult = await getCurrentOrganizationAction(sessionId); - - expect(result.success).toBe(false); - expect(result.error).toBe('Failed to get the current organization'); - // Matches the function’s failure payload shape - expect(result.data).toEqual({user: {}}); - }); - - it('should return failure shape when ThunderIDNextClient.getInstance throws', async () => { - (getClient as unknown as Mock).mockImplementationOnce(() => { - throw new Error('factory failed'); - }); - - const result: ActionResult = await getCurrentOrganizationAction(sessionId); - - expect(result.success).toBe(false); - expect(result.error).toBe('Failed to get the current organization'); - expect(result.data).toEqual({user: {}}); - }); - - it('should not mutate the organization object returned by upstream', async () => { - const upstreamOrg: Org & {extra: {nested: boolean}} = {...org, extra: {nested: true}}; - mockClient.getCurrentOrganization.mockResolvedValueOnce(upstreamOrg); - - const result: ActionResult = await getCurrentOrganizationAction(sessionId); - - // exact deep equality: whatever upstream returns is passed through - expect(result.data.organization).toEqual(upstreamOrg); - }); -}); diff --git a/packages/nextjs/src/server/actions/__tests__/getMyOrganizations.test.ts b/packages/nextjs/src/server/actions/__tests__/getMyOrganizations.test.ts deleted file mode 100644 index 2a5f7c8..0000000 --- a/packages/nextjs/src/server/actions/__tests__/getMyOrganizations.test.ts +++ /dev/null @@ -1,211 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// src/server/actions/__tests__/getMyOrganizations.test.ts -import {ThunderIDAPIError, Organization} from '@thunderid/node'; -import {describe, it, expect, vi, beforeEach, afterEach, type Mock} from 'vitest'; - -// --- Import SUT and mocked deps --- -import getClient from '../../getClient'; -import getMyOrganizations from '../getMyOrganizations'; -import getSessionId from '../getSessionId'; - -// --- Mocks (declare BEFORE importing the SUT) --- -vi.mock('../../getClient', () => ({ - default: vi.fn(), -})); - -// Mock the dynamically-imported module that SUT calls as: import('./getSessionId') -vi.mock('../getSessionId', () => ({ - default: vi.fn(), -})); - -describe('getMyOrganizations (Next.js server action)', () => { - const mockClient: {getAccessToken: ReturnType; getMyOrganizations: ReturnType} = { - getAccessToken: vi.fn(), - getMyOrganizations: vi.fn(), - }; - - const options: {filter: string; limit: number} = {filter: 'type eq "TENANT"', limit: 25}; - const orgs: Organization[] = [ - {id: 'org-1', name: 'Alpha', orgHandle: 'alpha'}, - {id: 'org-2', name: 'Beta', orgHandle: 'beta'}, - ]; - - beforeEach(() => { - vi.resetAllMocks(); - (getClient as unknown as Mock).mockReturnValue(mockClient); - (getSessionId as unknown as Mock).mockResolvedValue('sess-abc'); - mockClient.getAccessToken.mockResolvedValue('atk-123'); - mockClient.getMyOrganizations.mockResolvedValue(orgs); - vi.spyOn(console, 'error').mockImplementation(() => {}); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - it('should return organizations when sessionId is provided (no getSessionId fallback)', async () => { - const result: Organization[] = await getMyOrganizations(options, 'sess-123'); - - expect(getClient).toHaveBeenCalledTimes(1); - expect(getSessionId).not.toHaveBeenCalled(); - expect(mockClient.getAccessToken).toHaveBeenCalledWith('sess-123'); - expect(mockClient.getMyOrganizations).toHaveBeenCalledWith(options, 'sess-123'); - expect(result).toEqual(orgs); - }); - - it('should fall back to getSessionId when sessionId is undefined', async () => { - const result: Organization[] = await getMyOrganizations(options, undefined); - - expect(getSessionId).toHaveBeenCalledTimes(1); - expect(mockClient.getAccessToken).toHaveBeenCalledWith('sess-abc'); - expect(mockClient.getMyOrganizations).toHaveBeenCalledWith(options, 'sess-abc'); - expect(result).toEqual(orgs); - }); - - it('should fall back to getSessionId when sessionId is null', async () => { - const result: Organization[] = await getMyOrganizations(options, null as unknown as string); - - expect(getSessionId).toHaveBeenCalledTimes(1); - expect(mockClient.getAccessToken).toHaveBeenCalledWith('sess-abc'); - expect(mockClient.getMyOrganizations).toHaveBeenCalledWith(options, 'sess-abc'); - expect(result).toEqual(orgs); - }); - - it('should treat empty string sessionId as falsy and calls getSessionId', async () => { - const result: Organization[] = await getMyOrganizations(options, ''); - - expect(getSessionId).toHaveBeenCalledTimes(1); - expect(mockClient.getAccessToken).toHaveBeenCalledWith('sess-abc'); - expect(mockClient.getMyOrganizations).toHaveBeenCalledWith(options, 'sess-abc'); - expect(result).toEqual(orgs); - }); - - it('should pass through undefined options', async () => { - const result: Organization[] = await getMyOrganizations(undefined, 'sess-123'); - - expect(mockClient.getMyOrganizations).toHaveBeenCalledWith(undefined, 'sess-123'); - expect(result).toEqual(orgs); - }); - - it('should throw ThunderIDAPIError(401) when no session can be resolved', async () => { - (getSessionId as unknown as Mock).mockResolvedValueOnce(''); - - await expect(getMyOrganizations(options, undefined)).rejects.toMatchObject({ - constructor: ThunderIDAPIError, - message: expect.stringContaining('Failed to get the organizations for the user: No session ID available'), - statusCode: 401, - }); - - // Should fail before calling client methods - expect(mockClient.getAccessToken).not.toHaveBeenCalled(); - expect(mockClient.getMyOrganizations).not.toHaveBeenCalled(); - }); - - it('should throw ThunderIDAPIError(401) when access token resolves to undefined (not signed in)', async () => { - mockClient.getAccessToken.mockResolvedValueOnce(undefined); - - await expect(getMyOrganizations(options, 'sess-123')).rejects.toMatchObject({ - constructor: ThunderIDAPIError, - message: expect.stringContaining( - 'Failed to get the organizations for the user: User is not signed in - access token retrieval failed', - ), - statusCode: 401, - }); - - expect(mockClient.getAccessToken).toHaveBeenCalledWith('sess-123'); - // eslint-disable-next-line no-console - expect(console.error).toHaveBeenCalled(); // inner catch logs - expect(mockClient.getMyOrganizations).not.toHaveBeenCalled(); - }); - - it('should treat empty-string access token as not signed in (401)', async () => { - mockClient.getAccessToken.mockResolvedValueOnce(''); - - await expect(getMyOrganizations(options, 'sess-123')).rejects.toMatchObject({ - constructor: ThunderIDAPIError, - message: expect.stringContaining( - 'Failed to get the organizations for the user: User is not signed in - access token retrieval failed', - ), - statusCode: 401, - }); - - // eslint-disable-next-line no-console - expect(console.error).toHaveBeenCalled(); - expect(mockClient.getMyOrganizations).not.toHaveBeenCalled(); - }); - - it('should throw ThunderIDAPIError(401) when getAccessToken throws (e.g., upstream failure)', async () => { - mockClient.getAccessToken.mockRejectedValueOnce(new Error('token endpoint down')); - - await expect(getMyOrganizations(options, 'sess-123')).rejects.toMatchObject({ - constructor: ThunderIDAPIError, - message: expect.stringContaining( - 'Failed to get the organizations for the user: User is not signed in - access token retrieval failed', - ), - statusCode: 401, - }); - - // eslint-disable-next-line no-console - expect(console.error).toHaveBeenCalled(); - expect(mockClient.getMyOrganizations).not.toHaveBeenCalled(); - }); - - it('should wrap an ThunderIDAPIError from client.getMyOrganizations, preserving statusCode', async () => { - const upstream: ThunderIDAPIError = new ThunderIDAPIError('Upstream failed', 'ORG_LIST_503', 'server', 503); - mockClient.getMyOrganizations.mockRejectedValueOnce(upstream); - - await expect(getMyOrganizations(options, 'sess-123')).rejects.toMatchObject({ - constructor: ThunderIDAPIError, - message: expect.stringContaining('Failed to get the organizations for the user: Upstream failed'), - statusCode: 503, - }); - }); - - it('should wrap a generic Error from client.getMyOrganizations with undefined statusCode', async () => { - mockClient.getMyOrganizations.mockRejectedValueOnce(new Error('network down')); - - await expect(getMyOrganizations(options, 'sess-123')).rejects.toMatchObject({ - constructor: ThunderIDAPIError, - message: expect.stringContaining('Failed to get the organizations for the user: network down'), - statusCode: undefined, - }); - }); - - it('should wrap an error thrown by ThunderIDNextClient.getInstance()', async () => { - (getClient as unknown as Mock).mockImplementationOnce(() => { - throw new Error('factory failed'); - }); - - await expect(getMyOrganizations(options, 'sess-123')).rejects.toMatchObject({ - constructor: ThunderIDAPIError, - message: expect.stringContaining('Failed to get the organizations for the user: factory failed'), - statusCode: undefined, - }); - }); - - it('should handle minimal call: no options, undefined sessionId -> resolves via getSessionId and succeeds', async () => { - const result: Organization[] = await getMyOrganizations(); - - expect(getSessionId).toHaveBeenCalledTimes(1); - expect(mockClient.getAccessToken).toHaveBeenCalledWith('sess-abc'); - expect(mockClient.getMyOrganizations).toHaveBeenCalledWith(undefined, 'sess-abc'); - expect(result).toEqual(orgs); - }); -}); diff --git a/packages/nextjs/src/server/actions/__tests__/getOrganizationAction.test.ts b/packages/nextjs/src/server/actions/__tests__/getOrganizationAction.test.ts deleted file mode 100644 index 446bcad..0000000 --- a/packages/nextjs/src/server/actions/__tests__/getOrganizationAction.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {describe, it, expect, vi, beforeEach, afterEach, type Mock} from 'vitest'; - -// Import SUT and mocked deps -import getClient from '../../getClient'; -import getOrganizationAction from '../getOrganizationAction'; - -// Mock client factory BEFORE importing the SUT -vi.mock('../../getClient', () => ({ - default: vi.fn(), -})); - -// Minimal shape for testing; add fields only if you assert on them -interface OrganizationDetails { - id: string; - name: string; - orgHandle?: string; -} - -type ActionResult = Awaited>; - -describe('getOrganizationAction', () => { - const mockClient: {getOrganization: ReturnType} = { - getOrganization: vi.fn(), - }; - - const orgId = 'org-001'; - const sessionId = 'sess-123'; - const org: OrganizationDetails = {id: orgId, name: 'Alpha', orgHandle: 'alpha'}; - - beforeEach(() => { - vi.resetAllMocks(); - (getClient as unknown as Mock).mockReturnValue(mockClient); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - it('should return success with organization when upstream succeeds', async () => { - mockClient.getOrganization.mockResolvedValueOnce(org); - - const result: ActionResult = await getOrganizationAction(orgId, sessionId); - - expect(getClient).toHaveBeenCalledTimes(1); - expect(mockClient.getOrganization).toHaveBeenCalledWith(orgId, sessionId); - - expect(result).toEqual({ - data: {organization: org}, - error: null, - success: true, - }); - }); - - it('should pass through empty-string organizationId and sessionId (documents current behavior)', async () => { - mockClient.getOrganization.mockResolvedValueOnce(org); - - const result: ActionResult = await getOrganizationAction('', ''); - - expect(mockClient.getOrganization).toHaveBeenCalledWith('', ''); - expect(result.success).toBe(true); - expect(result.data.organization).toEqual(org); - }); - - it('should return failure shape when client.getOrganization rejects', async () => { - mockClient.getOrganization.mockRejectedValueOnce(new Error('upstream down')); - - const result: ActionResult = await getOrganizationAction(orgId, sessionId); - - expect(result).toEqual({ - data: {user: {}}, - error: 'Failed to get organization', - success: false, - }); - }); - - it('should return failure shape when ThunderIDNextClient.getInstance throws', async () => { - (getClient as unknown as Mock).mockImplementationOnce(() => { - throw new Error('factory failed'); - }); - - const result: ActionResult = await getOrganizationAction(orgId, sessionId); - - expect(result).toEqual({ - data: {user: {}}, - error: 'Failed to get organization', - success: false, - }); - }); - - it('should return failure shape when client rejects with a non-Error value', async () => { - mockClient.getOrganization.mockRejectedValueOnce('bad'); - const result: ActionResult = await getOrganizationAction(orgId, sessionId); - expect(result).toEqual({ - data: {user: {}}, - error: 'Failed to get organization', - success: false, - }); - }); - - it('should not mutate the organization object returned by upstream', async () => { - const upstreamOrg: OrganizationDetails & {extra: {nested: boolean}} = {...org, extra: {nested: true}}; - mockClient.getOrganization.mockResolvedValueOnce(upstreamOrg); - - const result: ActionResult = await getOrganizationAction(orgId, sessionId); - - expect(result.data.organization).toEqual(upstreamOrg); - }); -}); diff --git a/packages/nextjs/src/server/actions/createOrganization.ts b/packages/nextjs/src/server/actions/createOrganization.ts deleted file mode 100644 index 1c9ab69..0000000 --- a/packages/nextjs/src/server/actions/createOrganization.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -'use server'; - -import {CreateOrganizationPayload, Organization, ThunderIDAPIError} from '@thunderid/node'; -import getSessionId from './getSessionId'; -import getClient from '../getClient'; - -/** - * Server action to create an organization. - */ -const createOrganization = async (payload: CreateOrganizationPayload, sessionId: string): Promise => { - try { - const client = getClient(); - return await client.createOrganization(payload, sessionId ?? (await getSessionId())!); - } catch (error) { - throw new ThunderIDAPIError( - `Failed to create the organization: ${error instanceof Error ? error.message : String(error)}`, - 'createOrganization-ServerActionError-001', - 'nextjs', - error instanceof ThunderIDAPIError ? error.statusCode : undefined, - ); - } -}; - -export default createOrganization; diff --git a/packages/nextjs/src/server/actions/getAllOrganizations.ts b/packages/nextjs/src/server/actions/getAllOrganizations.ts deleted file mode 100644 index d520538..0000000 --- a/packages/nextjs/src/server/actions/getAllOrganizations.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -'use server'; - -import {AllOrganizationsApiResponse, ThunderIDAPIError} from '@thunderid/node'; -import getSessionId from './getSessionId'; -import getClient from '../getClient'; - -/** - * Server action to get organizations. - */ -const getAllOrganizations = async (options?: any, sessionId?: string): Promise => { - try { - const client = getClient(); - return await client.getAllOrganizations(options, sessionId ?? (await getSessionId())!); - } catch (error) { - throw new ThunderIDAPIError( - `Failed to get all the organizations for the user: ${error instanceof Error ? error.message : String(error)}`, - 'getAllOrganizations-ServerActionError-001', - 'nextjs', - error instanceof ThunderIDAPIError ? error.statusCode : undefined, - ); - } -}; - -export default getAllOrganizations; diff --git a/packages/nextjs/src/server/actions/getCurrentOrganizationAction.ts b/packages/nextjs/src/server/actions/getCurrentOrganizationAction.ts deleted file mode 100644 index 58390cb..0000000 --- a/packages/nextjs/src/server/actions/getCurrentOrganizationAction.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -'use server'; - -import {Organization} from '@thunderid/node'; -import getClient from '../getClient'; - -/** - * Server action to create an organization. - */ -const getCurrentOrganizationAction = async ( - sessionId: string, -): Promise<{ - data: {organization?: Organization; user?: Record}; - error: string | null; - success: boolean; -}> => { - try { - const client = getClient(); - const organization: Organization = (await client.getCurrentOrganization(sessionId))!; - return {data: {organization}, error: null, success: true}; - } catch (error) { - return { - data: { - user: {}, - }, - error: 'Failed to get the current organization', - success: false, - }; - } -}; - -export default getCurrentOrganizationAction; diff --git a/packages/nextjs/src/server/actions/getMyOrganizations.ts b/packages/nextjs/src/server/actions/getMyOrganizations.ts deleted file mode 100644 index 6300fcf..0000000 --- a/packages/nextjs/src/server/actions/getMyOrganizations.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -'use server'; - -import {ThunderIDAPIError, Organization} from '@thunderid/node'; -import getClient from '../getClient'; - -/** - * Server action to get organizations. - */ -const getMyOrganizations = async (options?: any, sessionId?: string): Promise => { - try { - const client = getClient(); - - // Get session ID if not provided - let resolvedSessionId: string | undefined = sessionId; - if (!resolvedSessionId) { - // Import getSessionId locally to avoid circular dependencies - const {default: getSessionId} = await import('./getSessionId'); - resolvedSessionId = await getSessionId(); - } - - if (!resolvedSessionId) { - throw new ThunderIDAPIError( - 'No session ID available for fetching organizations', - 'getMyOrganizations-SessionError-001', - 'nextjs', - 401, - ); - } - - // Check if user is signed in by trying to get access token - try { - const accessToken: string = await client.getAccessToken(resolvedSessionId); - - if (!accessToken) { - throw new ThunderIDAPIError( - 'No access token available - user is not signed in', - 'getMyOrganizations-NoAccessToken-001', - 'nextjs', - 401, - ); - } - } catch (error) { - // eslint-disable-next-line no-console - console.error('[getMyOrganizations] Failed to get access token:', error); - throw new ThunderIDAPIError( - 'User is not signed in - access token retrieval failed', - 'getMyOrganizations-NotSignedIn-001', - 'nextjs', - 401, - ); - } - - return await client.getMyOrganizations(options, resolvedSessionId); - } catch (error) { - throw new ThunderIDAPIError( - `Failed to get the organizations for the user: ${error instanceof Error ? error.message : String(error)}`, - 'getMyOrganizations-ServerActionError-001', - 'nextjs', - error instanceof ThunderIDAPIError ? error.statusCode : undefined, - ); - } -}; - -export default getMyOrganizations; diff --git a/packages/nextjs/src/server/actions/getOrganizationAction.ts b/packages/nextjs/src/server/actions/getOrganizationAction.ts deleted file mode 100644 index a15e2cd..0000000 --- a/packages/nextjs/src/server/actions/getOrganizationAction.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -'use server'; - -import {OrganizationDetails} from '@thunderid/node'; -import getClient from '../getClient'; - -/** - * Server action to create an organization. - */ -const getOrganizationAction = async ( - organizationId: string, - sessionId: string, -): Promise<{ - data: {organization?: OrganizationDetails; user?: Record}; - error: string | null; - success: boolean; -}> => { - try { - const client = getClient(); - const organization: OrganizationDetails = await client.getOrganization(organizationId, sessionId); - return {data: {organization}, error: null, success: true}; - } catch (error) { - return { - data: { - user: {}, - }, - error: 'Failed to get organization', - success: false, - }; - } -}; - -export default getOrganizationAction; diff --git a/packages/nextjs/src/server/actions/switchOrganization.ts b/packages/nextjs/src/server/actions/switchOrganization.ts deleted file mode 100644 index 1a043a3..0000000 --- a/packages/nextjs/src/server/actions/switchOrganization.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -'use server'; - -import {Organization, ThunderIDAPIError, IdToken, TokenResponse} from '@thunderid/node'; -import {cookies} from 'next/headers'; -import getSessionId from './getSessionId'; -import {ThunderIDNextConfig} from '../../models/config'; -import logger from '../../utils/logger'; -import SessionManager from '../../utils/SessionManager'; -import getClient from '../getClient'; - -type RequestCookies = Awaited>; - -/** - * Server action to switch organization. - */ -const switchOrganization = async ( - organization: Organization, - sessionId: string | undefined, -): Promise => { - try { - const cookieStore: RequestCookies = await cookies(); - const client = getClient(); - const resolvedSessionId: string = sessionId ?? (await getSessionId())!; - const response: TokenResponse | Response = await client.switchOrganization(organization, resolvedSessionId); - - const {revalidatePath} = await import('next/cache'); - revalidatePath('/'); - - if (response && (response as TokenResponse).accessToken) { - const tokenResponse: TokenResponse = response as TokenResponse; - const idToken: IdToken = await client.getDecodedIdToken(resolvedSessionId, tokenResponse.idToken); - const userIdFromToken: string = idToken.sub; - const organizationId: string | undefined = (idToken['user_org'] || idToken['organization_id']) as - | string - | undefined; - const config: ThunderIDNextConfig = await client.getConfiguration(); - const sessionCookieExpiryTime: number = SessionManager.resolveSessionCookieExpiry( - config.sessionCookie?.expiryTime, - ); - const expiresIn: number = parseInt(tokenResponse.expiresIn, 10); - - const sessionToken: string = await SessionManager.createSessionToken( - tokenResponse.accessToken, - userIdFromToken, - resolvedSessionId, - tokenResponse.scope, - expiresIn, - tokenResponse.refreshToken ?? '', - organizationId, - ); - - logger.debug('[switchOrganization] Session token created successfully.'); - - cookieStore.set( - SessionManager.getSessionCookieName(), - sessionToken, - SessionManager.getSessionCookieOptions(sessionCookieExpiryTime), - ); - } - - return response; - } catch (error) { - throw new ThunderIDAPIError( - `Failed to switch the organizations: ${error instanceof Error ? error.message : String(JSON.stringify(error))}`, - 'switchOrganization-ServerActionError-001', - 'nextjs', - error instanceof ThunderIDAPIError ? error.statusCode : undefined, - ); - } -}; - -export default switchOrganization; diff --git a/packages/nuxt/src/index.ts b/packages/nuxt/src/index.ts index 88eb41d..d067673 100644 --- a/packages/nuxt/src/index.ts +++ b/packages/nuxt/src/index.ts @@ -26,7 +26,7 @@ export type {ThunderIDNuxtConfig, ThunderIDSessionPayload, ThunderIDAuthState} f // `useThunderID`. The rest are re-exports of `@thunderid/vue` composables — // their contexts are mounted by `` (see runtime/components). export {useThunderID} from './runtime/composables/useThunderID'; -export {useUser, useOrganization, useFlow, useFlowMeta, useTheme} from '@thunderid/vue'; +export {useUser, useFlow, useFlowMeta, useTheme} from '@thunderid/vue'; export {useI18n as useThunderIDI18n} from '@thunderid/vue'; // ── Components ───────────────────────────────────────────────────────────── diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index e115e0d..1436365 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -174,33 +174,6 @@ export default defineNuxtModule({ method: 'patch' as const, route: '/api/auth/user/profile', }, - // ── Organisations ───────────────────────────────────────────────── - { - handler: resolve('./runtime/server/routes/auth/organizations/index.get'), - route: '/api/auth/organizations', - }, - { - handler: resolve('./runtime/server/routes/auth/organizations/index.post'), - method: 'post' as const, - route: '/api/auth/organizations', - }, - { - handler: resolve('./runtime/server/routes/auth/organizations/me.get'), - route: '/api/auth/organizations/me', - }, - { - handler: resolve('./runtime/server/routes/auth/organizations/current.get'), - route: '/api/auth/organizations/current', - }, - { - handler: resolve('./runtime/server/routes/auth/organizations/id.get'), - route: '/api/auth/organizations/:id', - }, - { - handler: resolve('./runtime/server/routes/auth/organizations/switch.post'), - method: 'post' as const, - route: '/api/auth/organizations/switch', - }, // ── Branding ────────────────────────────────────────────────────── {handler: resolve('./runtime/server/routes/auth/branding/branding.get'), route: '/api/auth/branding'}, ]; @@ -227,7 +200,6 @@ export default defineNuxtModule({ {from: resolve('./runtime/composables/useThunderID'), name: 'useThunderID'}, // Composables from @thunderid/vue — auto-imported directly, no local wrappers {from: '@thunderid/vue', name: 'useUser'}, - {from: '@thunderid/vue', name: 'useOrganization'}, {from: '@thunderid/vue', name: 'useFlow'}, {from: '@thunderid/vue', name: 'useFlowMeta'}, {from: '@thunderid/vue', name: 'useTheme'}, @@ -256,9 +228,9 @@ export default defineNuxtModule({ // This mirrors the Next.js SDK pattern where Base components come from // @thunderid/react and host-specific containers live in the Next.js package. // - // NOTE: Composables (useUser, useOrganization, useTheme, - // useFlow, useI18n) remain direct re-exports from @thunderid/vue via - // addImports above — only the components need Nuxt wrappers. + // NOTE: Composables (useUser, useTheme, useFlow, useI18n) remain direct + // re-exports from @thunderid/vue via addImports above — only the components + // need Nuxt wrappers. // ── Control flow ──────────────────────────────────────────────────────── addComponent({filePath: resolve('./runtime/components/control/SignedIn'), name: 'ThunderIDSignedIn'}); @@ -279,25 +251,6 @@ export default defineNuxtModule({ addComponent({filePath: resolve('./runtime/components/user/UserProfile'), name: 'ThunderIDUserProfile'}); addComponent({filePath: resolve('./runtime/components/user/UserDropdown'), name: 'ThunderIDUserDropdown'}); - // ── Organization ───────────────────────────────────────────────────────── - addComponent({filePath: resolve('./runtime/components/organization/Organization'), name: 'ThunderIDOrganization'}); - addComponent({ - filePath: resolve('./runtime/components/organization/OrganizationProfile'), - name: 'ThunderIDOrganizationProfile', - }); - addComponent({ - filePath: resolve('./runtime/components/organization/OrganizationSwitcher'), - name: 'ThunderIDOrganizationSwitcher', - }); - addComponent({ - filePath: resolve('./runtime/components/organization/OrganizationList'), - name: 'ThunderIDOrganizationList', - }); - addComponent({ - filePath: resolve('./runtime/components/organization/CreateOrganization'), - name: 'ThunderIDCreateOrganization', - }); - // ── Auth callback ──────────────────────────────────────────────────────── addComponent({filePath: resolve('./runtime/components/auth/Callback'), name: 'ThunderIDCallback'}); diff --git a/packages/nuxt/src/runtime/components/ThunderIDRoot.ts b/packages/nuxt/src/runtime/components/ThunderIDRoot.ts index 19acaf0..08d9304 100644 --- a/packages/nuxt/src/runtime/components/ThunderIDRoot.ts +++ b/packages/nuxt/src/runtime/components/ThunderIDRoot.ts @@ -18,9 +18,6 @@ import {generateFlattenedUserProfile} from '@thunderid/browser'; import type { - AllOrganizationsApiResponse, - CreateOrganizationPayload, - Organization, UpdateMeProfileConfig, User, UserProfile, @@ -29,7 +26,6 @@ import { FlowMetaProvider, FlowProvider, I18nProvider, - OrganizationProvider, ThemeProvider, UserProvider, } from '@thunderid/vue'; @@ -49,14 +45,11 @@ import {useState, useRuntimeConfig} from '#imports'; * - {@link FlowProvider} * - {@link UserProvider} ← `profile`, `flattenedProfile`, `schemas`, * `updateProfile`, `revalidateProfile`, `onUpdateProfile` - * - {@link OrganizationProvider} ← `currentOrganization`, `myOrganizations`, - * `onOrganizationSwitch`, `getAllOrganizations`, - * `revalidateMyOrganizations` * * The `THUNDERID_KEY` (config + auth state + actions) is still provided at the * app level by the Nuxt plugin; this component only supplies the auxiliary - * provider contexts so downstream composables (`useUser`, `useOrganization`, - * `useTheme`, `useThunderIDI18n`) receive real data. + * provider contexts so downstream composables (`useUser`, `useTheme`, + * `useThunderIDI18n`) receive real data. * * @example * ```vue @@ -73,8 +66,6 @@ const ThunderIDRoot: Component = defineComponent({ setup(_props: Record, {slots}: SetupContext): () => VNode { // ── Read SSR-hydrated state keys (seeded by the Nuxt plugin) ──────────── const userProfileState: Ref = useState('thunderid:user-profile'); - const currentOrgState: Ref = useState('thunderid:current-org'); - const myOrgsState: Ref = useState('thunderid:my-orgs'); // Used by onUpdateProfile to keep the top-level auth user claim in sync. const authState: Ref = useState('thunderid:auth'); @@ -88,7 +79,6 @@ const ThunderIDRoot: Component = defineComponent({ // Gate flags — mirror the same checks in thunderid-ssr.ts so client props // always agree with what the Nitro plugin decided to fetch server-side. const shouldFetchProfile: boolean = prefs?.user?.fetchUserProfile !== false; - const shouldFetchOrgs: boolean = prefs?.user?.fetchOrganizations !== false; // Defaults to 'light' — matches the Vue SDK's ThunderIDProvider, which // passes no mode and therefore uses ThemeProvider's `DEFAULT_THEME`. const themeMode: string = prefs?.theme?.mode ?? 'light'; @@ -160,52 +150,6 @@ const ThunderIDRoot: Component = defineComponent({ } }; - /** - * Token-exchange org switch via the `/api/auth/organizations/switch` Nitro route. - */ - const onOrganizationSwitch = async (organization: Organization): Promise => - $fetch('/api/auth/organizations/switch', {body: {organization}, method: 'POST'}); - - /** - * Paginated org list via the `/api/auth/organizations` Nitro route. - */ - const getAllOrganizations = async (): Promise => - $fetch('/api/auth/organizations'); - - /** - * Refresh the user's org membership list and update local state so - * `useOrganization().myOrganizations` stays reactive. - */ - const revalidateMyOrganizations = async (): Promise => { - try { - const res: Organization[] = await $fetch('/api/auth/organizations/me'); - myOrgsState.value = res ?? []; - return myOrgsState.value; - } catch { - return myOrgsState.value; - } - }; - - /** - * Create a new sub-organisation via the `POST /api/auth/organizations` route. - */ - const createOrganization = async (payload: CreateOrganizationPayload): Promise => - $fetch('/api/auth/organizations', {body: payload, method: 'POST'}); - - /** - * Refresh the current organisation from the session's ID token claims - * and update local state so `useOrganization().currentOrganization` stays reactive. - */ - const revalidateCurrentOrganization = async (): Promise => { - try { - const res: Organization | null = await $fetch('/api/auth/organizations/current'); - currentOrgState.value = res ?? null; - return currentOrgState.value; - } catch { - return currentOrgState.value; - } - }; - // ── Render tree — mirrors ThunderIDClientProvider (Next.js) ───────────── // // FlowMetaProvider is mounted unconditionally with `enabled: false` (V1 @@ -251,32 +195,7 @@ const ThunderIDRoot: Component = defineComponent({ updateProfile: shouldFetchProfile ? updateProfile : undefined, }, { - default: (): VNode | VNode[] | undefined => - h( - OrganizationProvider, - { - // When fetchOrganizations is false pass empty - // values so the provider renders without org data. - createOrganization: shouldFetchOrgs - ? (createOrganization as any) - : undefined, - currentOrganization: shouldFetchOrgs ? currentOrgState.value : null, - getAllOrganizations: shouldFetchOrgs ? getAllOrganizations : undefined, - myOrganizations: shouldFetchOrgs ? myOrgsState.value : [], - onOrganizationSwitch: shouldFetchOrgs - ? (onOrganizationSwitch as any) - : undefined, - revalidateCurrentOrganization: shouldFetchOrgs - ? revalidateCurrentOrganization - : undefined, - revalidateMyOrganizations: shouldFetchOrgs - ? revalidateMyOrganizations - : undefined, - }, - { - default: (): VNode | VNode[] | undefined => slots.default?.(), - }, - ), + default: (): VNode | VNode[] | undefined => slots.default?.(), }, ), }), diff --git a/packages/nuxt/src/runtime/components/organization/CreateOrganization.ts b/packages/nuxt/src/runtime/components/organization/CreateOrganization.ts deleted file mode 100644 index 5b539a3..0000000 --- a/packages/nuxt/src/runtime/components/organization/CreateOrganization.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {withVendorCSSClassPrefix} from '@thunderid/browser'; -import {BaseCreateOrganization} from '@thunderid/vue'; -import {type Component, type VNode, defineComponent, h} from 'vue'; -import {useOrganization} from '#imports'; - -interface CreateOrganizationSetupProps { - className: string; - description: string; - title: string; -} - -/** - * Nuxt-specific CreateOrganization container. - * - * Reads `createOrganization` from `useOrganization()` (Nuxt auto-import, - * re-exported from `@thunderid/vue`) and delegates rendering to - * {@link BaseCreateOrganization} from `@thunderid/vue`. - * - * @example - * ```vue - * - * ``` - */ -const CreateOrganization: Component = defineComponent({ - name: 'CreateOrganization', - props: { - className: {default: '', type: String}, - description: {default: 'Create a new sub-organization.', type: String}, - title: {default: 'Create Organization', type: String}, - }, - setup(props: CreateOrganizationSetupProps, {slots}: {slots: any}): () => VNode { - const {createOrganization} = useOrganization(); - - return (): VNode => - h( - BaseCreateOrganization, - { - class: withVendorCSSClassPrefix('create-organization--styled'), - className: props.className, - description: props.description, - onCreate: createOrganization - ? async (name: string): Promise => { - await createOrganization({description: '', name, parentId: '', type: 'TENANT'}, ''); - } - : undefined, - title: props.title, - }, - slots, - ); - }, -}); - -export default CreateOrganization; diff --git a/packages/nuxt/src/runtime/components/organization/Organization.ts b/packages/nuxt/src/runtime/components/organization/Organization.ts deleted file mode 100644 index 00f3da2..0000000 --- a/packages/nuxt/src/runtime/components/organization/Organization.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {type Component, type VNode, Fragment, defineComponent, h} from 'vue'; -import {useOrganization} from '#imports'; - -/** - * Nuxt-specific Organization control component. - * - * Exposes the current organization via a scoped slot. Renders the `fallback` - * slot when no organization is selected. - * - * Uses `useOrganization()` from the Nuxt auto-import layer (re-exported from - * `@thunderid/vue`) so it reads from the OrganizationProvider context. - * - * @example - * ```vue - * - * - * - * - * ``` - */ -const Organization: Component = defineComponent({ - name: 'Organization', - setup(_props: Record, {slots}: {slots: any}): () => VNode | VNode[] | null { - const {currentOrganization} = useOrganization(); - - return (): VNode | VNode[] | null => { - if (!currentOrganization?.value) { - const fallback: VNode[] | undefined = slots.fallback?.(); - return fallback ? h(Fragment, {}, fallback) : null; - } - - const content: VNode[] | undefined = slots.default?.({organization: currentOrganization.value}); - return content ? h(Fragment, {}, content) : null; - }; - }, -}); - -export default Organization; diff --git a/packages/nuxt/src/runtime/components/organization/OrganizationList.ts b/packages/nuxt/src/runtime/components/organization/OrganizationList.ts deleted file mode 100644 index 9e44d1a..0000000 --- a/packages/nuxt/src/runtime/components/organization/OrganizationList.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {type Organization as IOrganization, withVendorCSSClassPrefix} from '@thunderid/browser'; -import {BaseOrganizationList} from '@thunderid/vue'; -import {type Component, type VNode, defineComponent, h} from 'vue'; -import {useOrganization} from '#imports'; - -/** - * Nuxt-specific OrganizationList container. - * - * Reads organization list context from `useOrganization()` (Nuxt auto-import) - * and delegates rendering to {@link BaseOrganizationList} from `@thunderid/vue`. - * - * Emits a `select` event with the chosen {@link IOrganization} before calling - * `switchOrganization` so consumers can handle custom post-switch logic. - * - * @example - * ```vue - * - * ``` - */ -const OrganizationList: Component = defineComponent({ - emits: ['select'], - name: 'OrganizationList', - props: { - className: {default: '', type: String}, - }, - setup(props: {className: string}, {slots, emit}: {emit: any; slots: any}): () => VNode | VNode[] | null { - const {myOrganizations, isLoading, switchOrganization} = useOrganization(); - - const handleSelect = async (org: IOrganization): Promise => { - emit('select', org); - await switchOrganization(org); - }; - - return (): VNode | VNode[] | null => - h( - BaseOrganizationList, - { - class: withVendorCSSClassPrefix('organization-list--styled'), - className: props.className, - isLoading: isLoading?.value ?? false, - onSelect: handleSelect, - organizations: myOrganizations?.value ?? [], - }, - slots, - ); - }, -}); - -export default OrganizationList; diff --git a/packages/nuxt/src/runtime/components/organization/OrganizationProfile.ts b/packages/nuxt/src/runtime/components/organization/OrganizationProfile.ts deleted file mode 100644 index 5d6bfd5..0000000 --- a/packages/nuxt/src/runtime/components/organization/OrganizationProfile.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {withVendorCSSClassPrefix} from '@thunderid/browser'; -import {BaseOrganizationProfile} from '@thunderid/vue'; -import {type Component, type PropType, type VNode, defineComponent, h} from 'vue'; -import {useOrganization} from '#imports'; - -/** - * Nuxt-specific OrganizationProfile container. - * - * Reads the current organization from `useOrganization()` (Nuxt auto-import, - * re-exported from `@thunderid/vue`) and delegates rendering to - * {@link BaseOrganizationProfile} from `@thunderid/vue`. - * - * @example - * ```vue - * - * ``` - */ -const OrganizationProfile: Component = defineComponent({ - name: 'OrganizationProfile', - props: { - className: {default: '', type: String}, - editable: {default: false, type: Boolean}, - onUpdate: { - default: undefined, - type: Function as PropType<(payload: Record) => Promise>, - }, - title: {default: 'Organization Profile', type: String}, - }, - setup( - props: { - className: string; - editable: boolean; - onUpdate?: (payload: Record) => Promise; - title: string; - }, - {slots}: {slots: any}, - ): () => VNode | VNode[] | null { - const {currentOrganization} = useOrganization(); - - return (): VNode | VNode[] | null => - h( - BaseOrganizationProfile, - { - class: withVendorCSSClassPrefix('organization-profile--styled'), - className: props.className, - editable: props.editable, - onUpdate: props.onUpdate, - organization: currentOrganization?.value ?? null, - title: props.title, - }, - slots, - ); - }, -}); - -export default OrganizationProfile; diff --git a/packages/nuxt/src/runtime/components/organization/OrganizationSwitcher.ts b/packages/nuxt/src/runtime/components/organization/OrganizationSwitcher.ts deleted file mode 100644 index 2e1364c..0000000 --- a/packages/nuxt/src/runtime/components/organization/OrganizationSwitcher.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {withVendorCSSClassPrefix} from '@thunderid/browser'; -import {BaseOrganizationSwitcher} from '@thunderid/vue'; -import {type Component, type VNode, defineComponent, h} from 'vue'; -import {useOrganization} from '#imports'; - -/** - * Nuxt-specific OrganizationSwitcher container. - * - * Reads organization context from `useOrganization()` (Nuxt auto-import, - * re-exported from `@thunderid/vue`) and delegates rendering to - * {@link BaseOrganizationSwitcher} from `@thunderid/vue`. - * - * The `switchOrganization` action is provided by the OrganizationProvider - * context set up by {@link ThunderIDRoot}, which ultimately calls the Nitro - * `/api/auth/switch-org` route — no direct `window.location` usage. - * - * @example - * ```vue - * - * ``` - */ -const OrganizationSwitcher: Component = defineComponent({ - name: 'OrganizationSwitcher', - props: { - className: {default: '', type: String}, - }, - setup(props: {className: string}, {slots}: {slots: any}): () => VNode | VNode[] | null { - const {currentOrganization, myOrganizations, isLoading, switchOrganization} = useOrganization(); - - return (): VNode | VNode[] | null => - h( - BaseOrganizationSwitcher, - { - class: withVendorCSSClassPrefix('organization-switcher--styled'), - className: props.className, - currentOrganization: currentOrganization?.value ?? null, - isLoading: isLoading?.value ?? false, - onSwitch: switchOrganization, - organizations: myOrganizations?.value ?? [], - }, - slots, - ); - }, -}); - -export default OrganizationSwitcher; diff --git a/packages/nuxt/src/runtime/errors/error-codes.ts b/packages/nuxt/src/runtime/errors/error-codes.ts index 2f4b87d..8d3815e 100644 --- a/packages/nuxt/src/runtime/errors/error-codes.ts +++ b/packages/nuxt/src/runtime/errors/error-codes.ts @@ -33,9 +33,6 @@ export enum ErrorCode { // ── Security ─────────────────────────────────────────────────────── OpenRedirectBlocked = 'security/open-redirect-blocked', - // ── Organization ─────────────────────────────────────────────────── - OrganizationCreateFailed = 'organization/create-failed', - OrganizationSwitchFailed = 'organization/switch-failed', // ── Session ──────────────────────────────────────────────────────── SessionExpired = 'session/expired', SessionInvalid = 'session/invalid', diff --git a/packages/nuxt/src/runtime/plugins/thunderid.ts b/packages/nuxt/src/runtime/plugins/thunderid.ts index fc8a043..c7c4d2a 100644 --- a/packages/nuxt/src/runtime/plugins/thunderid.ts +++ b/packages/nuxt/src/runtime/plugins/thunderid.ts @@ -17,7 +17,7 @@ */ import {getRedirectBasedSignUpUrl} from '@thunderid/browser'; -import type {Organization, UserProfile} from '@thunderid/node'; +import type {UserProfile} from '@thunderid/node'; import {ThunderIDPlugin, THUNDERID_KEY} from '@thunderid/vue'; import type {H3Event} from 'h3'; import {computed} from 'vue'; @@ -42,8 +42,7 @@ import {defineNuxtPlugin, useState, useRequestEvent, useRuntimeConfig, navigateT * `navigateTo` so redirects work on both server and client. * 3. **ThunderIDRoot** — register the wrapper component that mounts the rest * of the provider tree (`I18nProvider`, `ThemeProvider`, `FlowProvider`, - * `UserProvider`, `OrganizationProvider`) - * so downstream composables receive real context values. + * `UserProvider`) so downstream composables receive real context values. * 4. **ThunderIDPlugin (delegated)** — install the Vue SDK plugin in * delegated mode so it skips browser-only initialisation (SSR-safe). */ @@ -95,8 +94,6 @@ export default defineNuxtPlugin((nuxtApp: NuxtApp) => { user: null, })); const userProfileState: Ref = useState('thunderid:user-profile', () => null); - const currentOrgState: Ref = useState('thunderid:current-org', () => null); - const myOrgsState: Ref = useState('thunderid:my-orgs', () => []); if (import.meta.server) { const event: H3Event | undefined = useRequestEvent(); @@ -110,8 +107,6 @@ export default defineNuxtPlugin((nuxtApp: NuxtApp) => { user: ssr.user, }; userProfileState.value = ssr.userProfile; - currentOrgState.value = ssr.currentOrganization; - myOrgsState.value = ssr.myOrganizations; } else { // Backwards-compat: fall back to the legacy context shape (pre-Step-2 plugin). const ssrContext: {isSignedIn?: boolean; session?: {sub?: string}} | undefined = event?.context?.thunderid as @@ -143,9 +138,6 @@ export default defineNuxtPlugin((nuxtApp: NuxtApp) => { // `user` is backed by the dedicated state key so ThunderIDRoot can read it // reactively without going through the THUNDERID_KEY indirection. const user: ComputedRef = computed(() => authState.value.user ?? null); - // `organization` reflects the SSR-resolved current org (hydrated from - // 'thunderid:current-org'). Kept readonly at the THUNDERID_KEY level. - const organizationRef: ComputedRef = computed(() => currentOrgState.value); // ── 3. Action helpers (Nuxt-aware navigation) ─────────────────────────── const signIn = async (options?: Record): Promise => { @@ -213,7 +205,6 @@ export default defineNuxtPlugin((nuxtApp: NuxtApp) => { isInitialized, isLoading, isSignedIn, - organization: organizationRef, organizationHandle: publicConfig.organizationHandle, platform: undefined, reInitialize: async () => false, @@ -226,7 +217,6 @@ export default defineNuxtPlugin((nuxtApp: NuxtApp) => { signUp, signUpUrl: publicConfig.signUpUrl, storage: undefined, - switchOrganization: noop, user, }); diff --git a/packages/nuxt/src/runtime/server/ThunderIDNuxtClient.ts b/packages/nuxt/src/runtime/server/ThunderIDNuxtClient.ts index c7bd8f2..d341ca3 100644 --- a/packages/nuxt/src/runtime/server/ThunderIDNuxtClient.ts +++ b/packages/nuxt/src/runtime/server/ThunderIDNuxtClient.ts @@ -20,20 +20,12 @@ import { ThunderIDNodeClient, type AuthClientConfig, type IdToken, - type Organization, - type OrganizationDetails, - type CreateOrganizationPayload, type Storage, type TokenExchangeRequestConfig, type TokenResponse, type User, type UserProfile, type UpdateMeProfileConfig, - type AllOrganizationsApiResponse, - getMeOrganizations, - getAllOrganizations, - createOrganization, - getOrganization, type ExtendedAuthorizeRequestUrlParams, type SignUpOptions, } from '@thunderid/node'; @@ -172,96 +164,10 @@ class ThunderIDNuxtClient extends ThunderIDNodeClient { return {flattenedProfile: user, profile: user, schemas: []}; } - override async getCurrentOrganization(sessionId: string): Promise { - try { - const idToken: IdToken = await this.getDecodedIdToken(sessionId); - if (!idToken?.org_id) { - return null; - } - return { - id: idToken.org_id, - name: idToken.org_name ?? '', - orgHandle: idToken.org_handle ?? '', - }; - } catch { - return null; - } - } - - override async getMyOrganizations(sessionId: string): Promise { - const accessToken: string = await this.getAccessToken(sessionId); - const configData: any = this.getStorageManager().getConfigData(); - const baseUrl: string = (configData?.baseUrl ?? '') as string; - - return getMeOrganizations({ - baseUrl, - headers: {Authorization: `Bearer ${accessToken}`}, - }); - } - override async updateUserProfile(config: UpdateMeProfileConfig, sessionId: string): Promise { throw new Error('Profile updates are not supported for the ThunderID platform.'); } - override async getAllOrganizations(options?: any, sessionId?: string): Promise { - const resolvedSessionId: string = sessionId ?? ''; - const accessToken: string = await this.getAccessToken(resolvedSessionId); - const configData: any = this.getStorageManager().getConfigData(); - const baseUrl: string = (configData?.baseUrl ?? '') as string; - - return getAllOrganizations({ - baseUrl, - headers: {Authorization: `Bearer ${accessToken}`}, - }); - } - - async createOrganization(payload: CreateOrganizationPayload, sessionId: string): Promise { - const accessToken: string = await this.getAccessToken(sessionId); - const configData: any = this.getStorageManager().getConfigData(); - const baseUrl: string = (configData?.baseUrl ?? '') as string; - - return createOrganization({ - baseUrl, - headers: {Authorization: `Bearer ${accessToken}`}, - payload, - }); - } - - async getOrganization(organizationId: string, sessionId: string): Promise { - const accessToken: string = await this.getAccessToken(sessionId); - const configData: any = this.getStorageManager().getConfigData(); - const baseUrl: string = (configData?.baseUrl ?? '') as string; - - return getOrganization({ - baseUrl, - headers: {Authorization: `Bearer ${accessToken}`}, - organizationId, - }); - } - - override async switchOrganization(organization: Organization, sessionId: string): Promise { - if (!organization.id) { - throw new Error('Organization ID is required for switching organizations.'); - } - - const exchangeConfig: TokenExchangeRequestConfig = { - attachToken: false, - data: { - client_id: '{{clientId}}', - client_secret: '{{clientSecret}}', - grant_type: 'organization_switch', - scope: '{{scopes}}', - switching_organization: organization.id, - token: '{{accessToken}}', - }, - id: 'organization-switch', - returnsSession: true, - signInRequired: true, - }; - - return this.exchangeToken(exchangeConfig, sessionId); - } - public override getStorageManager(): any { return super.getStorageManager(); } diff --git a/packages/nuxt/src/runtime/server/plugins/thunderid-ssr.ts b/packages/nuxt/src/runtime/server/plugins/thunderid-ssr.ts index b44ac98..ed4ecdc 100644 --- a/packages/nuxt/src/runtime/server/plugins/thunderid-ssr.ts +++ b/packages/nuxt/src/runtime/server/plugins/thunderid-ssr.ts @@ -49,7 +49,6 @@ function resolveCallbackUrl(event: H3Event): string { * within an organisation. * 4. In parallel (gated by `preferences`): * - Fetches user + SCIM2 user profile (`preferences.user.fetchUserProfile !== false`) - * - Fetches current org + my orgs (`preferences.user.fetchOrganizations !== false`) * 5. Writes the full {@link ThunderIDSSRData} to `event.context.thunderid.ssr` * so the Nuxt plugin can seed `useState` keys for zero-cost hydration. * @@ -154,20 +153,13 @@ export default defineNitroPlugin((nitro: {hooks: {hook: Function}}) => { // ── 4. Parallel SSR data fetches (gated by preferences) ─────────────── const shouldFetchProfile: boolean = prefs?.user?.fetchUserProfile !== false; - const shouldFetchOrgs: boolean = prefs?.user?.fetchOrganizations !== false; - const [userResult, userProfileResult, orgsResult, currentOrgResult] = await Promise.allSettled([ + const [userResult, userProfileResult] = await Promise.allSettled([ // Always fetch the basic user object (needed for ThunderIDAuthState.user) client.getUser(session.sessionId), // SCIM2 user profile (flattened + schemas) shouldFetchProfile ? client.getUserProfile(session.sessionId) : Promise.resolve(null), - - // User's organisations - shouldFetchOrgs ? client.getMyOrganizations(session.sessionId) : Promise.resolve([] as any[]), - - // Current organisation (derived from the ID token) - shouldFetchOrgs ? client.getCurrentOrganization(session.sessionId) : Promise.resolve(null), ]); if (userResult.status === 'rejected') { @@ -176,18 +168,10 @@ export default defineNitroPlugin((nitro: {hooks: {hook: Function}}) => { if (userProfileResult.status === 'rejected') { log.warn('Failed to fetch user profile (SCIM2):', userProfileResult.reason); } - if (orgsResult.status === 'rejected') { - log.warn('Failed to fetch my organisations:', orgsResult.reason); - } - if (currentOrgResult.status === 'rejected') { - log.warn('Failed to resolve current organisation:', currentOrgResult.reason); - } // ── 5. Write to event context ────────────────────────────────────────── const ssrData: ThunderIDSSRData = { - currentOrganization: currentOrgResult.status === 'fulfilled' ? currentOrgResult.value : null, isSignedIn: true, - myOrganizations: orgsResult.status === 'fulfilled' && Array.isArray(orgsResult.value) ? orgsResult.value : [], resolvedBaseUrl, session, user: userResult.status === 'fulfilled' ? userResult.value : null, diff --git a/packages/nuxt/src/runtime/server/routes/auth/organizations/current.get.ts b/packages/nuxt/src/runtime/server/routes/auth/organizations/current.get.ts deleted file mode 100644 index 3447d6d..0000000 --- a/packages/nuxt/src/runtime/server/routes/auth/organizations/current.get.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import type {Organization} from '@thunderid/node'; -import {defineEventHandler, createError} from 'h3'; -import type {H3Event} from 'h3'; -import ThunderIDNuxtClient from '../../../ThunderIDNuxtClient'; -import {verifyAndRehydrateSession} from '../../../utils/serverSession'; -import {useRuntimeConfig} from '#imports'; - -/** - * GET /api/auth/organizations/current - * - * Returns the organisation the authenticated user is currently acting within, - * derived from the session's ID token (`org_id` / `org_name` claims). - * Returns `null` when the user is not inside an organisation context. - * - * Used by `ThunderIDRoot.revalidateCurrentOrganization` callback. - */ -export default defineEventHandler(async (event: H3Event): Promise => { - const config: ReturnType = useRuntimeConfig(); - const sessionSecret: string | undefined = config.thunderid?.sessionSecret; - - const session: Awaited> = await verifyAndRehydrateSession( - event, - sessionSecret, - ); - if (!session) { - throw createError({statusCode: 401, statusMessage: 'Unauthorized: Invalid or expired session.'}); - } - - try { - const client: ThunderIDNuxtClient = ThunderIDNuxtClient.getInstance(); - return await client.getCurrentOrganization(session.sessionId); - } catch (err) { - throw createError({ - statusCode: 500, - statusMessage: `Failed to retrieve current organisation: ${err instanceof Error ? err.message : String(err)}`, - }); - } -}); diff --git a/packages/nuxt/src/runtime/server/routes/auth/organizations/id.get.ts b/packages/nuxt/src/runtime/server/routes/auth/organizations/id.get.ts deleted file mode 100644 index f9fd79e..0000000 --- a/packages/nuxt/src/runtime/server/routes/auth/organizations/id.get.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import type {OrganizationDetails} from '@thunderid/node'; -import {defineEventHandler, getRouterParam, createError} from 'h3'; -import type {H3Event} from 'h3'; -import ThunderIDNuxtClient from '../../../ThunderIDNuxtClient'; -import {verifyAndRehydrateSession} from '../../../utils/serverSession'; -import {useRuntimeConfig} from '#imports'; - -/** - * GET /api/auth/organizations/:id - * - * Returns the details of a single organisation by its ID. - * Requires an active session. - * - * Mirrors `getOrganizationAction` in the Next.js SDK. - */ -export default defineEventHandler(async (event: H3Event): Promise => { - const config: ReturnType = useRuntimeConfig(); - const sessionSecret: string | undefined = config.thunderid?.sessionSecret; - - const session: Awaited> = await verifyAndRehydrateSession( - event, - sessionSecret, - ); - if (!session) { - throw createError({statusCode: 401, statusMessage: 'Unauthorized: Invalid or expired session.'}); - } - - const organizationId: string | undefined = getRouterParam(event, 'id'); - if (!organizationId) { - throw createError({statusCode: 400, statusMessage: 'Organization ID is required.'}); - } - - try { - const client: ThunderIDNuxtClient = ThunderIDNuxtClient.getInstance(); - return await client.getOrganization(organizationId, session.sessionId); - } catch (err) { - throw createError({ - statusCode: 500, - statusMessage: `Failed to retrieve organisation: ${err instanceof Error ? err.message : String(err)}`, - }); - } -}); diff --git a/packages/nuxt/src/runtime/server/routes/auth/organizations/index.get.ts b/packages/nuxt/src/runtime/server/routes/auth/organizations/index.get.ts deleted file mode 100644 index c03cf9c..0000000 --- a/packages/nuxt/src/runtime/server/routes/auth/organizations/index.get.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import type {AllOrganizationsApiResponse} from '@thunderid/node'; -import {defineEventHandler, createError} from 'h3'; -import type {H3Event} from 'h3'; -import ThunderIDNuxtClient from '../../../ThunderIDNuxtClient'; -import {verifyAndRehydrateSession} from '../../../utils/serverSession'; -import {useRuntimeConfig} from '#imports'; - -/** - * GET /api/auth/organizations - * - * Returns all organisations accessible to the authenticated user (paginated). - * Used by `ThunderIDRoot.getAllOrganizations` callback. - * - * Mirrors `getAllOrganizations` server action in the Next.js SDK. - */ -export default defineEventHandler(async (event: H3Event): Promise => { - const config: ReturnType = useRuntimeConfig(); - const sessionSecret: string | undefined = config.thunderid?.sessionSecret; - - const session: Awaited> = await verifyAndRehydrateSession( - event, - sessionSecret, - ); - if (!session) { - throw createError({statusCode: 401, statusMessage: 'Unauthorized: Invalid or expired session.'}); - } - - try { - const client: ThunderIDNuxtClient = ThunderIDNuxtClient.getInstance(); - return await client.getAllOrganizations(undefined, session.sessionId); - } catch (err) { - throw createError({ - statusCode: 500, - statusMessage: `Failed to retrieve all organisations: ${err instanceof Error ? err.message : String(err)}`, - }); - } -}); diff --git a/packages/nuxt/src/runtime/server/routes/auth/organizations/index.post.ts b/packages/nuxt/src/runtime/server/routes/auth/organizations/index.post.ts deleted file mode 100644 index 3ef3d1a..0000000 --- a/packages/nuxt/src/runtime/server/routes/auth/organizations/index.post.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import type {CreateOrganizationPayload, Organization} from '@thunderid/node'; -import {defineEventHandler, readBody, createError} from 'h3'; -import type {H3Event} from 'h3'; -import ThunderIDNuxtClient from '../../../ThunderIDNuxtClient'; -import {verifyAndRehydrateSession} from '../../../utils/serverSession'; -import {useRuntimeConfig} from '#imports'; - -/** - * POST /api/auth/organizations - * - * Creates a new sub-organisation under the authenticated user's root organisation. - * - * Request body: {@link CreateOrganizationPayload} - * Response: {@link Organization} - * - * Mirrors `createOrganization` server action in the Next.js SDK. - */ -export default defineEventHandler(async (event: H3Event): Promise => { - const config: ReturnType = useRuntimeConfig(); - const sessionSecret: string | undefined = config.thunderid?.sessionSecret; - - const session: Awaited> = await verifyAndRehydrateSession( - event, - sessionSecret, - ); - if (!session) { - throw createError({statusCode: 401, statusMessage: 'Unauthorized: Invalid or expired session.'}); - } - - let payload: CreateOrganizationPayload; - try { - payload = await readBody(event); - } catch { - throw createError({statusCode: 400, statusMessage: 'Invalid request body.'}); - } - - try { - const client: ThunderIDNuxtClient = ThunderIDNuxtClient.getInstance(); - return await client.createOrganization(payload, session.sessionId); - } catch (err) { - throw createError({ - statusCode: 500, - statusMessage: `Failed to create organisation: ${err instanceof Error ? err.message : String(err)}`, - }); - } -}); diff --git a/packages/nuxt/src/runtime/server/routes/auth/organizations/me.get.ts b/packages/nuxt/src/runtime/server/routes/auth/organizations/me.get.ts deleted file mode 100644 index 5143ad5..0000000 --- a/packages/nuxt/src/runtime/server/routes/auth/organizations/me.get.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import type {Organization} from '@thunderid/node'; -import {defineEventHandler, createError} from 'h3'; -import type {H3Event} from 'h3'; -import ThunderIDNuxtClient from '../../../ThunderIDNuxtClient'; -import {verifyAndRehydrateSession} from '../../../utils/serverSession'; -import {useRuntimeConfig} from '#imports'; - -/** - * GET /api/auth/organizations/me - * - * Returns the list of organisations the authenticated user is a member of. - * Used by `ThunderIDRoot.revalidateMyOrganizations` to refresh client-side state. - * - * Mirrors `getMyOrganizations` server action in the Next.js SDK. - */ -export default defineEventHandler(async (event: H3Event): Promise => { - const config: ReturnType = useRuntimeConfig(); - const sessionSecret: string | undefined = config.thunderid?.sessionSecret; - - const session: Awaited> = await verifyAndRehydrateSession( - event, - sessionSecret, - ); - if (!session) { - throw createError({statusCode: 401, statusMessage: 'Unauthorized: Invalid or expired session.'}); - } - - try { - const client: ThunderIDNuxtClient = ThunderIDNuxtClient.getInstance(); - return await client.getMyOrganizations(session.sessionId); - } catch (err) { - throw createError({ - statusCode: 500, - statusMessage: `Failed to retrieve organisations: ${err instanceof Error ? err.message : String(err)}`, - }); - } -}); diff --git a/packages/nuxt/src/runtime/server/routes/auth/organizations/switch.post.ts b/packages/nuxt/src/runtime/server/routes/auth/organizations/switch.post.ts deleted file mode 100644 index 98326bc..0000000 --- a/packages/nuxt/src/runtime/server/routes/auth/organizations/switch.post.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import type {Organization, TokenResponse} from '@thunderid/node'; -import {defineEventHandler, readBody, createError} from 'h3'; -import type {H3Event} from 'h3'; -import ThunderIDNuxtClient from '../../../ThunderIDNuxtClient'; -import {verifyAndRehydrateSession} from '../../../utils/serverSession'; -import {issueSessionCookie} from '../../../utils/session'; -import {useRuntimeConfig} from '#imports'; - -/** - * POST /api/auth/organizations/switch - * - * Performs an `organization_switch` token exchange for the given organisation, - * then re-issues the JWT session cookie so subsequent requests carry the new - * organisation context. - * - * Request body: `{ organization: Organization }` - * - * Mirrors `switchOrganization` server action in the Next.js SDK. - */ -export default defineEventHandler(async (event: H3Event): Promise<{success: boolean}> => { - const config: ReturnType = useRuntimeConfig(); - const sessionSecret: string | undefined = config.thunderid?.sessionSecret; - - const session: Awaited> = await verifyAndRehydrateSession( - event, - sessionSecret, - ); - if (!session) { - throw createError({statusCode: 401, statusMessage: 'Unauthorized: Invalid or expired session.'}); - } - const {sessionId} = session; - - let organization: Organization; - try { - const body: {organization: Organization} = await readBody<{organization: Organization}>(event); - organization = body.organization; - } catch { - throw createError({statusCode: 400, statusMessage: 'Invalid request body.'}); - } - - if (!organization?.id) { - throw createError({statusCode: 400, statusMessage: 'organization.id is required.'}); - } - - let tokenResponse: TokenResponse; - try { - const client: ThunderIDNuxtClient = ThunderIDNuxtClient.getInstance(); - const response: TokenResponse | Response = await client.switchOrganization(organization, sessionId); - tokenResponse = response as TokenResponse; - } catch (err) { - throw createError({ - statusCode: 500, - statusMessage: `Organisation switch failed: ${err instanceof Error ? err.message : String(err)}`, - }); - } - - // Re-issue the session cookie with the new token so subsequent SSR requests - // pick up the switched organisation context — mirrors callback.get.ts. - try { - const runtimeConfig: ReturnType = useRuntimeConfig(); - const runtimeSessionSecret: string | undefined = runtimeConfig.thunderid?.sessionSecret; - await issueSessionCookie(event, sessionId, tokenResponse, runtimeSessionSecret); - } catch (err) { - throw createError({ - statusCode: 500, - statusMessage: `Failed to establish new session after organisation switch: ${ - err instanceof Error ? err.message : String(err) - }`, - }); - } - - return {success: true}; -}); diff --git a/packages/nuxt/src/runtime/types.ts b/packages/nuxt/src/runtime/types.ts index 49465b6..72223f7 100644 --- a/packages/nuxt/src/runtime/types.ts +++ b/packages/nuxt/src/runtime/types.ts @@ -18,7 +18,6 @@ import type { I18nPreferences, - Organization, TokenEndpointAuthMethod, User, UserProfile, @@ -62,8 +61,6 @@ export interface ThunderIDNuxtConfig { mode?: 'light' | 'dark' | 'system' | 'class' | 'branding'; }; user?: { - /** Whether to fetch the user's organisations during SSR (default: true). */ - fetchOrganizations?: boolean; /** Whether to fetch the SCIM2 user profile during SSR (default: true). */ fetchUserProfile?: boolean; }; @@ -132,11 +129,7 @@ export interface ThunderIDTempSessionPayload extends JWTPayload { * hydrated `useState` keys so the client never re-fetches on first render. */ export interface ThunderIDSSRData { - /** The organisation the user is currently acting within (null when not in an org). */ - currentOrganization: Organization | null; isSignedIn: boolean; - /** All organisations the user is a member of (empty array when `preferences.user.fetchOrganizations` is false). */ - myOrganizations: Organization[]; /** * The base URL actually used for this request. * Equals `${baseUrl}/o` when the user is acting within an organisation diff --git a/packages/react/src/ThunderIDReactClient.ts b/packages/react/src/ThunderIDReactClient.ts index 179d274..86fd919 100644 --- a/packages/react/src/ThunderIDReactClient.ts +++ b/packages/react/src/ThunderIDReactClient.ts @@ -24,9 +24,7 @@ import { SignUpOptions, ThunderIDRuntimeError, executeEmbeddedSignInFlow, - Organization, IdToken, - AllOrganizationsApiResponse, extractUserClaimsFromIdToken, TokenResponse, HttpRequestConfig, @@ -39,8 +37,6 @@ import { EmbeddedSignInFlowStatus, EmbeddedSignUpFlowStatus, } from '@thunderid/browser'; -import getAllOrganizations from './api/getAllOrganizations'; -import getMeOrganizations from './api/getMeOrganizations'; import {ThunderIDReactConfig} from './models/config'; class ThunderIDReactClient extends ThunderIDBrowserClient { @@ -125,109 +121,6 @@ class ThunderIDReactClient { - try { - let baseUrl: string = options?.baseUrl; - - if (!baseUrl) { - const configData: any = await this.getStorageManager().getConfigData(); - baseUrl = configData?.baseUrl; - } - - return await getMeOrganizations({baseUrl, instanceId: this.getInstanceId()}); - } catch (error) { - throw new ThunderIDRuntimeError( - `Failed to fetch the user's associated organizations: ${ - error instanceof Error ? error.message : String(error) - }`, - 'ThunderIDReactClient-getMyOrganizations-RuntimeError-001', - 'react', - 'An error occurred while fetching associated organizations of the signed-in user.', - ); - } - } - - override async getAllOrganizations(options?: any): Promise { - try { - let baseUrl: string = options?.baseUrl; - - if (!baseUrl) { - const configData: any = await this.getStorageManager().getConfigData(); - baseUrl = configData?.baseUrl; - } - - return await getAllOrganizations({baseUrl, instanceId: this.getInstanceId()}); - } catch (error) { - throw new ThunderIDRuntimeError( - `Failed to fetch all organizations: ${error instanceof Error ? error.message : String(error)}`, - 'ThunderIDReactClient-getAllOrganizations-RuntimeError-001', - 'react', - 'An error occurred while fetching all the organizations associated with the user.', - ); - } - } - - override async getCurrentOrganization(): Promise { - try { - return await this.withLoading(async () => { - const idToken: IdToken = await this.getDecodedIdToken(); - return { - id: idToken?.org_id ?? '', - name: idToken?.org_name ?? '', - orgHandle: idToken?.org_handle ?? '', - }; - }); - } catch (error) { - throw new ThunderIDRuntimeError( - `Failed to fetch the current organization: ${error instanceof Error ? error.message : String(error)}`, - 'ThunderIDReactClient-getCurrentOrganization-RuntimeError-001', - 'react', - 'An error occurred while fetching the current organization of the signed-in user.', - ); - } - } - - override async switchOrganization(organization: Organization): Promise { - return this.withLoading(async () => { - try { - const configData: any = await this.getStorageManager().getConfigData(); - const sourceInstanceId: number | undefined = configData?.organizationChain?.sourceInstanceId; - - if (!organization.id) { - throw new ThunderIDRuntimeError( - 'Organization ID is required for switching organizations', - 'react-ThunderIDReactClient-SwitchOrganizationError-001', - 'react', - 'The organization object must contain a valid ID to perform the organization switch.', - ); - } - - const exchangeConfig: TokenExchangeRequestConfig = { - attachToken: false, - data: { - client_id: '{{clientId}}', - grant_type: 'organization_switch', - scope: '{{scopes}}', - switching_organization: organization.id, - token: '{{accessToken}}', - }, - id: 'organization-switch', - returnsSession: true, - signInRequired: sourceInstanceId === undefined, - }; - - return (await super.exchangeToken(exchangeConfig)) as TokenResponse | Response; - } catch (error) { - throw new ThunderIDRuntimeError( - `Failed to switch organization: ${error.message || error}`, - 'react-ThunderIDReactClient-SwitchOrganizationError-003', - 'react', - 'An error occurred while switching to the specified organization. Please try again.', - ); - } - }); - } - override isLoading(): boolean { return this.loadingState; } diff --git a/packages/react/src/api/createOrganization.ts b/packages/react/src/api/createOrganization.ts deleted file mode 100644 index ebb8e38..0000000 --- a/packages/react/src/api/createOrganization.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { - Organization, - HttpResponse, - FetchHttpClient, - HttpRequestConfig, - createOrganization as baseCreateOrganization, - CreateOrganizationConfig as BaseCreateOrganizationConfig, -} from '@thunderid/browser'; - -/** - * Configuration for the createOrganization request (React-specific) - */ -export interface CreateOrganizationConfig extends Omit { - /** - * Optional custom fetcher function. If not provided, the ThunderID SPA client's httpClient will be used - * which is a wrapper around axios http.request - */ - fetcher?: (url: string, config: RequestInit) => Promise; - /** - * Optional instance ID for multi-instance support. Defaults to 0. - */ - instanceId?: number; -} - -/** - * Creates a new organization. - * This function uses the ThunderID SPA client's httpClient by default, but allows for custom fetchers. - * - * @param config - Configuration object containing baseUrl, payload and optional request config. - * @returns A promise that resolves with the created organization information. - * @example - * ```typescript - * // Using default ThunderID SPA client httpClient - * try { - * const organization = await createOrganization({ - * baseUrl: "https://localhost:8090", - * payload: { - * description: "Share your screens", - * name: "Team Viewer", - * orgHandle: "team-viewer", - * parentId: "f4825104-4948-40d9-ab65-a960eee3e3d5", - * type: "TENANT" - * } - * }); - * console.log(organization); - * } catch (error) { - * if (error instanceof ThunderIDAPIError) { - * console.error('Failed to create organization:', error.message); - * } - * } - * ``` - * - * @example - * ```typescript - * // Using custom fetcher - * try { - * const organization = await createOrganization({ - * baseUrl: "https://localhost:8090", - * payload: { - * description: "Share your screens", - * name: "Team Viewer", - * orgHandle: "team-viewer", - * parentId: "f4825104-4948-40d9-ab65-a960eee3e3d5", - * type: "TENANT" - * }, - * fetcher: customFetchFunction - * }); - * console.log(organization); - * } catch (error) { - * if (error instanceof ThunderIDAPIError) { - * console.error('Failed to create organization:', error.message); - * } - * } - * ``` - */ -const createOrganization = async ({ - fetcher, - instanceId = 0, - ...requestConfig -}: CreateOrganizationConfig): Promise => { - const defaultFetcher = async (url: string, config: RequestInit): Promise => { - const httpClient: FetchHttpClient = FetchHttpClient.getInstance(instanceId); - const response: HttpResponse = await httpClient.request({ - data: config.body ? JSON.parse(config.body as string) : undefined, - headers: config.headers as Record, - method: config.method || 'POST', - url, - } as HttpRequestConfig); - - return { - json: () => Promise.resolve(response.data), - ok: response.status >= 200 && response.status < 300, - status: response.status, - statusText: response.statusText || '', - text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)), - } as Response; - }; - - return baseCreateOrganization({ - ...requestConfig, - fetcher: fetcher || defaultFetcher, - }); -}; - -export default createOrganization; diff --git a/packages/react/src/api/getAllOrganizations.ts b/packages/react/src/api/getAllOrganizations.ts deleted file mode 100644 index 44bb44e..0000000 --- a/packages/react/src/api/getAllOrganizations.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { - HttpResponse, - FetchHttpClient, - HttpRequestConfig, - getAllOrganizations as baseGetAllOrganizations, - GetAllOrganizationsConfig as BaseGetAllOrganizationsConfig, - AllOrganizationsApiResponse, -} from '@thunderid/browser'; - -/** - * Configuration for the getAllOrganizations request (React-specific) - */ -export interface GetAllOrganizationsConfig extends Omit { - /** - * Optional custom fetcher function. If not provided, the ThunderID SPA client's httpClient will be used - * which is a wrapper around axios http.request - */ - fetcher?: (url: string, config: RequestInit) => Promise; - /** - * Optional instance ID for multi-instance support. Defaults to 0. - */ - instanceId?: number; -} - -/** - * Retrieves all organizations with pagination support. - * This function uses the ThunderID SPA client's httpClient by default, but allows for custom fetchers. - * - * @param config - Configuration object containing baseUrl, optional query parameters, and request config. - * @returns A promise that resolves with the paginated organizations information. - * @example - * ```typescript - * // Using default ThunderID SPA client httpClient - * try { - * const response = await getAllOrganizations({ - * baseUrl: "https://localhost:8090", - * filter: "", - * limit: 10, - * recursive: false - * }); - * console.log(response.organizations); - * } catch (error) { - * if (error instanceof ThunderIDAPIError) { - * console.error('Failed to get organizations:', error.message); - * } - * } - * ``` - * - * @example - * ```typescript - * // Using custom fetcher - * try { - * const response = await getAllOrganizations({ - * baseUrl: "https://localhost:8090", - * filter: "", - * limit: 10, - * recursive: false, - * fetcher: customFetchFunction - * }); - * console.log(response.organizations); - * } catch (error) { - * if (error instanceof ThunderIDAPIError) { - * console.error('Failed to get organizations:', error.message); - * } - * } - * ``` - */ -const getAllOrganizations = async ({ - fetcher, - instanceId = 0, - ...requestConfig -}: GetAllOrganizationsConfig): Promise => { - const defaultFetcher = async (url: string, config: RequestInit): Promise => { - const httpClient: FetchHttpClient = FetchHttpClient.getInstance(instanceId); - const response: HttpResponse = await httpClient.request({ - headers: config.headers as Record, - method: config.method || 'GET', - url, - } as HttpRequestConfig); - - return { - json: () => Promise.resolve(response.data), - ok: response.status >= 200 && response.status < 300, - status: response.status, - statusText: response.statusText || '', - text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)), - } as Response; - }; - - return baseGetAllOrganizations({ - ...requestConfig, - fetcher: fetcher || defaultFetcher, - }); -}; - -export default getAllOrganizations; diff --git a/packages/react/src/api/getMeOrganizations.ts b/packages/react/src/api/getMeOrganizations.ts deleted file mode 100644 index 96acfc6..0000000 --- a/packages/react/src/api/getMeOrganizations.ts +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { - Organization, - HttpResponse, - FetchHttpClient, - HttpRequestConfig, - getMeOrganizations as baseGetMeOrganizations, - GetMeOrganizationsConfig as BaseGetMeOrganizationsConfig, -} from '@thunderid/browser'; - -/** - * Configuration for the getMeOrganizations request (React-specific) - */ -export interface GetMeOrganizationsConfig extends Omit { - /** - * Optional custom fetcher function. If not provided, the ThunderID SPA client's httpClient will be used - * which is a wrapper around axios http.request - */ - fetcher?: (url: string, config: RequestInit) => Promise; - /** - * Optional instance ID for multi-instance support. Defaults to 0. - */ - instanceId?: number; -} - -/** - * Retrieves the organizations associated with the current user. - * This function uses the ThunderID SPA client's httpClient by default, but allows for custom fetchers. - * - * @param config - Configuration object containing baseUrl, optional query parameters, and request config. - * @returns A promise that resolves with the organizations information. - * @example - * ```typescript - * // Using default ThunderID SPA client httpClient - * try { - * const organizations = await getMeOrganizations({ - * baseUrl: "https://localhost:8090", - * after: "", - * before: "", - * filter: "", - * limit: 10, - * recursive: false - * }); - * console.log(organizations); - * } catch (error) { - * if (error instanceof ThunderIDAPIError) { - * console.error('Failed to get organizations:', error.message); - * } - * } - * ``` - * - * @example - * ```typescript - * // Using custom fetcher - * try { - * const organizations = await getMeOrganizations({ - * baseUrl: "https://localhost:8090", - * after: "", - * before: "", - * filter: "", - * limit: 10, - * recursive: false, - * fetcher: customFetchFunction - * }); - * console.log(organizations); - * } catch (error) { - * if (error instanceof ThunderIDAPIError) { - * console.error('Failed to get organizations:', error.message); - * } - * } - * ``` - */ -const getMeOrganizations = async ({ - fetcher, - instanceId = 0, - ...requestConfig -}: GetMeOrganizationsConfig): Promise => { - const defaultFetcher = async (url: string, config: RequestInit): Promise => { - const httpClient: FetchHttpClient = FetchHttpClient.getInstance(instanceId); - const response: HttpResponse = await httpClient.request({ - headers: config.headers as Record, - method: config.method || 'GET', - url, - } as HttpRequestConfig); - - return { - json: () => Promise.resolve(response.data), - ok: response.status >= 200 && response.status < 300, - status: response.status, - statusText: response.statusText || '', - text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)), - } as Response; - }; - - return baseGetMeOrganizations({ - ...requestConfig, - fetcher: fetcher || defaultFetcher, - }); -}; - -export default getMeOrganizations; diff --git a/packages/react/src/api/getOrganization.ts b/packages/react/src/api/getOrganization.ts deleted file mode 100644 index f3ca1ff..0000000 --- a/packages/react/src/api/getOrganization.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { - HttpResponse, - FetchHttpClient, - HttpRequestConfig, - getOrganization as baseGetOrganization, - GetOrganizationConfig as BaseGetOrganizationConfig, - OrganizationDetails, -} from '@thunderid/browser'; - -/** - * Configuration for the getOrganization request (React-specific) - */ -export interface GetOrganizationConfig extends Omit { - /** - * Optional custom fetcher function. If not provided, the ThunderID SPA client's httpClient will be used - * which is a wrapper around axios http.request - */ - fetcher?: (url: string, config: RequestInit) => Promise; - /** - * Optional instance ID for multi-instance support. Defaults to 0. - */ - instanceId?: number; -} - -/** - * Retrieves detailed information for a specific organization. - * This function uses the ThunderID SPA client's httpClient by default, but allows for custom fetchers. - * - * @param config - Configuration object containing baseUrl, organizationId, and request config. - * @returns A promise that resolves with the organization details. - * @example - * ```typescript - * // Using default ThunderID SPA client httpClient - * try { - * const organization = await getOrganization({ - * baseUrl: "https://localhost:8090", - * organizationId: "0d5e071b-d3d3-475d-b3c6-1a20ee2fa9b1" - * }); - * console.log(organization); - * } catch (error) { - * if (error instanceof ThunderIDAPIError) { - * console.error('Failed to get organization:', error.message); - * } - * } - * ``` - * - * @example - * ```typescript - * // Using custom fetcher - * try { - * const organization = await getOrganization({ - * baseUrl: "https://localhost:8090", - * organizationId: "0d5e071b-d3d3-475d-b3c6-1a20ee2fa9b1", - * fetcher: customFetchFunction - * }); - * console.log(organization); - * } catch (error) { - * if (error instanceof ThunderIDAPIError) { - * console.error('Failed to get organization:', error.message); - * } - * } - * ``` - */ -const getOrganization = async ({ - fetcher, - instanceId = 0, - ...requestConfig -}: GetOrganizationConfig): Promise => { - const defaultFetcher = async (url: string, config: RequestInit): Promise => { - const httpClient: FetchHttpClient = FetchHttpClient.getInstance(instanceId); - const response: HttpResponse = await httpClient.request({ - headers: config.headers as Record, - method: config.method || 'GET', - url, - } as HttpRequestConfig); - - return { - json: () => Promise.resolve(response.data), - ok: response.status >= 200 && response.status < 300, - status: response.status, - statusText: response.statusText || '', - text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)), - } as Response; - }; - - return baseGetOrganization({ - ...requestConfig, - fetcher: fetcher || defaultFetcher, - }); -}; - -export default getOrganization; diff --git a/packages/react/src/api/getSchemas.ts b/packages/react/src/api/getSchemas.ts deleted file mode 100644 index f4c400c..0000000 --- a/packages/react/src/api/getSchemas.ts +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { - Schema, - HttpResponse, - FetchHttpClient, - HttpRequestConfig, - getSchemas as baseGetSchemas, - GetSchemasConfig as BaseGetSchemasConfig, -} from '@thunderid/browser'; - -/** - * Configuration for the getSchemas request (React-specific) - */ -export interface GetSchemasConfig extends Omit { - /** - * Optional custom fetcher function. If not provided, the ThunderID SPA client's httpClient will be used - * which is a wrapper around axios http.request - */ - fetcher?: (url: string, config: RequestInit) => Promise; - /** - * Optional instance ID for multi-instance support. Defaults to 0. - */ - instanceId?: number; -} - -/** - * Retrieves the SCIM2 schemas from the specified endpoint. - * This function uses the ThunderID SPA client's httpClient by default, but allows for custom fetchers. - * - * @param config - Request configuration object. - * @returns A promise that resolves with the SCIM2 schemas information. - * @example - * ```typescript - * // Using default ThunderID SPA client httpClient - * try { - * const schemas = await getSchemas({ - * url: "https://localhost:8090/scim2/Schemas", - * }); - * console.log(schemas); - * } catch (error) { - * if (error instanceof ThunderIDAPIError) { - * console.error('Failed to get schemas:', error.message); - * } - * } - * ``` - * - * @example - * ```typescript - * // Using custom fetcher - * try { - * const schemas = await getSchemas({ - * url: "https://localhost:8090/scim2/Schemas", - * fetcher: customFetchFunction - * }); - * console.log(schemas); - * } catch (error) { - * if (error instanceof ThunderIDAPIError) { - * console.error('Failed to get schemas:', error.message); - * } - * } - * ``` - */ -const getSchemas = async ({fetcher, instanceId = 0, ...requestConfig}: GetSchemasConfig): Promise => { - const defaultFetcher = async (url: string, config: RequestInit): Promise => { - const httpClient: FetchHttpClient = FetchHttpClient.getInstance(instanceId); - const response: HttpResponse = await httpClient.request({ - headers: config.headers as Record, - method: config.method || 'GET', - url, - } as HttpRequestConfig); - - return { - json: () => Promise.resolve(response.data), - ok: response.status >= 200 && response.status < 300, - status: response.status, - statusText: response.statusText || '', - text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)), - } as Response; - }; - - return baseGetSchemas({ - ...requestConfig, - fetcher: fetcher || defaultFetcher, - }); -}; - -export default getSchemas; diff --git a/packages/react/src/api/updateOrganization.ts b/packages/react/src/api/updateOrganization.ts deleted file mode 100644 index d440cbf..0000000 --- a/packages/react/src/api/updateOrganization.ts +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { - HttpResponse, - FetchHttpClient, - HttpRequestConfig, - updateOrganization as baseUpdateOrganization, - UpdateOrganizationConfig as BaseUpdateOrganizationConfig, - OrganizationDetails, - createPatchOperations, -} from '@thunderid/browser'; - -/** - * Configuration for the updateOrganization request (React-specific) - */ -export interface UpdateOrganizationConfig extends Omit { - /** - * Optional custom fetcher function. If not provided, the ThunderID SPA client's httpClient will be used - * which is a wrapper around axios http.request - */ - fetcher?: (url: string, config: RequestInit) => Promise; - /** - * Optional instance ID for multi-instance support. Defaults to 0. - */ - instanceId?: number; -} - -/** - * Updates the organization information using the Organizations Management API. - * This function uses the ThunderID SPA client's httpClient by default, but allows for custom fetchers. - * - * @param config - Configuration object with baseUrl, organizationId, operations and optional request config. - * @returns A promise that resolves with the updated organization information. - * @example - * ```typescript - * // Using the helper function to create operations automatically - * const operations = createPatchOperations({ - * name: "Updated Organization Name", // Will use REPLACE - * description: "", // Will use REMOVE (empty string) - * customField: "Some value" // Will use REPLACE - * }); - * - * await updateOrganization({ - * baseUrl: "https://localhost:8090", - * organizationId: "0d5e071b-d3d3-475d-b3c6-1a20ee2fa9b1", - * operations - * }); - * - * // Or manually specify operations - * await updateOrganization({ - * baseUrl: "https://localhost:8090", - * organizationId: "0d5e071b-d3d3-475d-b3c6-1a20ee2fa9b1", - * operations: [ - * { operation: "REPLACE", path: "/name", value: "Updated Organization Name" }, - * { operation: "REMOVE", path: "/description" } - * ] - * }); - * ``` - * - * @example - * ```typescript - * // Using custom fetcher - * await updateOrganization({ - * baseUrl: "https://localhost:8090", - * organizationId: "0d5e071b-d3d3-475d-b3c6-1a20ee2fa9b1", - * operations: [ - * { operation: "REPLACE", path: "/name", value: "Updated Organization Name" } - * ], - * fetcher: customFetchFunction - * }); - * ``` - */ -const updateOrganization = async ({ - fetcher, - instanceId = 0, - ...requestConfig -}: UpdateOrganizationConfig): Promise => { - const defaultFetcher = async (url: string, config: RequestInit): Promise => { - const httpClient: FetchHttpClient = FetchHttpClient.getInstance(instanceId); - const response: HttpResponse = await httpClient.request({ - data: config.body ? JSON.parse(config.body as string) : undefined, - headers: config.headers as Record, - method: config.method || 'PATCH', - url, - } as HttpRequestConfig); - - return { - json: () => Promise.resolve(response.data), - ok: response.status >= 200 && response.status < 300, - status: response.status, - statusText: response.statusText || '', - text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)), - } as Response; - }; - - return baseUpdateOrganization({ - ...requestConfig, - fetcher: fetcher || defaultFetcher, - }); -}; - -// Re-export the helper function -export {createPatchOperations}; - -export default updateOrganization; diff --git a/packages/react/src/components/control/OrganizationContext/OrganizationContext.tsx b/packages/react/src/components/control/OrganizationContext/OrganizationContext.tsx deleted file mode 100644 index 77e747b..0000000 --- a/packages/react/src/components/control/OrganizationContext/OrganizationContext.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {FC, PropsWithChildren} from 'react'; -import OrganizationContextController from './OrganizationContextController'; -import ThunderIDProvider, {ThunderIDProviderProps} from '../../../contexts/ThunderID/ThunderIDProvider'; -import useThunderID from '../../../contexts/ThunderID/useThunderID'; - -export interface OrganizationContextProps extends Omit { - /** - * Optional base URL for the organization context. If not provided, it will default to the source provider's base URL. - */ - baseUrl?: string; - /** - * Instance ID for this organization context. Must be unique across the app if multiple contexts are used. - */ - instanceId: number; - /** - * Optional source instance ID. If not provided, immediate parent provider is used as source. - */ - sourceInstanceId?: number; - /** - * ID of the organization to authenticate with - */ - targetOrganizationId: string; -} - -const OrganizationContext: FC> = ({ - instanceId, - baseUrl, - clientId, - afterSignInUrl, - afterSignOutUrl, - targetOrganizationId, - sourceInstanceId, - scopes, - children, - ...rest -}: PropsWithChildren) => { - // Get the source provider's signed-in status - const { - isSignedIn: isSourceSignedIn, - instanceId: sourceInstanceIdFromContext, - baseUrl: sourceBaseUrl, - clientId: sourceClientId, - } = useThunderID(); - - return ( - - - {children} - - - ); -}; - -export default OrganizationContext; diff --git a/packages/react/src/components/control/OrganizationContext/OrganizationContextController.tsx b/packages/react/src/components/control/OrganizationContext/OrganizationContextController.tsx deleted file mode 100644 index bbe5712..0000000 --- a/packages/react/src/components/control/OrganizationContext/OrganizationContextController.tsx +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {Organization} from '@thunderid/browser'; -import {FC, useEffect, useRef} from 'react'; -import useThunderID from '../../../contexts/ThunderID/useThunderID'; - -interface OrganizationContextControllerProps { - /** - * Children to render - */ - children: React.ReactNode; - /** - * Whether the source provider is signed in - */ - isSourceSignedIn: boolean; - /** - * ID of the organization to authenticate with - */ - targetOrganizationId: string; -} - -const OrganizationContextController: FC = ({ - targetOrganizationId, - isSourceSignedIn, - children, -}: OrganizationContextControllerProps) => { - const {isInitialized, isSignedIn, switchOrganization, isLoading} = useThunderID(); - const hasAuthenticatedRef: React.MutableRefObject = useRef(false); - const isAuthenticatingRef: React.MutableRefObject = useRef(false); - - /** - * Handle the organization switch when: - * - Current instance is initialized and NOT signed in - * - Source provider IS signed in - * Uses the `switchOrganization` function from the ThunderID context. - */ - useEffect(() => { - const performOrganizationSwitch = async (): Promise => { - // Prevent multiple authentication attempts - if (hasAuthenticatedRef.current || isAuthenticatingRef.current) { - return; - } - - // Wait for initialization to complete - if (!isInitialized || isLoading) { - return; - } - - // Only proceed if user is not already signed in to this instance - if (isSignedIn) { - hasAuthenticatedRef.current = true; - return; - } - - // CRITICAL: Only proceed if source provider is signed in - if (!isSourceSignedIn) { - return; - } - - try { - isAuthenticatingRef.current = true; - hasAuthenticatedRef.current = true; - - // Build the organization object for authentication - const targetOrganization: Organization = { - id: targetOrganizationId, - name: '', // Name will be populated after authentication - orgHandle: '', // Will be populated after authentication - }; - - // Call the switchOrganization API from context (handles token exchange) - await switchOrganization(targetOrganization); - } catch (error) { - // eslint-disable-next-line no-console - console.error('Linked organization authentication failed:', error); - - // Reset the flag to allow retry - hasAuthenticatedRef.current = false; - } finally { - isAuthenticatingRef.current = false; - } - }; - - performOrganizationSwitch(); - }, [isInitialized, isSignedIn, isLoading, isSourceSignedIn, targetOrganizationId, switchOrganization]); - - return <>{children}; -}; - -export default OrganizationContextController; diff --git a/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.styles.ts b/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.styles.ts deleted file mode 100644 index 05e2bc1..0000000 --- a/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.styles.ts +++ /dev/null @@ -1,180 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {css} from '@emotion/css'; -import {Theme} from '@thunderid/browser'; -import {useMemo} from 'react'; - -/** - * Creates styles for the BaseCreateOrganization component using BEM methodology - * @param theme - The theme object containing design tokens - * @param colorScheme - The current color scheme (used for memoization) - * @returns Object containing CSS class names for component styling - */ -const useStyles = (theme: Theme, colorScheme: string): Record => - useMemo(() => { - const root: string = css` - padding: calc(${theme.vars.spacing.unit} * 4); - min-width: 600px; - margin: 0 auto; - font-family: ${theme.vars.typography.fontFamily}; - `; - - const card: string = css` - background: ${theme.vars.colors.background.surface}; - border-radius: ${theme.vars.borderRadius.large}; - padding: calc(${theme.vars.spacing.unit} * 4); - `; - - const content: string = css` - display: flex; - flex-direction: column; - gap: calc(${theme.vars.spacing.unit} * 2); - `; - - const form: string = css` - display: flex; - flex-direction: column; - gap: calc(${theme.vars.spacing.unit} * 2); - width: 100%; - `; - - const header: string = css` - display: flex; - align-items: center; - gap: calc(${theme.vars.spacing.unit} * 1.5); - margin-bottom: calc(${theme.vars.spacing.unit} * 1.5); - `; - - const field: string = css` - display: flex; - align-items: center; - padding: ${theme.vars.spacing.unit} 0; - border-bottom: 1px solid ${theme.vars.colors.border}; - min-height: 32px; - `; - - const fieldGroup: string = css` - display: flex; - flex-direction: column; - gap: calc(${theme.vars.spacing.unit} * 0.5); - `; - - const textarea: string = css` - width: 100%; - padding: ${theme.vars.spacing.unit} calc(${theme.vars.spacing.unit} * 1.5); - border: 1px solid ${theme.vars.colors.border}; - border-radius: ${theme.vars.borderRadius.medium}; - font-size: ${theme.vars.typography.fontSizes.md}; - color: ${theme.vars.colors.text.primary}; - background-color: ${theme.vars.colors.background.surface}; - font-family: ${theme.vars.typography.fontFamily}; - min-height: 80px; - resize: vertical; - outline: none; - &:focus { - border-color: ${theme.vars.colors.primary.main}; - box-shadow: 0 0 0 2px ${theme.vars.colors.primary.main}20; - } - &:disabled { - background-color: ${theme.vars.colors.background.disabled}; - color: ${theme.vars.colors.text.secondary}; - cursor: not-allowed; - } - `; - - const textareaError: string = css` - border-color: ${theme.vars.colors.error.main}; - `; - - const input: string = css``; - - const avatarContainer: string = css` - align-items: flex-start; - display: flex; - gap: calc(${theme.vars.spacing.unit} * 2); - margin-bottom: ${theme.vars.spacing.unit}; - `; - - const actions: string = css` - display: flex; - gap: ${theme.vars.spacing.unit}; - justify-content: flex-end; - padding-top: calc(${theme.vars.spacing.unit} * 2); - `; - - const infoContainer: string = css` - display: flex; - flex-direction: column; - gap: ${theme.vars.spacing.unit}; - `; - - const value: string = css` - color: ${theme.vars.colors.text.primary}; - flex: 1; - display: flex; - align-items: center; - gap: ${theme.vars.spacing.unit}; - overflow: hidden; - min-height: 32px; - line-height: 32px; - `; - - const popup: string = css` - padding: calc(${theme.vars.spacing.unit} * 2); - `; - - const errorAlert: string = css` - margin-bottom: calc(${theme.vars.spacing.unit} * 2); - `; - - return { - actions, - avatarContainer, - card, - content, - errorAlert, - field, - fieldGroup, - form, - header, - infoContainer, - input, - popup, - root, - textarea, - textareaError, - value, - }; - }, [ - theme.vars.spacing.unit, - theme.vars.colors.background.surface, - theme.vars.colors.border, - theme.vars.borderRadius.large, - theme.vars.borderRadius.medium, - theme.vars.typography.fontSizes.md, - theme.vars.colors.text.primary, - theme.vars.colors.primary.main, - theme.vars.colors.background.disabled, - theme.vars.colors.text.secondary, - theme.vars.colors.error.main, - theme.vars.typography.fontFamily, - colorScheme, - ]); - -export default useStyles; diff --git a/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.tsx b/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.tsx deleted file mode 100644 index 0701a10..0000000 --- a/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.tsx +++ /dev/null @@ -1,279 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import {cx} from '@emotion/css'; -import {CreateOrganizationPayload, createPackageComponentLogger, Preferences} from '@thunderid/browser'; -import {ChangeEvent, CSSProperties, FC, FormEvent, ReactElement, ReactNode, useState} from 'react'; -import useStyles from './BaseCreateOrganization.styles'; -import useTheme from '../../../contexts/Theme/useTheme'; -import useTranslation from '../../../hooks/useTranslation'; -import AlertPrimitive from '../../primitives/Alert/Alert'; -import Button from '../../primitives/Button/Button'; -import DialogPrimitive from '../../primitives/Dialog/Dialog'; -import FormControl from '../../primitives/FormControl/FormControl'; -import InputLabel from '../../primitives/InputLabel/InputLabel'; -import TextField from '../../primitives/TextField/TextField'; - -const logger: ReturnType = createPackageComponentLogger( - '@thunderid/react', - 'BaseCreateOrganization', -); - -/** - * Interface for organization form data. - */ -export interface OrganizationFormData { - description: string; - handle: string; - name: string; -} - -/** - * Props interface for the BaseCreateOrganization component. - */ -export interface BaseCreateOrganizationProps { - cardLayout?: boolean; - className?: string; - defaultParentId?: string; - error?: string | null; - initialValues?: Partial; - loading?: boolean; - mode?: 'inline' | 'popup'; - onCancel?: () => void; - onOpenChange?: (open: boolean) => void; - onSubmit?: (payload: CreateOrganizationPayload) => void | Promise; - onSuccess?: (organization: any) => void; - open?: boolean; - /** - * Component-level preferences to override global i18n and theme settings. - * Preferences are deep-merged with global ones, with component preferences - * taking precedence. Affects this component and all its descendants. - */ - preferences?: Preferences; - renderAdditionalFields?: () => ReactNode; - style?: CSSProperties; - - title?: string; -} - -/** - * Removes special characters except space and hyphen from the organization name - * and generates a valid handle. - * @param name - * @returns - */ -const generateHandleFromName = (name: string): string => - name - .toLowerCase() - .replace(/[^a-z0-9\s-]/g, '') - .replace(/\s+/g, '-') - .replace(/-+/g, '-') - .replace(/^-|-$/g, ''); - -/** - * BaseCreateOrganization component provides the core functionality for creating organizations. - * This component serves as the base for framework-specific implementations. - */ -export const BaseCreateOrganization: FC = ({ - cardLayout = true, - className = '', - defaultParentId = '', - error, - initialValues = {}, - loading = false, - mode = 'inline', - onCancel, - onOpenChange, - onSubmit, - onSuccess, - open = false, - preferences, - renderAdditionalFields, - style, - title = 'Create Organization', -}: BaseCreateOrganizationProps): ReactElement => { - const {theme, colorScheme} = useTheme(); - const styles: ReturnType = useStyles(theme, colorScheme); - const {t} = useTranslation(preferences?.i18n); - const [formData, setFormData] = useState({ - description: '', - handle: '', - name: '', - ...initialValues, - }); - const [formErrors, setFormErrors] = useState & {avatar?: string}>({}); - - const validateForm = (): boolean => { - const errors: Partial = {}; - - if (!formData.name.trim()) { - errors.name = 'Organization name is required'; - } - - if (!formData.handle.trim()) { - errors.handle = 'Organization handle is required'; - } else if (!/^[a-z0-9-]+$/.test(formData.handle)) { - errors.handle = 'Handle can only contain lowercase letters, numbers, and hyphens'; - } - - if (!formData.description.trim()) { - errors.description = 'Organization description is required'; - } - - setFormErrors(errors); - return Object.keys(errors).length === 0; - }; - - const handleInputChange = (field: keyof OrganizationFormData, value: string): void => { - setFormData((prev: OrganizationFormData) => ({ - ...prev, - [field]: value, - })); - - if (formErrors[field]) { - setFormErrors((prev: Partial & {avatar?: string}) => ({ - ...prev, - [field]: undefined, - })); - } - }; - - /** - * Handles changes to the organization name input. - * Automatically generates the organization handle based on the name if the handle is not set or matches - * - * @param value - The new value for the organization name. - */ - const handleNameChange = (value: string): void => { - handleInputChange('name', value); - - if (!formData.handle || formData.handle === generateHandleFromName(formData.name)) { - const newHandle: string = generateHandleFromName(value); - handleInputChange('handle', newHandle); - } - }; - - const handleSubmit = async (e: FormEvent): Promise => { - e.preventDefault(); - - if (!validateForm() || loading) { - return; - } - - const payload: CreateOrganizationPayload = { - description: formData.description.trim(), - name: formData.name.trim(), - orgHandle: formData.handle.trim(), - parentId: defaultParentId, - type: 'TENANT', - }; - - try { - await onSubmit?.(payload); - if (onSuccess) { - onSuccess(payload); - } - } catch (submitError) { - // Error handling is done by parent component - logger.error('Form submission error:'); - } - }; - - const createOrganizationContent: ReactElement = ( -
-
-
- {error && ( - - Error - {error} - - )} -
- ): void => handleNameChange(e.target.value)} - disabled={loading} - required - error={formErrors.name} - className={cx(styles['input'])} - /> -
-
- ): void => handleInputChange('handle', e.target.value)} - disabled={loading} - required - error={formErrors.handle} - helperText="This will be your organization's unique identifier. Only lowercase letters, numbers, and hyphens are allowed." - className={cx(styles['input'])} - /> -
-
- - {t('elements.fields.organization.description.label')} -