diff --git a/packages/javascript/src/api/createUser.ts b/packages/javascript/src/api/createUser.ts new file mode 100644 index 00000000..a207a534 --- /dev/null +++ b/packages/javascript/src/api/createUser.ts @@ -0,0 +1,117 @@ +/** + * 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 {User} from '../models/user'; +import AsgardeoAPIError from '../errors/AsgardeoAPIError'; + +/** + * Configuration for the createUser request + */ +export interface CreateUserConfig extends Omit { + /** + * The absolute API endpoint. + * Defaults to https://api.asgardeo.io/t/dxlab/scim2/Users + */ + url?: string; + /** + * The base path of the API endpoint (e.g., https://api.asgardeo.io/t/dxlab) + */ + baseUrl?: string; + /** + * The user object to create (SCIM2 User schema) + */ + payload: any; + /** + * Optional custom fetcher function. + * If not provided, native fetch will be used + */ + fetcher?: (url: string, config: RequestInit) => Promise; +} + +/** + * Creates a new user at the SCIM2 Users endpoint. + * + * @param config - Configuration object with URL, payload and optional request config. + * @returns A promise that resolves with the created user profile information. + * @example + * ```typescript + * await createUser({ + * url: "https://api.asgardeo.io/t/dxlab/scim2/Users", + * payload: { ... } + * }); + * ``` + */ +const createUser = async ({url, baseUrl, payload, fetcher, ...requestConfig}: CreateUserConfig): Promise => { + try { + new URL(url ?? baseUrl); + } catch (error) { + throw new AsgardeoAPIError( + `Invalid URL provided. ${error?.toString()}`, + 'createUser-ValidationError-001', + 'javascript', + 400, + 'The provided `url` or `baseUrl` path does not adhere to the URL schema.', + ); + } + + const fetchFn = fetcher || fetch; + const resolvedUrl: string = url ?? `${baseUrl}/scim2/Users`; + + const requestInit: RequestInit = { + method: 'POST', + ...requestConfig, + headers: { + ...requestConfig.headers, + 'Content-Type': 'application/scim+json', + Accept: 'application/json', + }, + body: JSON.stringify(payload), + }; + + try { + const response: Response = await fetchFn(resolvedUrl, requestInit); + + if (!response?.ok) { + const errorText = await response.text(); + + throw new AsgardeoAPIError( + `Failed to create user: ${errorText}`, + 'createUser-ResponseError-001', + 'javascript', + response.status, + response.statusText, + ); + } + + return (await response.json()) as User; + } catch (error) { + if (error instanceof AsgardeoAPIError) { + throw error; + } + + throw new AsgardeoAPIError( + error?.response?.data?.detail || 'An error occurred while creating the user. Please try again.', + 'createUser-NetworkError-001', + 'javascript', + error?.data?.status, + 'Network Error', + ); + } +}; + +export default createUser; diff --git a/packages/javascript/src/api/getUserstores.ts b/packages/javascript/src/api/getUserstores.ts new file mode 100644 index 00000000..5687e420 --- /dev/null +++ b/packages/javascript/src/api/getUserstores.ts @@ -0,0 +1,97 @@ +/** + * 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 AsgardeoAPIError from '../errors/AsgardeoAPIError'; +import {Userstore, UserstoreProperty} from '../models/userstore'; + +export interface GetUserstoresConfig extends Omit { + url?: string; + baseUrl?: string; + fetcher?: (url: string, config: RequestInit) => Promise; + requiredAttributes?: string; +} + +/** + * Retrieves the userstores from the specified endpoint. + * @param config - Request configuration object. + * @returns A promise that resolves with the userstores information. + */ +const getUserstores = async ({ + url, + baseUrl, + fetcher, + requiredAttributes, + ...requestConfig +}: GetUserstoresConfig): Promise => { + try { + new URL(url ?? baseUrl); + } catch (error) { + throw new AsgardeoAPIError( + `Invalid URL provided. ${error?.toString()}`, + 'getUserstores-ValidationError-001', + 'javascript', + 400, + 'The provided `url` or `baseUrl` path does not adhere to the URL schema.', + ); + } + + const fetchFn = fetcher || fetch; + let resolvedUrl: string = url ?? `${baseUrl}/api/server/v1/userstores`; + if (requiredAttributes) { + const sep = resolvedUrl.includes('?') ? '&' : '?'; + resolvedUrl += `${sep}requiredAttributes=${encodeURIComponent(requiredAttributes)}`; + } + + const requestInit: RequestInit = { + ...requestConfig, + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...requestConfig.headers, + }, + }; + + try { + const response: Response = await fetchFn(resolvedUrl, requestInit); + if (!response?.ok) { + const errorText = await response.text(); + throw new AsgardeoAPIError( + errorText || 'Failed to fetch userstores.', + 'getUserstores-ResponseError-001', + 'javascript', + response.status, + response.statusText, + ); + } + return (await response.json()) as Userstore[]; + } catch (error) { + if (error instanceof AsgardeoAPIError) { + throw error; + } + throw new AsgardeoAPIError( + error?.response?.data?.detail || 'An error occurred while fetching userstores. Please try again.', + 'getUserstores-NetworkError-001', + 'javascript', + error?.data?.status, + 'Network Error', + ); + } +}; + +export default getUserstores; diff --git a/packages/javascript/src/i18n/en-US.ts b/packages/javascript/src/i18n/en-US.ts index 789135a9..ec7e1f90 100644 --- a/packages/javascript/src/i18n/en-US.ts +++ b/packages/javascript/src/i18n/en-US.ts @@ -122,6 +122,15 @@ const translations: I18nTranslations = { 'organization.create.creating': 'Creating...', 'organization.create.cancel': 'Cancel', + /* |---------------------------------------------------------------| */ + /* | User Creation | */ + /* |---------------------------------------------------------------| */ + + 'user.create.cancel': 'Cancel', + 'user.create.submit': 'Create User', + 'user.create.submitting': 'Creating...', + 'user.create.generic.error': 'An error occurred while creating the user. Please try again.', + /* |---------------------------------------------------------------| */ /* | Messages | */ /* |---------------------------------------------------------------| */ diff --git a/packages/javascript/src/index.ts b/packages/javascript/src/index.ts index 2c9e3217..b98c69d3 100644 --- a/packages/javascript/src/index.ts +++ b/packages/javascript/src/index.ts @@ -38,6 +38,8 @@ export {default as getOrganization, OrganizationDetails, GetOrganizationConfig} export {default as updateOrganization, createPatchOperations, UpdateOrganizationConfig} from './api/updateOrganization'; export {default as updateMeProfile, UpdateMeProfileConfig} from './api/updateMeProfile'; export {default as getBrandingPreference, GetBrandingPreferenceConfig} from './api/getBrandingPreference'; +export {default as createUser, CreateUserConfig} from './api/createUser'; +export {default as getUserstores, GetUserstoresConfig} from './api/getUserstores'; export {default as ApplicationNativeAuthenticationConstants} from './constants/ApplicationNativeAuthenticationConstants'; export {default as TokenConstants} from './constants/TokenConstants'; @@ -63,6 +65,7 @@ export { EmbeddedSignInFlowAuthenticatorPromptType, EmbeddedSignInFlowAuthenticatorKnownIdPType, } from './models/embedded-signin-flow'; +export {UserstoreProperty, Userstore} from './models/userstore'; export { EmbeddedFlowType, EmbeddedFlowStatus, @@ -112,7 +115,7 @@ export { BrandingOrganizationDetails, UrlsConfig, } from './models/branding-preference'; -export {Schema, SchemaAttribute, WellKnownSchemaIds, FlattenedSchema} from './models/scim2-schema'; +export {Schema, SchemaAttribute, WellKnownSchemaIds, FlattenedSchema, ProfileSchemaType} from './models/scim2-schema'; export {RecursivePartial} from './models/utility-types'; export {FieldType} from './models/field'; export {I18nBundle, I18nTranslations, I18nMetadata} from './models/i18n'; @@ -123,10 +126,12 @@ export {default as createTheme} from './theme/createTheme'; export {ThemeColors, ThemeConfig, Theme, ThemeMode, ThemeDetection} from './theme/types'; export {default as bem} from './utils/bem'; +export {default as getAttributeProfileSchema} from './utils/getAttributeProfileSchema'; 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 detectUserstoreEnvironment} from './utils/detectUserstoreEnvironment'; export {default as extractUserClaimsFromIdToken} from './utils/extractUserClaimsFromIdToken'; export {default as extractPkceStorageKeyFromState} from './utils/extractPkceStorageKeyFromState'; export {default as flattenUserSchema} from './utils/flattenUserSchema'; diff --git a/packages/javascript/src/models/i18n.ts b/packages/javascript/src/models/i18n.ts index 39afb9c9..3c5b0105 100644 --- a/packages/javascript/src/models/i18n.ts +++ b/packages/javascript/src/models/i18n.ts @@ -124,6 +124,15 @@ export interface I18nTranslations { 'organization.create.creating': string; 'organization.create.cancel': string; + /* |---------------------------------------------------------------| */ + /* | User Creation | */ + /* |---------------------------------------------------------------| */ + + 'user.create.cancel': string; + 'user.create.submit': string; + 'user.create.submitting': string; + 'user.create.generic.error': string; + /* |---------------------------------------------------------------| */ /* | Messages | */ /* |---------------------------------------------------------------| */ diff --git a/packages/javascript/src/models/scim2-schema.ts b/packages/javascript/src/models/scim2-schema.ts index 0a6fe200..54447048 100644 --- a/packages/javascript/src/models/scim2-schema.ts +++ b/packages/javascript/src/models/scim2-schema.ts @@ -32,6 +32,12 @@ export interface SchemaAttribute { supportedByDefault?: string; sharedProfileValueResolvingMethod?: string; subAttributes?: SchemaAttribute[]; + profiles: { + [key: string]: { + required?: boolean; + supportedByDefault?: boolean; + }; + }; } /** @@ -67,3 +73,5 @@ export enum WellKnownSchemaIds { /** Custom User Schema */ CustomUser = 'urn:scim:schemas:extension:custom:User', } + +export type ProfileSchemaType = 'console' | 'endUser' | 'selfRegistration'; diff --git a/packages/javascript/src/models/userstore.ts b/packages/javascript/src/models/userstore.ts new file mode 100644 index 00000000..cae4af2d --- /dev/null +++ b/packages/javascript/src/models/userstore.ts @@ -0,0 +1,33 @@ +/** + * 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 UserstoreProperty { + name: string; + value: string; +} + +export interface Userstore { + id: string; + name: string; + enabled: boolean; + description?: string; + isLocal: boolean; + self: string; + typeName: string; + properties?: UserstoreProperty[]; +} diff --git a/packages/javascript/src/utils/detectUserstoreEnvironment.ts b/packages/javascript/src/utils/detectUserstoreEnvironment.ts new file mode 100644 index 00000000..15f51ab0 --- /dev/null +++ b/packages/javascript/src/utils/detectUserstoreEnvironment.ts @@ -0,0 +1,65 @@ +/** + * Utility to detect environment (Asgardeo or IS) and filter userstores. + * + * Usage: + * const result = detectUserstoreEnvironment(userstores, primaryUserstore); + * + * Returns: + * { + * isAsgardeo: boolean, + * isIS: boolean, + * userstoresWritable: Userstore[], + * userstoresReadOnly: Userstore[], + * allUserstores: Userstore[], + * primaryUserstore?: Userstore + * } + */ + +import {Userstore} from '../models/userstore'; + +export interface DetectUserstoreEnvResult { + isAsgardeo: boolean; + isIS: boolean; + userstoresWritable: Userstore[]; + userstoresReadOnly: Userstore[]; + allUserstores: Userstore[]; + primaryUserstore?: Userstore; +} + +const detectUserstoreEnvironment = ( + userstores: Userstore[], + primaryUserstore?: Userstore, +): DetectUserstoreEnvResult => { + let isAsgardeo = false; + let isIS = false; + + if ( + Array.isArray(userstores) && + userstores.some(u => u.name === 'DEFAULT' && u.typeName === 'AsgardeoBusinessUserStoreManager') + ) { + isAsgardeo = true; + } else if (Array.isArray(userstores) && userstores.length === 0 && primaryUserstore) { + isIS = true; + } + + // Helper to check readonly property + const isReadOnly = (userstore: Userstore) => { + if (!userstore.properties) return false; + const prop = userstore.properties.find(p => p.name === 'ReadOnly'); + return prop?.value === 'true'; + }; + + const userstoresReadOnly = userstores.filter(isReadOnly); + const userstoresWritable = userstores.filter(u => !isReadOnly(u)); + + return { + isAsgardeo, + isIS, + userstoresWritable, + userstoresReadOnly, + allUserstores: userstores, + primaryUserstore: isIS ? primaryUserstore : undefined, + }; +}; + +export default detectUserstoreEnvironment; diff --git a/packages/javascript/src/utils/getAttributeProfileSchema.ts b/packages/javascript/src/utils/getAttributeProfileSchema.ts new file mode 100644 index 00000000..bce862a0 --- /dev/null +++ b/packages/javascript/src/utils/getAttributeProfileSchema.ts @@ -0,0 +1,63 @@ +/** + * Filters and processes SCIM schemas based on profile and supported/required rules. + * + * - If `supportedByDefault` is true and `required` is true, keep the attribute. + * - If `supportedByDefault` is false and `required` is true and there's a `profiles` object, + * check if the passed in profile is among the keys and its `supportedByDefault` isn't false. + * If false, don't return. + * + * Recursively processes subAttributes as well. + * + * @param schemas - Array of SCIM schemas (with attributes) + * @param profile - Profile type: 'console', 'endUser', or 'selfRegistration' + * @returns Filtered schemas with only the attributes that match the rules + */ + +import {ProfileSchemaType, Schema, SchemaAttribute} from '../models/scim2-schema'; + +const isTrue = (val: any): boolean => val === true || val === 'true'; + +const filterAttributes = (attributes: SchemaAttribute[], profile: ProfileSchemaType): SchemaAttribute[] => + attributes + ?.map(attr => { + let keep = false; + const supported = isTrue(attr.supportedByDefault); + // required can be at top level or inside profiles[profile] + const profileConfig = attr.profiles ? attr.profiles[profile] : undefined; + const required = typeof profileConfig?.required === 'boolean' ? profileConfig.required : !!attr.required; + + if (supported && required) { + keep = true; + } else if (!supported && required && profileConfig) { + if (isTrue(profileConfig.supportedByDefault) && isTrue(profileConfig.required)) { + keep = true; + } + } + + let subAttributes: SchemaAttribute[] | undefined = undefined; + + if (Array.isArray(attr.subAttributes) && attr.subAttributes.length > 0) { + subAttributes = filterAttributes(attr.subAttributes, profile); + } + + if (keep || (subAttributes && subAttributes.length > 0)) { + return { + ...attr, + subAttributes, + }; + } + + return null; + }) + .filter(Boolean) as SchemaAttribute[]; + +/** + * Accepts a flat array of attributes and filters them by profile rules. + * If you pass a SCIM schema object array, extract the attributes first. + */ +const getAttributeProfileSchema = (attributes: SchemaAttribute[], profile: ProfileSchemaType): SchemaAttribute[] => { + if (!Array.isArray(attributes)) return []; + return filterAttributes(attributes, profile); +}; + +export default getAttributeProfileSchema; diff --git a/packages/nextjs/src/client/components/presentation/CreateOrganization/CreateOrganization.tsx b/packages/nextjs/src/client/components/presentation/CreateOrganization/CreateOrganization.tsx index a2d24afe..282608df 100644 --- a/packages/nextjs/src/client/components/presentation/CreateOrganization/CreateOrganization.tsx +++ b/packages/nextjs/src/client/components/presentation/CreateOrganization/CreateOrganization.tsx @@ -35,7 +35,7 @@ export interface CreateOrganizationProps extends Omit Promise; + onCreate?: (payload: CreateOrganizationPayload) => Promise; } /** @@ -54,7 +54,7 @@ export interface CreateOrganizationProps extends Omit { + * onCreate={async (payload) => { * const result = await myCustomAPI.createOrganization(payload); * return result; * }} @@ -71,7 +71,7 @@ export interface CreateOrganizationProps extends Omit = ({ - onCreateOrganization, + onCreate, fallback = <>, onSuccess, defaultParentId, @@ -101,8 +101,8 @@ export const CreateOrganization: FC = ({ try { let result: any; - if (onCreateOrganization) { - result = await onCreateOrganization(payload); + if (onCreate) { + result = await onCreate(payload); } else { if (!baseUrl) { throw new Error('Base URL is required for organization creation'); diff --git a/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx b/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx index 6465b4e7..bd99599b 100644 --- a/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx +++ b/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx @@ -18,12 +18,10 @@ 'use client'; -import {FC, ReactElement} from 'react'; -import {BaseUserProfile, BaseUserProfileProps, useUser} from '@asgardeo/react'; -import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; +import {FC, ReactElement, useState} from 'react'; +import {BaseUserProfile, BaseUserProfileProps, useTranslation, useUser} from '@asgardeo/react'; import getSessionId from '../../../../server/actions/getSessionId'; -import updateUserProfileAction from '../../../../server/actions/updateUserProfileAction'; -import {Schema, User} from '@asgardeo/node'; +import {AsgardeoError, Schema, User} from '@asgardeo/node'; /** * Props for the UserProfile component. @@ -55,12 +53,27 @@ export type UserProfileProps = Omit = ({...rest}: UserProfileProps): ReactElement => { - const {baseUrl} = useAsgardeo(); const {profile, flattenedProfile, schemas, onUpdateProfile, updateProfile} = useUser(); + const {t} = useTranslation(); + + const [error, setError] = useState(null); const handleProfileUpdate = async (payload: any): Promise => { - const result = await updateProfile(payload, (await getSessionId()) as string); - onUpdateProfile(result?.data?.user); + setError(null); + + try { + const result = await updateProfile(payload, (await getSessionId()) as string); + + onUpdateProfile(result?.data?.user); + } catch (error: unknown) { + let message: string = t('user.profile.update.generic.error'); + + if (error instanceof AsgardeoError) { + message = error?.message; + } + + setError(message); + } }; return ( @@ -69,6 +82,7 @@ const UserProfile: FC = ({...rest}: UserProfileProps): ReactEl flattenedProfile={flattenedProfile as User} schemas={schemas as Schema[]} onUpdate={handleProfileUpdate} + error={error} {...rest} /> ); diff --git a/packages/nextjs/src/utils/SessionManager.ts b/packages/nextjs/src/utils/SessionManager.ts index 63a43c77..0091f612 100644 --- a/packages/nextjs/src/utils/SessionManager.ts +++ b/packages/nextjs/src/utils/SessionManager.ts @@ -28,13 +28,15 @@ export interface SessionTokenPayload extends JWTPayload { /** Session ID */ sessionId: string; /** OAuth scopes */ - scopes: string[]; + scopes: string; /** Organization ID if applicable */ organizationId?: string; /** Issued at timestamp */ iat: number; /** Expiration timestamp */ exp: number; + /** Access token */ + accessToken: string; } /** diff --git a/packages/react/docs/components/asgardeo-provider.md b/packages/react/docs/components/asgardeo-provider.md index a4000622..70e14a68 100644 --- a/packages/react/docs/components/asgardeo-provider.md +++ b/packages/react/docs/components/asgardeo-provider.md @@ -8,68 +8,60 @@ The `AsgardeoProvider` initializes the Asgardeo authentication client, manages a ## Props -All props are based on the `AsgardeoReactConfig` interface, which extends the base configuration from `@asgardeo/javascript`. +The `AsgardeoProvider` component accepts the following props: -### Required Props +| Prop | Type | Required | Description | +|------------------|----------|----------|-------------| +| `clientID` | `string` | ✅ | Client ID of your application | +| `baseUrl` | `string` | ✅ | The base URL of the Asgardeo tenant (e.g., `https://api.asgardeo.io/t/abc-org`) | +| `signInRedirectURL` | `string` | ❌ | URL to redirect to after login | +| `signOutRedirectURL` | `string` | ❌ | URL to redirect to after logout | +| `scope` | `string[]` | ❌ | Requested scopes (defaults to `['openid']`) | +| `responseMode` | `'query' \| 'form_post'` | ❌ | Response mode for OIDC requests | +| `onSignIn` | `(state) => void` | ❌ | Callback after successful login | +| `onSignOut` | `() => void` | ❌ | Callback after logout | +| `tokenValidation`| `TokenValidation` | ❌ | Configuration for token validation | +| `preferences` | `Preferences` | ❌ | Customization options for UI behavior and styling | -| Prop | Type | Default | Description | -|------|------|---------|-------------| -| `baseUrl` | `string` | **REQUIRED** | The base URL of your Asgardeo organization. Format: `https://api.asgardeo.io/t/{org_name}` | -| `clientId` | `string` | **REQUIRED** | The client ID obtained from your Asgardeo application registration | -| `afterSignInUrl` | `string` | `window.location.origin` | URL to redirect users after successful sign-in. Must match configured redirect URIs in Asgardeo | -| `afterSignOutUrl` | `string` | `window.location.origin` | URL to redirect users after sign-out. Must match configured post-logout redirect URIs | -| `scopes` | `string \| string[]` | `openid profile internal_login` | OAuth scopes to request during authentication (e.g., `"openid profile email"` or `["openid", "profile", "email"]`) | -| `organizationHandle` | `string` | - | Organization handle for organization-specific features like branding. Auto-derived from `baseUrl` if not provided. Required for custom domains | -| `applicationId` | `string` | - | UUID of the Asgardeo application for application-specific branding and features | -| `signInUrl` | `string` | - | Custom sign-in page URL. If provided, users will be redirected here instead of Asgardeo's default sign-in page | -| `signUpUrl` | `string` | - | Custom sign-up page URL. If provided, users will be redirected here instead of Asgardeo's default sign-up page | -| `clientSecret` | `string` | - | Client secret for confidential clients. Not recommended for browser applications | -| `tokenValidation` | [TokenValidation](#tokenvalidation) | - | Token validation configuration for ID tokens including validation flags and clock tolerance | -| `preferences` | [Preferences](#preferences) | - | Configuration object for theming, internationalization, and UI customization | +--- -
+??? info "TokenValidation" -

TokenValidation

+ The `tokenValidation` prop allows you to configure how ID tokens are validated. -The `tokenValidation` prop allows you to configure how ID tokens are validated. + | Property | Type | Default | Description | + |----------|------|---------|-------------| + | `idToken` | `IdTokenValidation` | `{}` | Configuration for ID token validation | -| Property | Type | Default | Description | -|----------|------|---------|-------------| -| `idToken` | `IdTokenValidation` | `{}` | Configuration for ID token validation | + #### IdTokenValidation -#### IdTokenValidation + | Property | Type | Default | Description | + |----------|------|---------|-------------| + | `validate` | `boolean` | `true` | Whether to validate the ID token | + | `validateIssuer` | `boolean` | `true` | Whether to validate the issuer | + | `clockTolerance` | `number` | `300` | Allowed clock skew in seconds | -| Property | Type | Default | Description | -|----------|------|---------|-------------| -| `validate` | `boolean` | `true` | Whether to validate the ID token | -| `validateIssuer` | `boolean` | `true` | Whether to validate the issuer | -| `clockTolerance` | `number` | `300` | Allowed clock skew in seconds | +--- -
+??? info "Preferences" -
+ The `preferences` prop allows you to customize the UI components provided by the SDK. -

Preferences

+ #### Theme Preferences (`preferences.theme`) -The `preferences` prop allows you to customize the UI components provided by the SDK. + | Property | Type | Default | Description | + |----------|------|---------|-------------| + | `inheritFromBranding` | `boolean` | `true` | Whether to inherit theme from Asgardeo organization/application branding | + | `mode` | `'light' \| 'dark' \| 'system'` | `'system'` | Theme mode. `'system'` follows user's OS preference | + | `overrides` | `ThemeConfig` | `{}` | Custom theme overrides for colors, typography, spacing, etc. | -#### Theme Preferences (`preferences.theme`) + #### Internationalization Preferences (`preferences.i18n`) -| Property | Type | Default | Description | -|----------|------|---------|-------------| -| `inheritFromBranding` | `boolean` | `true` | Whether to inherit theme from Asgardeo organization/application branding | -| `mode` | `'light' \| 'dark' \| 'system'` | `'system'` | Theme mode. `'system'` follows user's OS preference | -| `overrides` | `ThemeConfig` | `{}` | Custom theme overrides for colors, typography, spacing, etc. | - -#### Internationalization Preferences (`preferences.i18n`) - -| Property | Type | Default | Description | -|----------|------|---------|-------------| -| `language` | `string` | Browser default | Language code for UI text (e.g., `'en-US'`, `'es-ES'`) | -| `fallbackLanguage` | `string` | `'en-US'` | Fallback language when translations aren't available | -| `bundles` | `object` | `{}` | Custom translation bundles to override default text | - -
+ | Property | Type | Default | Description | + |----------|------|---------|-------------| + | `language` | `string` | Browser default | Language code for UI text (e.g., `'en-US'`, `'es-ES'`) | + | `fallbackLanguage` | `string` | `'en-US'` | Fallback language when translations aren't available | + | `bundles` | `object` | `{}` | Custom translation bundles to override default text | ## Usage diff --git a/packages/react/src/api/createUser.ts b/packages/react/src/api/createUser.ts new file mode 100644 index 00000000..baa8f669 --- /dev/null +++ b/packages/react/src/api/createUser.ts @@ -0,0 +1,104 @@ +/** + * 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 { + User, + HttpInstance, + AsgardeoSPAClient, + HttpRequestConfig, + createUser as baseCreateUser, + CreateUserConfig as BaseCreateUserConfig, +} from '@asgardeo/browser'; + +const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); + +/** + * Configuration for the createUser request (React-specific) + */ +export interface CreateUserConfig extends Omit { + /** + * Optional custom fetcher function. If not provided, the Asgardeo SPA client's httpClient will be used + * which is a wrapper around axios http.request + */ + fetcher?: (url: string, config: RequestInit) => Promise; +} + +/** + * Creates a new user. + * This function uses the Asgardeo 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 user information. + * @example + * ```typescript + * // Using default Asgardeo SPA client httpClient + * try { + * const user = await createUser({ + * baseUrl: "https://api.asgardeo.io/t/", + * payload: { ... } + * }); + * console.log(user); + * } catch (error) { + * if (error instanceof AsgardeoAPIError) { + * console.error('Failed to create user:', error.message); + * } + * } + * ``` + * + * @example + * ```typescript + * // Using custom fetcher + * try { + * const user = await createUser({ + * baseUrl: "https://api.asgardeo.io/t/", + * payload: { ... }, + * fetcher: customFetchFunction + * }); + * console.log(user); + * } catch (error) { + * if (error instanceof AsgardeoAPIError) { + * console.error('Failed to create user:', error.message); + * } + * } + * ``` + */ +const createUser = async ({fetcher, ...requestConfig}: CreateUserConfig): Promise => { + const defaultFetcher = async (url: string, config: RequestInit): Promise => { + const response = await httpClient({ + url, + method: config.method || 'POST', + headers: config.headers as Record, + data: config.body ? JSON.parse(config.body as string) : undefined, + } as HttpRequestConfig); + + 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; + }; + + return baseCreateUser({ + ...requestConfig, + fetcher: fetcher || defaultFetcher, + }); +}; + +export default createUser; diff --git a/packages/react/src/api/getUserstores.ts b/packages/react/src/api/getUserstores.ts new file mode 100644 index 00000000..5cc4b200 --- /dev/null +++ b/packages/react/src/api/getUserstores.ts @@ -0,0 +1,60 @@ +/** + * 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 { + Userstore, + getUserstores as baseGetUserstores, + GetUserstoresConfig as BaseGetUserstoresConfig, +} from '@asgardeo/browser'; +import {HttpInstance, AsgardeoSPAClient, HttpRequestConfig} from '@asgardeo/browser'; + +const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); + +export interface GetUserstoresConfig extends Omit { + fetcher?: (url: string, config: RequestInit) => Promise; +} + +/** + * Retrieves the userstores from the specified endpoint. + * This function uses the Asgardeo SPA client's httpClient by default, but allows for custom fetchers. + * + * @param config - Request configuration object. + * @returns A promise that resolves with the userstores information. + */ +const getUserstores = async ({fetcher, ...requestConfig}: GetUserstoresConfig): Promise => { + const defaultFetcher = async (url: string, config: RequestInit): Promise => { + const response = await httpClient({ + url, + method: config.method || 'GET', + headers: config.headers as Record, + } as HttpRequestConfig); + 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; + }; + return baseGetUserstores({ + ...requestConfig, + fetcher: fetcher || defaultFetcher, + }); +}; + +export default getUserstores; diff --git a/packages/react/src/components/presentation/CreateOrganization/CreateOrganization.tsx b/packages/react/src/components/presentation/CreateOrganization/CreateOrganization.tsx index 7b88c55a..c79c0bd1 100644 --- a/packages/react/src/components/presentation/CreateOrganization/CreateOrganization.tsx +++ b/packages/react/src/components/presentation/CreateOrganization/CreateOrganization.tsx @@ -34,7 +34,7 @@ export interface CreateOrganizationProps extends Omit Promise; + onCreate?: (payload: CreateOrganizationPayload) => Promise; } /** @@ -53,7 +53,7 @@ export interface CreateOrganizationProps extends Omit { + * onCreate={async (payload) => { * const result = await myCustomAPI.createOrganization(payload); * return result; * }} @@ -70,7 +70,7 @@ export interface CreateOrganizationProps extends Omit = ({ - onCreateOrganization, + onCreate, fallback = null, onSuccess, defaultParentId, @@ -99,8 +99,8 @@ export const CreateOrganization: FC = ({ try { let result: any; - if (onCreateOrganization) { - result = await onCreateOrganization(payload); + if (onCreate) { + result = await onCreate(payload); } else { if (!baseUrl) { throw new Error('Base URL is required for organization creation'); diff --git a/packages/react/src/components/presentation/CreateUser/BaseCreateUser.tsx b/packages/react/src/components/presentation/CreateUser/BaseCreateUser.tsx new file mode 100644 index 00000000..4c750cca --- /dev/null +++ b/packages/react/src/components/presentation/CreateUser/BaseCreateUser.tsx @@ -0,0 +1,348 @@ +/** + * 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 {CSSProperties, FC, ReactElement, ReactNode, useState} from 'react'; +import {useForm, FormField} from '../../../hooks/useForm'; +import useTheme from '../../../contexts/Theme/useTheme'; +import useTranslation from '../../../hooks/useTranslation'; +import useStyles from './CreateUser.styles'; +import Alert from '../../primitives/Alert/Alert'; +import Button from '../../primitives/Button/Button'; +import Dialog from '../../primitives/Dialog/Dialog'; +import FormControl from '../../primitives/FormControl/FormControl'; +import InputLabel from '../../primitives/InputLabel/InputLabel'; +import TextField from '../../primitives/TextField/TextField'; +import DatePicker from '../../primitives/DatePicker/DatePicker'; +import Checkbox from '../../primitives/Checkbox/Checkbox'; +import Select from '../../primitives/Select/Select'; + +export interface BaseCreateUserProps { + cardLayout?: boolean; + className?: string; + error?: string | null; + loading?: boolean; + mode?: 'inline' | 'popup'; + onCancel?: () => void; + onCreate: (payload: any) => Promise; + onPopupClose?: (open: boolean) => void; + onSuccess?: (user: any) => void; + popupOpen?: boolean; + renderAdditionalFields?: () => ReactNode; + schemas: any[]; + style?: CSSProperties; + title?: string; + userstores?: any; +} + +const BaseCreateUser: FC = ({ + cardLayout = true, + className = '', + error = null, + loading = false, + mode = 'inline', + onCancel, + onCreate, + onPopupClose, + onSuccess, + popupOpen = false, + renderAdditionalFields, + schemas, + style, + title = 'Create User', + userstores = [], +}): ReactElement => { + const {theme, colorScheme} = useTheme(); + const {t} = useTranslation(); + const styles = useStyles(theme, colorScheme); + + // Build form fields for useForm (flat attribute array) + const formFields: FormField[] = []; + // Add userstore dropdown field + formFields.push({ + name: 'userstore', + required: true, + initialValue: userstores.length > 0 ? userstores[0].id : '', + validator: value => { + if (!value) return t('field.required'); + return null; + }, + }); + schemas?.forEach(attr => { + if (attr.subAttributes && Array.isArray(attr.subAttributes)) { + attr.subAttributes.forEach(subAttr => { + formFields.push({ + name: `${attr.name}.${subAttr.name}`, + required: !!subAttr.required, + initialValue: '', + validator: value => { + if (subAttr.required && (!value || value.trim() === '')) { + return t('field.required'); + } + return null; + }, + }); + }); + } else { + formFields.push({ + name: attr.name, + required: !!attr.required, + initialValue: '', + validator: value => { + if (attr.required && (!value || value.trim() === '')) { + return t('field.required'); + } + return null; + }, + }); + } + }); + + const form = useForm>({ + initialValues: {}, + fields: formFields, + validateOnBlur: true, + validateOnChange: true, + requiredMessage: t('field.required'), + }); + + const { + values: formState, + errors, + touched, + isValid: isFormValid, + setValue, + setTouched, + validateForm, + touchAllFields, + reset: resetForm, + } = form; + + const handleChange = (name: string, value: any) => { + setValue(name, value); + setTouched(name, true); + }; + + const handleSubmit = async (e: React.FormEvent): Promise => { + e.preventDefault(); + touchAllFields(); + const validation = validateForm(); + if (!validation.isValid || loading) return; + + try { + await onCreate(formState); + + if (onSuccess) { + onSuccess(formState); + } + + resetForm(); + } catch (submitError) { + // Error handling is done by parent component + console.error('Form submission error:', submitError); + } + }; + + // Helper to get placeholder + const getFieldPlaceholder = (schema: any): string => { + const {type, displayName, description, name} = schema; + const fieldLabel = displayName || description || name || 'value'; + switch (type) { + case 'DATE_TIME': + return `Enter your ${fieldLabel.toLowerCase()}`; + case 'BOOLEAN': + return `Select ${fieldLabel.toLowerCase()}`; + case 'COMPLEX': + return `Enter ${fieldLabel.toLowerCase()} details`; + default: + return `Enter your ${fieldLabel.toLowerCase()}`; + } + }; + + // Render a single field based on type + const renderField = (fieldName: string, schema: any) => { + const {type, required} = schema; + const value = formState[fieldName] || ''; + const errorMsg = touched[fieldName] && errors[fieldName] ? errors[fieldName] : undefined; + + switch (type) { + case 'STRING': + return ( + handleChange(fieldName, e.target.value)} + required={!!required} + error={errorMsg} + placeholder={getFieldPlaceholder(schema)} + disabled={loading} + /> + ); + case 'DATE_TIME': + return ( + handleChange(fieldName, e.target.value)} + required={!!required} + error={errorMsg} + placeholder={getFieldPlaceholder(schema)} + disabled={loading} + /> + ); + case 'BOOLEAN': + return ( + handleChange(fieldName, e.target.checked)} + required={!!required} + label={getFieldPlaceholder(schema)} + error={errorMsg} + disabled={loading} + /> + ); + case 'COMPLEX': + return ( + +