diff --git a/.changeset/metal-colts-lose.md b/.changeset/metal-colts-lose.md index 1690cee8..5cd94472 100644 --- a/.changeset/metal-colts-lose.md +++ b/.changeset/metal-colts-lose.md @@ -1,5 +1,6 @@ --- -'@asgardeo/nextjs': patch +'@asgardeo/nextjs': minor +'@asgardeo/react': patch --- Stabilize the SDK diff --git a/packages/javascript/src/__legacy__/client.ts b/packages/javascript/src/__legacy__/client.ts index 42f3975f..0cfd1ef4 100644 --- a/packages/javascript/src/__legacy__/client.ts +++ b/packages/javascript/src/__legacy__/client.ts @@ -237,13 +237,14 @@ export class AsgardeoAuthClient { await this._storageManager.setTemporaryDataParameter(pkceKey, codeVerifier, userId); } - console.log('[AsgardeoAuthClient] configData:', configData); + if (authRequestConfig['client_secret']) { + authRequestConfig['client_secret'] = configData.clientSecret; + } const authorizeRequestParams: Map = getAuthorizeRequestUrlParams( { redirectUri: configData.afterSignInUrl, clientId: configData.clientId, - clientSecret: configData.clientSecret, scopes: processOpenIDScopes(configData.scopes), responseMode: configData.responseMode, codeChallengeMethod: PKCEConstants.DEFAULT_CODE_CHALLENGE_METHOD, diff --git a/packages/javascript/src/api/createOrganization.ts b/packages/javascript/src/api/createOrganization.ts new file mode 100644 index 00000000..6e004111 --- /dev/null +++ b/packages/javascript/src/api/createOrganization.ts @@ -0,0 +1,212 @@ +/** + * 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 '../models/organization'; +import AsgardeoAPIError from '../errors/AsgardeoAPIError'; + +/** + * Interface for organization creation payload. + */ +export interface CreateOrganizationPayload { + /** + * Organization description. + */ + description: string; + /** + * Organization handle/slug. + */ + orgHandle?: string; + /** + * Organization name. + */ + name: 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; + /** + * Organization creation payload + */ + payload: CreateOrganizationPayload; + /** + * Optional custom fetcher function. + * If not provided, native fetch will be used + */ + fetcher?: (url: string, config: RequestInit) => Promise; +} + +/** + * 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://api.asgardeo.io/t/", + * 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 AsgardeoAPIError) { + * 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://api.asgardeo.io/t/", + * 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 AsgardeoAPIError) { + * console.error('Failed to create organization:', error.message); + * } + * } + * ``` + */ +const createOrganization = async ({ + baseUrl, + payload, + fetcher, + ...requestConfig +}: CreateOrganizationConfig): Promise => { + try { + new URL(baseUrl); + } catch (error) { + throw new AsgardeoAPIError( + `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 AsgardeoAPIError( + 'Organization payload is required', + 'createOrganization-ValidationError-002', + 'javascript', + 400, + 'Invalid Request', + ); + } + + // Always set type to TENANT for now + const organizationPayload = { + ...payload, + type: 'TENANT' as const, + }; + + const fetchFn = fetcher || fetch; + const resolvedUrl = `${baseUrl}/api/server/v1/organizations`; + + const requestInit: RequestInit = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...requestConfig.headers, + }, + body: JSON.stringify(organizationPayload), + ...requestConfig, + }; + + try { + const response: Response = await fetchFn(resolvedUrl, requestInit); + + if (!response?.ok) { + const errorText = await response.text(); + + throw new AsgardeoAPIError( + `Failed to create organization: ${errorText}`, + 'createOrganization-ResponseError-001', + 'javascript', + response.status, + response.statusText, + ); + } + + return (await response.json()) as Organization; + } catch (error) { + if (error instanceof AsgardeoAPIError) { + throw error; + } + + throw new AsgardeoAPIError( + `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 new file mode 100644 index 00000000..0cd7223c --- /dev/null +++ b/packages/javascript/src/api/getAllOrganizations.ts @@ -0,0 +1,197 @@ +/** + * 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 '../models/organization'; +import AsgardeoAPIError from '../errors/AsgardeoAPIError'; + +/** + * Interface for paginated organization response. + */ +export interface PaginatedOrganizationsResponse { + hasMore?: boolean; + nextCursor?: string; + organizations: Organization[]; + totalCount?: number; +} + +/** + * Configuration for the getAllOrganizations request + */ +export interface GetAllOrganizationsConfig extends Omit { + /** + * The base URL for the API endpoint. + */ + baseUrl: string; + /** + * Filter expression for organizations + */ + filter?: string; + /** + * Maximum number of organizations to return + */ + limit?: number; + /** + * Whether to include child organizations recursively + */ + recursive?: boolean; + /** + * Optional custom fetcher function. + * If not provided, native fetch will be used + */ + fetcher?: (url: string, config: RequestInit) => Promise; +} + +/** + * 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://api.asgardeo.io/t/", + * filter: "", + * limit: 10, + * recursive: false + * }); + * console.log(response.organizations); + * } catch (error) { + * if (error instanceof AsgardeoAPIError) { + * 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://api.asgardeo.io/t/", + * 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 AsgardeoAPIError) { + * console.error('Failed to get organizations:', error.message); + * } + * } + * ``` + */ +const getAllOrganizations = async ({ + baseUrl, + filter = '', + limit = 10, + recursive = false, + fetcher, + ...requestConfig +}: GetAllOrganizationsConfig): Promise => { + try { + new URL(baseUrl); + } catch (error) { + throw new AsgardeoAPIError( + `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 = fetcher || fetch; + const resolvedUrl = `${baseUrl}/api/server/v1/organizations?${queryParams.toString()}`; + + const requestInit: RequestInit = { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...requestConfig.headers, + }, + ...requestConfig, + }; + + try { + const response: Response = await fetchFn(resolvedUrl, requestInit); + + if (!response?.ok) { + const errorText = await response.text(); + + throw new AsgardeoAPIError( + `Failed to get organizations: ${errorText}`, + 'getAllOrganizations-ResponseError-001', + 'javascript', + response.status, + response.statusText, + ); + } + + const data = (await response.json()) as any; + + return { + hasMore: data.hasMore, + nextCursor: data.nextCursor, + organizations: data.organizations || [], + totalCount: data.totalCount, + }; + } catch (error) { + if (error instanceof AsgardeoAPIError) { + throw error; + } + + throw new AsgardeoAPIError( + `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 new file mode 100644 index 00000000..159a4158 --- /dev/null +++ b/packages/javascript/src/api/getMeOrganizations.ts @@ -0,0 +1,203 @@ +/** + * 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 '../models/organization'; +import AsgardeoAPIError from '../errors/AsgardeoAPIError'; + +/** + * Configuration for the getMeOrganizations request + */ +export interface GetMeOrganizationsConfig extends Omit { + /** + * The base URL for the API endpoint. + */ + baseUrl: string; + /** + * Base64 encoded cursor value for forward pagination + */ + after?: string; + /** + * Authorized application name filter + */ + authorizedAppName?: string; + /** + * Base64 encoded cursor value for backward pagination + */ + before?: string; + /** + * Filter expression for organizations + */ + filter?: string; + /** + * Maximum number of organizations to return + */ + limit?: number; + /** + * Whether to include child organizations recursively + */ + recursive?: boolean; + /** + * Optional custom fetcher function. + * If not provided, native fetch will be used + */ + fetcher?: (url: string, config: RequestInit) => Promise; +} + +/** + * 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://api.asgardeo.io/t/", + * after: "", + * before: "", + * filter: "", + * limit: 10, + * recursive: false + * }); + * console.log(organizations); + * } catch (error) { + * if (error instanceof AsgardeoAPIError) { + * 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://api.asgardeo.io/t/", + * 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 AsgardeoAPIError) { + * 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 { + new URL(baseUrl); + } catch (error) { + throw new AsgardeoAPIError( + `Invalid base URL provided. ${error?.toString()}`, + 'getMeOrganizations-ValidationError-001', + 'javascript', + 400, + 'The provided `baseUrl` does not adhere to the URL schema.', + ); + } + + const queryParams = new URLSearchParams( + Object.fromEntries( + Object.entries({ + after, + authorizedAppName, + before, + filter, + limit: limit.toString(), + recursive: recursive.toString(), + }).filter(([, value]) => Boolean(value)), + ), + ); + + const fetchFn = fetcher || fetch; + const resolvedUrl = `${baseUrl}/api/users/v1/me/organizations?${queryParams.toString()}`; + + const requestInit: RequestInit = { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...requestConfig.headers, + }, + ...requestConfig, + }; + + try { + const response: Response = await fetchFn(resolvedUrl, requestInit); + + if (!response?.ok) { + const errorText = await response.text(); + + throw new AsgardeoAPIError( + `Failed to fetch associated organizations of the user: ${errorText}`, + 'getMeOrganizations-ResponseError-001', + 'javascript', + response.status, + response.statusText, + ); + } + + const data = (await response.json()) as any; + return data.organizations || []; + } catch (error) { + if (error instanceof AsgardeoAPIError) { + throw error; + } + + throw new AsgardeoAPIError( + `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 new file mode 100644 index 00000000..0c791215 --- /dev/null +++ b/packages/javascript/src/api/getOrganization.ts @@ -0,0 +1,185 @@ +/** + * 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'; + +/** + * 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; + /** + * The ID of the organization to retrieve + */ + organizationId: string; + /** + * Optional custom fetcher function. + * If not provided, native fetch will be used + */ + fetcher?: (url: string, config: RequestInit) => Promise; +} + +/** + * 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://api.asgardeo.io/t/dxlab", + * organizationId: "0d5e071b-d3d3-475d-b3c6-1a20ee2fa9b1" + * }); + * console.log(organization); + * } catch (error) { + * if (error instanceof AsgardeoAPIError) { + * 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://api.asgardeo.io/t/dxlab", + * 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 AsgardeoAPIError) { + * console.error('Failed to get organization:', error.message); + * } + * } + * ``` + */ +const getOrganization = async ({ + baseUrl, + organizationId, + fetcher, + ...requestConfig +}: GetOrganizationConfig): Promise => { + try { + new URL(baseUrl); + } catch (error) { + throw new AsgardeoAPIError( + `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 AsgardeoAPIError( + 'Organization ID is required', + 'getOrganization-ValidationError-002', + 'javascript', + 400, + 'Invalid Request', + ); + } + + const fetchFn = fetcher || fetch; + const resolvedUrl = `${baseUrl}/api/server/v1/organizations/${organizationId}`; + + const requestInit: RequestInit = { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...requestConfig.headers, + }, + ...requestConfig, + }; + + try { + const response: Response = await fetchFn(resolvedUrl, requestInit); + + if (!response?.ok) { + const errorText = await response.text(); + + throw new AsgardeoAPIError( + `Failed to fetch organization details: ${errorText}`, + 'getOrganization-ResponseError-001', + 'javascript', + response.status, + response.statusText, + ); + } + + return (await response.json()) as OrganizationDetails; + } catch (error) { + if (error instanceof AsgardeoAPIError) { + throw error; + } + + throw new AsgardeoAPIError( + `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/getSchemas.ts b/packages/javascript/src/api/getSchemas.ts new file mode 100644 index 00000000..495872f2 --- /dev/null +++ b/packages/javascript/src/api/getSchemas.ts @@ -0,0 +1,149 @@ +/** + * 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} from '../models/scim2-schema'; +import AsgardeoAPIError from '../errors/AsgardeoAPIError'; + +/** + * Configuration for the getSchemas request + */ +export interface GetSchemasConfig extends Omit { + /** + * The absolute API endpoint. + */ + url?: string; + /** + * The base path of the API endpoint. + */ + baseUrl?: string; + /** + * Optional custom fetcher function. + * If not provided, native fetch will be used + */ + fetcher?: (url: string, config: RequestInit) => Promise; +} + +/** + * Retrieves the SCIM2 schemas from the specified endpoint. + * + * @param config - Request configuration object. + * @returns A promise that resolves with the SCIM2 schemas information. + * @example + * ```typescript + * // Using default fetch + * try { + * const schemas = await getSchemas({ + * url: "https://api.asgardeo.io/t//scim2/Schemas", + * }); + * console.log(schemas); + * } catch (error) { + * if (error instanceof AsgardeoAPIError) { + * console.error('Failed to get schemas:', error.message); + * } + * } + * ``` + * + * @example + * ```typescript + * // Using custom fetcher (e.g., axios-based httpClient) + * try { + * const schemas = await getSchemas({ + * url: "https://api.asgardeo.io/t//scim2/Schemas", + * 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(schemas); + * } catch (error) { + * if (error instanceof AsgardeoAPIError) { + * console.error('Failed to get schemas:', error.message); + * } + * } + * ``` + */ +const getSchemas = async ({url, baseUrl, fetcher, ...requestConfig}: GetSchemasConfig): Promise => { + try { + new URL(url ?? baseUrl); + } catch (error) { + throw new AsgardeoAPIError( + `Invalid URL provided. ${error?.toString()}`, + 'getSchemas-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/Schemas`; + + const requestInit: RequestInit = { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...requestConfig.headers, + }, + ...requestConfig, + }; + + try { + const response: Response = await fetchFn(resolvedUrl, requestInit); + + if (!response?.ok) { + const errorText = await response.text(); + + throw new AsgardeoAPIError( + `Failed to fetch SCIM2 schemas: ${errorText}`, + 'getSchemas-ResponseError-001', + 'javascript', + response.status, + response.statusText, + ); + } + + return (await response.json()) as Schema[]; + } catch (error) { + if (error instanceof AsgardeoAPIError) { + throw error; + } + + throw new AsgardeoAPIError( + `Network or parsing error: ${error instanceof Error ? error.message : 'Unknown error'}`, + 'getSchemas-NetworkError-001', + 'javascript', + 0, + 'Network Error', + ); + } +}; + +export default getSchemas; diff --git a/packages/javascript/src/api/initializeEmbeddedSignInFlow.ts b/packages/javascript/src/api/initializeEmbeddedSignInFlow.ts index 7e42c7c0..b0ed7623 100644 --- a/packages/javascript/src/api/initializeEmbeddedSignInFlow.ts +++ b/packages/javascript/src/api/initializeEmbeddedSignInFlow.ts @@ -73,8 +73,6 @@ const initializeEmbeddedSignInFlow = async ({ } }); - console.log('Executing embedded sign-in flow with payload:', url, searchParams.toString()); - const {headers: customHeaders, ...otherConfig} = requestConfig; const response: Response = await fetch(url ?? `${baseUrl}/oauth2/authorize`, { method: requestConfig.method || 'POST', diff --git a/packages/javascript/src/api/scim2/__tests__/getMeProfile.test.ts b/packages/javascript/src/api/scim2/__tests__/getMeProfile.test.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/javascript/src/api/scim2/getMeProfile.ts b/packages/javascript/src/api/scim2/getMeProfile.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/javascript/src/api/scim2/index.ts b/packages/javascript/src/api/scim2/index.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/javascript/src/api/updateMeProfile.ts b/packages/javascript/src/api/updateMeProfile.ts new file mode 100644 index 00000000..e95ed481 --- /dev/null +++ b/packages/javascript/src/api/updateMeProfile.ts @@ -0,0 +1,159 @@ +/** + * 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 updateMeProfile request + */ +export interface UpdateMeProfileConfig extends Omit { + /** + * The absolute API endpoint. + */ + url?: string; + /** + * The base path of the API endpoint. + */ + baseUrl?: string; + /** + * The value object to patch (SCIM2 PATCH value) + */ + payload: any; + /** + * Optional custom fetcher function. + * If not provided, native fetch will be used + */ + fetcher?: (url: string, config: RequestInit) => Promise; +} + +/** + * Updates the user profile information at the specified SCIM2 Me endpoint. + * + * @param config - Configuration object with URL, payload and optional request config. + * @returns A promise that resolves with the updated user profile information. + * @example + * ```typescript + * // Using default fetch + * await updateMeProfile({ + * url: "https://api.asgardeo.io/t//scim2/Me", + * payload: { "urn:scim:wso2:schema": { mobileNumbers: ["0777933830"] } } + * }); + * ``` + * + * @example + * ```typescript + * // Using custom fetcher (e.g., axios-based httpClient) + * await updateMeProfile({ + * url: "https://api.asgardeo.io/t//scim2/Me", + * payload: { "urn:scim:wso2:schema": { mobileNumbers: ["0777933830"] } }, + * 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 updateMeProfile = async ({ + url, + baseUrl, + payload, + fetcher, + ...requestConfig +}: UpdateMeProfileConfig): Promise => { + try { + new URL(url ?? baseUrl); + } catch (error) { + throw new AsgardeoAPIError( + `Invalid URL provided. ${error?.toString()}`, + 'updateMeProfile-ValidationError-001', + 'javascript', + 400, + 'The provided `url` or `baseUrl` path does not adhere to the URL schema.', + ); + } + + const data = { + Operations: [ + { + op: 'replace', + value: payload, + }, + ], + schemas: ['urn:ietf:params:scim:api:messages:2.0:PatchOp'], + }; + + const fetchFn = fetcher || fetch; + const resolvedUrl: string = url ?? `${baseUrl}/scim2/Me`; + + const requestInit: RequestInit = { + method: 'PATCH', + headers: { + 'Content-Type': 'application/scim+json', + Accept: 'application/json', + ...requestConfig.headers, + }, + body: JSON.stringify(data), + ...requestConfig, + }; + + try { + const response: Response = await fetchFn(resolvedUrl, requestInit); + + if (!response?.ok) { + const errorText = await response.text(); + + throw new AsgardeoAPIError( + `Failed to update user profile: ${errorText}`, + 'updateMeProfile-ResponseError-001', + 'javascript', + response.status, + response.statusText, + ); + } + + return (await response.json()) as User; + } catch (error) { + if (error instanceof AsgardeoAPIError) { + throw error; + } + + throw new AsgardeoAPIError( + `Network or parsing error: ${error instanceof Error ? error.message : 'Unknown error'}`, + 'updateMeProfile-NetworkError-001', + 'javascript', + 0, + 'Network Error', + ); + } +}; + +export default updateMeProfile; diff --git a/packages/react/src/api/scim2/updateOrganization.ts b/packages/javascript/src/api/updateOrganization.ts similarity index 56% rename from packages/react/src/api/scim2/updateOrganization.ts rename to packages/javascript/src/api/updateOrganization.ts index 0214803e..64c83866 100644 --- a/packages/react/src/api/scim2/updateOrganization.ts +++ b/packages/javascript/src/api/updateOrganization.ts @@ -16,18 +16,41 @@ * under the License. */ -import {AsgardeoAPIError, HttpInstance, AsgardeoSPAClient, HttpRequestConfig, isEmpty} from '@asgardeo/browser'; +import AsgardeoAPIError from '../errors/AsgardeoAPIError'; +import isEmpty from '../utils/isEmpty'; import {OrganizationDetails} from './getOrganization'; -const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); +/** + * Configuration for the updateOrganization request + */ +export interface UpdateOrganizationConfig extends Omit { + /** + * The base URL for the API endpoint. + */ + baseUrl: string; + /** + * The ID of the organization to update + */ + organizationId: string; + /** + * Array of patch operations to apply + */ + operations: Array<{ + operation: 'REPLACE' | 'ADD' | 'REMOVE'; + path: string; + value?: any; + }>; + /** + * Optional custom fetcher function. + * If not provided, native fetch will be used + */ + fetcher?: (url: string, config: RequestInit) => Promise; +} /** * Updates the organization information using the Organizations Management API. * - * @param baseUrl - The base URL for the API. - * @param organizationId - The ID of the organization to update. - * @param operations - Array of patch operations to apply. - * @param requestConfig - Additional request config if needed. + * @param config - Configuration object with baseUrl, organizationId, operations and optional request config. * @returns A promise that resolves with the updated organization information. * @example * ```typescript @@ -54,30 +77,52 @@ const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bin * ] * }); * ``` + * + * @example + * ```typescript + * // Using custom fetcher (e.g., axios-based httpClient) + * await updateOrganization({ + * baseUrl: "https://api.asgardeo.io/t/", + * 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 -}: { - baseUrl: string; - organizationId: string; - operations: Array<{ - operation: 'REPLACE' | 'ADD' | 'REMOVE'; - path: string; - value?: any; - }>; -} & Partial): Promise => { +}: UpdateOrganizationConfig): Promise => { try { new URL(baseUrl); } catch (error) { throw new AsgardeoAPIError( - 'Invalid base URL provided', + `Invalid base URL provided. ${error?.toString()}`, 'updateOrganization-ValidationError-001', 'javascript', 400, - 'Invalid Request', + 'The provided `baseUrl` does not adhere to the URL schema.', ); } @@ -101,32 +146,49 @@ const updateOrganization = async ({ ); } - const url = `${baseUrl}/api/server/v1/organizations/${organizationId}`; + const fetchFn = fetcher || fetch; + const resolvedUrl = `${baseUrl}/api/server/v1/organizations/${organizationId}`; - const response: any = await httpClient({ - url, + const requestInit: RequestInit = { method: 'PATCH', headers: { 'Content-Type': 'application/json', Accept: 'application/json', + ...requestConfig.headers, }, - data: operations, + body: JSON.stringify(operations), ...requestConfig, - } as HttpRequestConfig); + }; - if (!response.data) { - const errorText = await response.text(); + try { + const response: Response = await fetchFn(resolvedUrl, requestInit); + + if (!response?.ok) { + const errorText = await response.text(); + + throw new AsgardeoAPIError( + `Failed to update organization: ${errorText}`, + 'updateOrganization-ResponseError-001', + 'javascript', + response.status, + response.statusText, + ); + } + + return (await response.json()) as OrganizationDetails; + } catch (error) { + if (error instanceof AsgardeoAPIError) { + throw error; + } throw new AsgardeoAPIError( - `Failed to update organization: ${errorText}`, - 'updateOrganization-ResponseError-001', + `Network or parsing error: ${error instanceof Error ? error.message : 'Unknown error'}`, + 'updateOrganization-NetworkError-001', 'javascript', - response.status, - response.statusText, + 0, + 'Network Error', ); } - - return response.data; }; /** diff --git a/packages/javascript/src/constants/OIDCRequestConstants.ts b/packages/javascript/src/constants/OIDCRequestConstants.ts index 3a5e8319..edb91369 100644 --- a/packages/javascript/src/constants/OIDCRequestConstants.ts +++ b/packages/javascript/src/constants/OIDCRequestConstants.ts @@ -59,7 +59,7 @@ const OIDCRequestConstants = { /** * The default scopes used in OIDC sign-in requests. */ - DEFAULT_SCOPES: [ScopeConstants.OPENID, ScopeConstants.INTERNAL_LOGIN], + DEFAULT_SCOPES: [ScopeConstants.OPENID, ScopeConstants.PROFILE, ScopeConstants.INTERNAL_LOGIN], }, }, diff --git a/packages/javascript/src/constants/ScopeConstants.ts b/packages/javascript/src/constants/ScopeConstants.ts index a54c696e..88ce28aa 100644 --- a/packages/javascript/src/constants/ScopeConstants.ts +++ b/packages/javascript/src/constants/ScopeConstants.ts @@ -38,6 +38,7 @@ const ScopeConstants: { INTERNAL_LOGIN: string; OPENID: string; + PROFILE: string; } = { /** * The scope for accessing the user's profile information from SCIM. @@ -52,6 +53,13 @@ const ScopeConstants: { * is initiating an OpenID Connect authentication request. */ OPENID: 'openid', + + /** + * The OpenID Connect profile scope. + * This scope allows the client to access the user's profile information. + * It includes details such as the user's name, email, and other profile attributes. + */ + PROFILE: 'profile', } as const; export default ScopeConstants; diff --git a/packages/javascript/src/index.ts b/packages/javascript/src/index.ts index 2a110aed..6c1aa6e2 100644 --- a/packages/javascript/src/index.ts +++ b/packages/javascript/src/index.ts @@ -26,6 +26,19 @@ export {default as executeEmbeddedSignInFlow} from './api/executeEmbeddedSignInF export {default as executeEmbeddedSignUpFlow} from './api/executeEmbeddedSignUpFlow'; export {default as getUserInfo} from './api/getUserInfo'; export {default as getScim2Me, GetScim2MeConfig} from './api/getScim2Me'; +export {default as getSchemas, GetSchemasConfig} from './api/getSchemas'; +export {default as getAllOrganizations} from './api/getAllOrganizations'; +export {default as createOrganization} from './api/createOrganization'; +export {default as getMeOrganizations} from './api/getMeOrganizations'; +export {default as getOrganization} from './api/getOrganization'; +export {default as updateOrganization, createPatchOperations} from './api/updateOrganization'; +export {default as updateMeProfile} from './api/updateMeProfile'; +export type {PaginatedOrganizationsResponse, GetAllOrganizationsConfig} from './api/getAllOrganizations'; +export type {CreateOrganizationPayload, CreateOrganizationConfig} from './api/createOrganization'; +export type {GetMeOrganizationsConfig} from './api/getMeOrganizations'; +export type {OrganizationDetails, GetOrganizationConfig} from './api/getOrganization'; +export type {UpdateOrganizationConfig} from './api/updateOrganization'; +export type {UpdateMeProfileConfig} from './api/updateMeProfile'; export {default as ApplicationNativeAuthenticationConstants} from './constants/ApplicationNativeAuthenticationConstants'; export {default as TokenConstants} from './constants/TokenConstants'; diff --git a/packages/javascript/src/utils/getAuthorizeRequestUrlParams.ts b/packages/javascript/src/utils/getAuthorizeRequestUrlParams.ts index c7d69e5c..3d17216e 100644 --- a/packages/javascript/src/utils/getAuthorizeRequestUrlParams.ts +++ b/packages/javascript/src/utils/getAuthorizeRequestUrlParams.ts @@ -55,7 +55,6 @@ const getAuthorizeRequestUrlParams = ( options: { redirectUri: string; clientId: string; - clientSecret?: string; scopes?: string; responseMode?: string; codeChallenge?: string; @@ -79,10 +78,6 @@ const getAuthorizeRequestUrlParams = ( authorizeRequestParams.set('response_mode', responseMode as string); } - if (clientSecret) { - authorizeRequestParams.set('client_secret', clientSecret as string); - } - const pkceKey: string = pkceOptions?.key; if (codeChallenge) { diff --git a/packages/nextjs/src/AsgardeoNextClient.ts b/packages/nextjs/src/AsgardeoNextClient.ts index c93b996d..9c8de194 100644 --- a/packages/nextjs/src/AsgardeoNextClient.ts +++ b/packages/nextjs/src/AsgardeoNextClient.ts @@ -32,14 +32,20 @@ import { EmbeddedSignInFlowHandleRequestPayload, executeEmbeddedSignInFlow, EmbeddedFlowExecuteRequestConfig, - CookieConfig, - generateSessionId, - EmbeddedSignInFlowStatus, + ExtendedAuthorizeRequestUrlParams, + generateUserProfile, + flattenUserSchema, + getScim2Me, + getSchemas, + generateFlattenedUserProfile, + updateMeProfile, + executeEmbeddedSignUpFlow, } from '@asgardeo/node'; import {NextRequest, NextResponse} from 'next/server'; import {AsgardeoNextConfig} from './models/config'; import getSessionId from './server/actions/getSessionId'; import decorateConfigWithNextEnv from './utils/decorateConfigWithNextEnv'; +import getClientOrigin from './server/actions/getClientOrigin'; const removeTrailingSlash = (path: string): string => (path.endsWith('/') ? path.slice(0, -1) : path); /** @@ -53,7 +59,7 @@ const removeTrailingSlash = (path: string): string => (path.endsWith('/') ? path class AsgardeoNextClient extends AsgardeoNodeClient { private static instance: AsgardeoNextClient; private asgardeo: LegacyAsgardeoNodeClient; - private isInitialized: boolean = false; + public isInitialized: boolean = false; private constructor() { super(); @@ -83,7 +89,7 @@ class AsgardeoNextClient exte } } - override initialize(config: T): Promise { + override async initialize(config: T): Promise { if (this.isInitialized) { console.warn('[AsgardeoNextClient] Client is already initialized'); return Promise.resolve(true); @@ -94,17 +100,7 @@ class AsgardeoNextClient exte this.isInitialized = true; - console.log('[AsgardeoNextClient] Initializing with decorateConfigWithNextEnv:', { - baseUrl, - clientId, - clientSecret, - signInUrl, - signUpUrl, - afterSignInUrl, - afterSignOutUrl, - enablePKCE: false, - ...rest, - }); + const origin: string = await getClientOrigin(); return this.asgardeo.initialize({ baseUrl, @@ -112,24 +108,105 @@ class AsgardeoNextClient exte clientSecret, signInUrl, signUpUrl, - afterSignInUrl, - afterSignOutUrl, + afterSignInUrl: afterSignInUrl ?? origin, + afterSignOutUrl: afterSignOutUrl ?? origin, enablePKCE: false, ...rest, } as any); } override async getUser(userId?: string): Promise { + await this.ensureInitialized(); const resolvedSessionId: string = userId || ((await getSessionId()) as string); - return this.asgardeo.getUser(resolvedSessionId); + try { + const configData = await this.asgardeo.getConfigData(); + const baseUrl = configData?.baseUrl; + + const profile = await getScim2Me({ + baseUrl, + headers: { + Authorization: `Bearer ${await this.getAccessToken(userId)}`, + }, + }); + + const schemas = await getSchemas({ + baseUrl, + headers: { + Authorization: `Bearer ${await this.getAccessToken(userId)}`, + }, + }); + + return generateUserProfile(profile, flattenUserSchema(schemas)); + } catch (error) { + return this.asgardeo.getUser(resolvedSessionId); + } } - override async getOrganizations(): Promise { - throw new Error('Method not implemented.'); + override async getUserProfile(userId?: string): Promise { + await this.ensureInitialized(); + + try { + const configData = await this.asgardeo.getConfigData(); + const baseUrl = configData?.baseUrl; + + const profile = await getScim2Me({ + baseUrl, + headers: { + Authorization: `Bearer ${await this.getAccessToken(userId)}`, + }, + }); + + const schemas = await getSchemas({ + baseUrl, + headers: { + Authorization: `Bearer ${await this.getAccessToken(userId)}`, + }, + }); + + const processedSchemas = flattenUserSchema(schemas); + + const output = { + schemas: processedSchemas, + flattenedProfile: generateFlattenedUserProfile(profile, processedSchemas), + profile, + }; + + return output; + } catch (error) { + return { + schemas: [], + flattenedProfile: await this.asgardeo.getDecodedIdToken(), + profile: await this.asgardeo.getDecodedIdToken(), + }; + } + } + + async updateUserProfile(payload: any, userId?: string) { + await this.ensureInitialized(); + + try { + const configData = await this.asgardeo.getConfigData(); + const baseUrl = configData?.baseUrl; + + return await updateMeProfile({ + baseUrl, + payload, + headers: { + Authorization: `Bearer ${await this.getAccessToken(userId)}`, + }, + }); + } catch (error) { + throw new AsgardeoRuntimeError( + `Failed to update user profile: ${error instanceof Error ? error.message : 'Unknown error'}`, + 'AsgardeoNextClient-UpdateProfileError-001', + 'react', + 'An error occurred while updating the user profile. Please check your configuration and network connection.', + ); + } } - override getUserProfile(): Promise { + override async getOrganizations(): Promise { throw new Error('Method not implemented.'); } @@ -149,6 +226,10 @@ class AsgardeoNextClient exte return this.asgardeo.isSignedIn(sessionId as string); } + getAccessToken(sessionId?: string): Promise { + return this.asgardeo.getAccessToken(sessionId as string); + } + override getConfiguration(): T { return this.asgardeo.getConfigData() as unknown as T; } @@ -170,11 +251,18 @@ class AsgardeoNextClient exte const arg3 = args[2]; const arg4 = args[3]; - if (typeof arg1 === 'object' && 'flowId' in arg1 && typeof arg1 === 'object' && 'url' in arg2) { + if (typeof arg1 === 'object' && 'flowId' in arg1) { if (arg1.flowId === '') { + const defaultSignInUrl: URL = new URL( + await this.getAuthorizeRequestUrl({ + response_mode: 'direct', + client_secret: '{{clientSecret}}', + }), + ); + return initializeEmbeddedSignInFlow({ - payload: arg2.payload, - url: arg2.url, + url: `${defaultSignInUrl.origin}${defaultSignInUrl.pathname}`, + payload: Object.fromEntries(defaultSignInUrl.searchParams.entries()), }); } @@ -213,11 +301,31 @@ class AsgardeoNextClient exte override async signUp(options?: SignUpOptions): Promise; override async signUp(payload: EmbeddedFlowExecuteRequestPayload): Promise; override async signUp(...args: any[]): Promise { + if (args.length === 0) { + throw new AsgardeoRuntimeError( + 'No arguments provided for signUp method.', + 'AsgardeoNextClient-ValidationError-001', + 'nextjs', + 'The signUp method requires at least one argument, either a SignUpOptions object or an EmbeddedFlowExecuteRequestPayload.', + ); + } + + const firstArg = args[0]; + + if (typeof firstArg === 'object' && 'flowType' in firstArg) { + const configData = await this.asgardeo.getConfigData(); + const baseUrl = configData?.baseUrl; + + return executeEmbeddedSignUpFlow({ + baseUrl, + payload: firstArg as EmbeddedFlowExecuteRequestPayload, + }); + } throw new AsgardeoRuntimeError( 'Not implemented', - 'react-AsgardeoReactClient-ValidationError-002', - 'react', - 'The signUp method with SignUpOptions is not implemented in the React client.', + 'AsgardeoNextClient-ValidationError-002', + 'nextjs', + 'The signUp method with SignUpOptions is not implemented in the Next.js client.', ); } @@ -225,25 +333,16 @@ class AsgardeoNextClient exte * Gets the sign-in URL for authentication. * Ensures the client is initialized before making the call. * + * @param customParams - Custom parameters to include in the sign-in URL. * @param userId - The user ID * @returns Promise that resolves to the sign-in URL */ - public async getSignInUrl(userId?: string): Promise { - await this.ensureInitialized(); - return this.asgardeo.getSignInUrl(undefined, userId); - } - - /** - * Gets the sign-in URL for authentication with custom request config. - * Ensures the client is initialized before making the call. - * - * @param requestConfig - Custom request configuration - * @param userId - The user ID - * @returns Promise that resolves to the sign-in URL - */ - public async getSignInUrlWithConfig(requestConfig?: any, userId?: string): Promise { + public async getAuthorizeRequestUrl( + customParams: ExtendedAuthorizeRequestUrlParams, + userId?: string, + ): Promise { await this.ensureInitialized(); - return this.asgardeo.getSignInUrl(requestConfig, userId); + return this.asgardeo.getSignInUrl(customParams, userId); } /** diff --git a/packages/nextjs/src/client/components/actions/SignInButton/SignInButton.tsx b/packages/nextjs/src/client/components/actions/SignInButton/SignInButton.tsx index e6476536..a28bc98d 100644 --- a/packages/nextjs/src/client/components/actions/SignInButton/SignInButton.tsx +++ b/packages/nextjs/src/client/components/actions/SignInButton/SignInButton.tsx @@ -82,7 +82,7 @@ const SignInButton = forwardRef( throw new AsgardeoRuntimeError( `Sign in failed: ${error instanceof Error ? error.message : String(error)}`, 'SignInButton-handleSignIn-RuntimeError-001', - 'next', + 'nextjs', 'Something went wrong while trying to sign in. Please try again later.', ); } finally { diff --git a/packages/nextjs/src/client/components/actions/SignUpButton/SignUpButton.tsx b/packages/nextjs/src/client/components/actions/SignUpButton/SignUpButton.tsx index cfd0d65b..9729c5db 100644 --- a/packages/nextjs/src/client/components/actions/SignUpButton/SignUpButton.tsx +++ b/packages/nextjs/src/client/components/actions/SignUpButton/SignUpButton.tsx @@ -18,41 +18,108 @@ 'use client'; -import {FC, forwardRef, PropsWithChildren, ReactElement, Ref} from 'react'; -import InternalAuthAPIRoutesConfig from '../../../../configs/InternalAuthAPIRoutesConfig'; -import {BaseSignUpButton, BaseSignUpButtonProps} from '@asgardeo/react'; +import {AsgardeoRuntimeError} from '@asgardeo/node'; +import {forwardRef, ForwardRefExoticComponent, MouseEvent, ReactElement, Ref, RefAttributes, useState} from 'react'; +import {BaseSignUpButton, BaseSignUpButtonProps, useTranslation} from '@asgardeo/react'; +import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; +import {useRouter} from 'next/navigation'; /** - * Interface for SignInButton component props. + * Props interface of {@link SignUpButton} */ export type SignUpButtonProps = BaseSignUpButtonProps; /** - * SignInButton component. This button initiates the sign-in process when clicked. + * SignUpButton component that supports both render props and traditional props patterns. + * It redirects the user to the Asgardeo sign-up page configured for the application. * - * @example + * @remarks This component is only supported in browser based React applications (CSR). + * + * @example Using render props pattern + * ```tsx + * + * {({ signUp, isLoading }) => ( + * + * )} + * + * ``` + * + * @example Using traditional props pattern * ```tsx - * import { SignInButton } from '@asgardeo/auth-react'; + * Create Account + * ``` * - * const App = () => { - * const buttonRef = useRef(null); - * return ( - * - * Sign In - * - * ); - * } + * @example Using component-level preferences + * ```tsx + * + * Custom Sign Up + * * ``` */ -const SignUpButton: FC> = forwardRef< +const SignUpButton: ForwardRefExoticComponent> = forwardRef< HTMLButtonElement, - PropsWithChildren ->( - ({className, style, ...rest}: PropsWithChildren, ref: Ref): ReactElement => ( -
- - - ), -); + SignUpButtonProps +>(({children, onClick, preferences, ...rest}: SignUpButtonProps, ref: Ref): ReactElement => { + const {signUp, signUpUrl} = useAsgardeo(); + const router = useRouter(); + const {t} = useTranslation(preferences?.i18n); + + const [isLoading, setIsLoading] = useState(false); + + const handleSignUp = async (e?: MouseEvent): Promise => { + try { + setIsLoading(true); + + // If a custom `signUpUrl` is provided, use it for navigation. + if (signUpUrl) { + router.push(signUpUrl); + } else { + await signUp(); + } + + if (onClick) { + onClick(e as MouseEvent); + } + } catch (error) { + throw new AsgardeoRuntimeError( + `Sign up failed: ${error instanceof Error ? error.message : String(error)}`, + 'SignUpButton-handleSignUp-RuntimeError-001', + 'nextjs', + 'Something went wrong while trying to sign up. Please try again later.', + ); + } finally { + setIsLoading(false); + } + }; + + return ( + + {children ?? t('elements.buttons.signUp')} + + ); +}); + +SignUpButton.displayName = 'SignUpButton'; export default SignUpButton; diff --git a/packages/nextjs/src/client/components/control/SignedIn/SignedIn.tsx b/packages/nextjs/src/client/components/control/SignedIn/SignedIn.tsx index e1bd3f4d..54d4174c 100644 --- a/packages/nextjs/src/client/components/control/SignedIn/SignedIn.tsx +++ b/packages/nextjs/src/client/components/control/SignedIn/SignedIn.tsx @@ -18,8 +18,8 @@ 'use client'; -import {FC, PropsWithChildren, ReactNode, useEffect, useState} from 'react'; -import isSignedIn from '../../../../server/actions/isSignedIn'; +import {FC, PropsWithChildren, ReactNode} from 'react'; +import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; /** * Props interface of {@link SignedIn} @@ -51,23 +51,9 @@ const SignedIn: FC> = ({ children, fallback = null, }: PropsWithChildren) => { - const [isSignedInSync, setIsSignedInSync] = useState(null); + const {isSignedIn} = useAsgardeo(); - useEffect(() => { - (async (): Promise => { - try { - const result: boolean = await isSignedIn(); - - setIsSignedInSync(result); - } catch (error) { - setIsSignedInSync(false); - } - })(); - }, []); - - if (isSignedInSync === null) return null; - - return <>{isSignedInSync ? children : fallback}; + return <>{isSignedIn ? children : fallback}; }; export default SignedIn; diff --git a/packages/nextjs/src/client/components/control/SignedOut/SignedOut.tsx b/packages/nextjs/src/client/components/control/SignedOut/SignedOut.tsx index 2c8ac726..0982263d 100644 --- a/packages/nextjs/src/client/components/control/SignedOut/SignedOut.tsx +++ b/packages/nextjs/src/client/components/control/SignedOut/SignedOut.tsx @@ -18,8 +18,8 @@ 'use client'; -import {FC, PropsWithChildren, ReactNode, useEffect, useState} from 'react'; -import isSignedIn from '../../../../server/actions/isSignedIn'; +import {FC, PropsWithChildren, ReactNode} from 'react'; +import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; /** * Props interface of {@link SignedOut} @@ -51,23 +51,9 @@ const SignedOut: FC> = ({ children, fallback = null, }: PropsWithChildren) => { - const [isSignedInSync, setIsSignedInSync] = useState(null); + const {isSignedIn} = useAsgardeo(); - useEffect(() => { - (async (): Promise => { - try { - const result: boolean = await isSignedIn(); - - setIsSignedInSync(result); - } catch (error) { - setIsSignedInSync(false); - } - })(); - }, []); - - if (isSignedInSync === null) return null; - - return <>{!isSignedInSync ? children : fallback}; + return <>{!isSignedIn ? children : fallback}; }; export default SignedOut; diff --git a/packages/nextjs/src/client/components/presentation/SignIn/SignIn.tsx b/packages/nextjs/src/client/components/presentation/SignIn/SignIn.tsx index 336a0a94..639ecd55 100644 --- a/packages/nextjs/src/client/components/presentation/SignIn/SignIn.tsx +++ b/packages/nextjs/src/client/components/presentation/SignIn/SignIn.tsx @@ -32,7 +32,7 @@ import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; * Props for the SignIn component. * Extends BaseSignInProps for full compatibility with the React BaseSignIn component */ -export type SignInProps = BaseSignInProps; +export type SignInProps = Pick; /** * A SignIn component for Next.js that provides native authentication flow. @@ -77,19 +77,8 @@ export type SignInProps = BaseSignInProps; * }; * ``` */ -const SignIn: FC = ({ - afterSignInUrl, - className, - onError, - onFlowChange, - onInitialize, - onSubmit, - onSuccess, - size = 'medium', - variant = 'outlined', - ...rest -}: SignInProps) => { - const {signIn} = useAsgardeo(); +const SignIn: FC = ({size = 'medium', variant = 'outlined', ...rest}: SignInProps) => { + const {signIn, afterSignInUrl} = useAsgardeo(); const handleInitialize = async (): Promise => await signIn({ @@ -103,21 +92,16 @@ const SignIn: FC = ({ const handleOnSubmit = async ( payload: EmbeddedSignInFlowHandleRequestPayload, request: EmbeddedFlowExecuteRequestConfig, - ): Promise => await signIn(payload, request); - - const handleError = (error: Error): void => { - onError?.(error); + ): Promise => { + return await signIn(payload, request); }; return ( { + * return ( + * { + * console.log('Sign-up successful:', response); + * // Handle successful sign-up (e.g., redirect, show confirmation) + * }} + * onError={(error) => { + * console.error('Sign-up failed:', error); + * }} + * onComplete={(redirectUrl) => { + * // Platform-specific redirect handling (e.g., Next.js router.push) + * router.push(redirectUrl); // or window.location.href = redirectUrl + * }} + * size="medium" + * variant="outlined" + * afterSignUpUrl="/welcome" + * /> + * ); + * }; + * ``` + */ +const SignUp: FC = ({className, size = 'medium', variant = 'outlined', afterSignUpUrl, onError}) => { + const {signUp, isInitialized} = useAsgardeo(); + + /** + * Initialize the sign-up flow. + */ + const handleInitialize = async ( + payload?: EmbeddedFlowExecuteRequestPayload, + ): Promise => { + return await signUp( + payload || { + flowType: EmbeddedFlowType.Registration, + }, + ); + }; + + /** + * Handle sign-up steps. + */ + const handleOnSubmit = async (payload: EmbeddedFlowExecuteRequestPayload): Promise => + await signUp(payload); + + return ( + + ); +}; + +export default SignUp; diff --git a/packages/nextjs/src/client/components/presentation/UserDropdown/UserDropdown.tsx b/packages/nextjs/src/client/components/presentation/UserDropdown/UserDropdown.tsx new file mode 100644 index 00000000..44045c81 --- /dev/null +++ b/packages/nextjs/src/client/components/presentation/UserDropdown/UserDropdown.tsx @@ -0,0 +1,193 @@ +/** + * 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 {FC, ReactElement, ReactNode, useState} from 'react'; +import {BaseUserDropdown, BaseUserDropdownProps} from '@asgardeo/react'; +import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; +import UserProfile from '../UserProfile/UserProfile'; + +/** + * Render props data passed to the children function + */ +export interface UserDropdownRenderProps { + /** Function to close the profile dialog */ + closeProfile: () => void; + /** Whether user data is currently loading */ + isLoading: boolean; + /** Whether the profile dialog is currently open */ + isProfileOpen: boolean; + /** Function to open the user profile dialog */ + openProfile: () => void; + /** Function to sign out the user */ + signOut: () => void; + /** The authenticated user object */ + user: any; +} + +/** + * Props for the UserDropdown component. + * Extends BaseUserDropdownProps but excludes user, onManageProfile, and onSignOut since they're handled internally + */ +export type UserDropdownProps = Omit & { + /** + * Render prop function that receives user state and actions. + * When provided, this completely replaces the default dropdown rendering. + */ + children?: (props: UserDropdownRenderProps) => ReactNode; + /** + * Custom render function for the dropdown content. + * When provided, this replaces just the dropdown content while keeping the trigger. + */ + renderDropdown?: (props: UserDropdownRenderProps) => ReactNode; + /** + * Custom render function for the trigger button. + * When provided, this replaces just the trigger button while keeping the dropdown. + */ + renderTrigger?: (props: UserDropdownRenderProps) => ReactNode; +}; + +/** + * UserDropdown component displays a user avatar with a dropdown menu. + * When clicked, it shows a popover with customizable menu items. + * This component is the React-specific implementation that uses the BaseUserDropdown + * and automatically retrieves the user data from Asgardeo context. + * + * Supports render props for complete customization of the dropdown appearance and behavior. + * + * @example + * ```tsx + * // Basic usage - will use user from Asgardeo context + * {} }, + * { label: 'Settings', href: '/settings' }, + * { label: 'Sign Out', onClick: () => {} } + * ]} /> + * + * // With custom configuration + * Please sign in} + * /> + * + * // Using render props for complete customization + * + * {({ user, isLoading, openProfile, signOut }) => ( + *
+ * + * + *
+ * )} + *
+ * + * // Using partial render props + * ( + * + * )} + * /> + * ``` + */ +const UserDropdown: FC = ({ + children, + renderTrigger, + renderDropdown, + onSignOut, + ...rest +}: UserDropdownProps): ReactElement => { + const {user, isLoading, signOut} = useAsgardeo(); + const [isProfileOpen, setIsProfileOpen] = useState(false); + + const handleManageProfile = () => { + setIsProfileOpen(true); + }; + + const handleSignOut = () => { + signOut(); + onSignOut && onSignOut(); + }; + + const closeProfile = () => { + setIsProfileOpen(false); + }; + + // Prepare render props data + const renderProps: UserDropdownRenderProps = { + user, + isLoading: isLoading as boolean, + openProfile: handleManageProfile, + signOut: handleSignOut, + isProfileOpen, + closeProfile, + }; + + // If children render prop is provided, use it for complete customization + if (children) { + return ( + <> + {children(renderProps)} + + + ); + } + + // If partial render props are provided, customize specific parts + if (renderTrigger || renderDropdown) { + // This would require significant changes to BaseUserDropdown to support partial customization + // For now, we'll provide a simple implementation that shows how it could work + return ( + <> + {renderTrigger ? ( + renderTrigger(renderProps) + ) : ( + + )} + {/* Note: renderDropdown would need BaseUserDropdown modifications to implement properly */} + + + ); + } + + // Default behavior - use BaseUserDropdown as before + return ( + <> + + {isProfileOpen && } + + ); +}; + +export default UserDropdown; diff --git a/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx b/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx new file mode 100644 index 00000000..73c24427 --- /dev/null +++ b/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx @@ -0,0 +1,77 @@ +/** + * 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 {FC, ReactElement} from 'react'; +import {BaseUserProfile, BaseUserProfileProps, useUser} from '@asgardeo/react'; +import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; +import getSessionId from '../../../../server/actions/getSessionId'; +import updateUserProfileAction from '../../../../server/actions/updateUserProfileAction'; +import { Schema, User } from '@asgardeo/node'; + +/** + * Props for the UserProfile component. + * Extends BaseUserProfileProps but makes the user prop optional since it will be obtained from useAsgardeo + */ +export type UserProfileProps = Omit; + +/** + * UserProfile component displays the authenticated user's profile information in a + * structured and styled format. It shows user details such as display name, email, + * username, and other available profile information from Asgardeo. + * + * This component is the React-specific implementation that uses the BaseUserProfile + * and automatically retrieves the user data from Asgardeo context if not provided. + * + * @example + * ```tsx + * // Basic usage - will use user from Asgardeo context + * + * + * // With explicit user data + * + * + * // With card layout and custom fallback + * Please sign in to view your profile} + * /> + * ``` + */ +const UserProfile: FC = ({...rest}: UserProfileProps): ReactElement => { + const {baseUrl} = useAsgardeo(); + const {profile, flattenedProfile, schemas, revalidateProfile} = useUser(); + + const handleProfileUpdate = async (payload: any): Promise => { + await updateUserProfileAction(payload, (await getSessionId()) as string); + await revalidateProfile(); + }; + + return ( + + ); +}; + +export default UserProfile; diff --git a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts index 807c287c..c1fc018c 100644 --- a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts +++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts @@ -32,6 +32,7 @@ export type AsgardeoContextProps = Partial; */ const AsgardeoContext: Context = createContext({ signInUrl: undefined, + signUpUrl: undefined, afterSignInUrl: undefined, baseUrl: undefined, isInitialized: false, diff --git a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx index fb90dd94..cc0dcbe2 100644 --- a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx @@ -18,33 +18,99 @@ 'use client'; -import {EmbeddedFlowExecuteRequestConfig, EmbeddedSignInFlowHandleRequestPayload, User} from '@asgardeo/node'; +import { + EmbeddedFlowExecuteRequestConfig, + EmbeddedFlowExecuteRequestPayload, + EmbeddedSignInFlowHandleRequestPayload, + User, + UserProfile, +} from '@asgardeo/node'; import {I18nProvider, FlowProvider, UserProvider, ThemeProvider, AsgardeoProviderProps} from '@asgardeo/react'; import {FC, PropsWithChildren, useEffect, useMemo, useState} from 'react'; -import {useRouter} from 'next/navigation'; +import {useRouter, useSearchParams} from 'next/navigation'; import AsgardeoContext, {AsgardeoContextProps} from './AsgardeoContext'; -import {getIsSignedInAction, getUserAction} from '../../../server/actions/authActions'; /** * Props interface of {@link AsgardeoClientProvider} */ -export type AsgardeoClientProviderProps = Partial> & Pick & { - signOut: AsgardeoContextProps['signOut']; - signIn: AsgardeoContextProps['signIn']; -}; +export type AsgardeoClientProviderProps = Partial> & + Pick & { + signOut: AsgardeoContextProps['signOut']; + signIn: AsgardeoContextProps['signIn']; + signUp: AsgardeoContextProps['signUp']; + handleOAuthCallback: (code: string, state: string, sessionState?: string) => Promise<{success: boolean; error?: string; redirectUrl?: string}>; + isSignedIn: boolean; + userProfile: UserProfile; + user: User | null; + }; const AsgardeoClientProvider: FC> = ({ + baseUrl, children, signIn, signOut, + signUp, + handleOAuthCallback, preferences, + isSignedIn, signInUrl, + signUpUrl, + user, + userProfile, }: PropsWithChildren) => { const router = useRouter(); + const searchParams = useSearchParams(); const [isDarkMode, setIsDarkMode] = useState(false); - const [user, setUser] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isSignedIn, setIsSignedIn] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [_userProfile, setUserProfile] = useState(userProfile); + + // Handle OAuth callback automatically + useEffect(() => { + // Don't handle callback if already signed in + if (isSignedIn) return; + + const processOAuthCallback = async () => { + try { + const code = searchParams.get('code'); + const state = searchParams.get('state'); + const sessionState = searchParams.get('session_state'); + const error = searchParams.get('error'); + const errorDescription = searchParams.get('error_description'); + + // Check for OAuth errors first + if (error) { + console.error('[AsgardeoClientProvider] OAuth error:', error, errorDescription); + // Redirect to sign-in page with error + router.push(`/signin?error=${encodeURIComponent(error)}&error_description=${encodeURIComponent(errorDescription || '')}`); + return; + } + + // Handle OAuth callback if code and state are present + if (code && state) { + setIsLoading(true); + + const result = await handleOAuthCallback(code, state, sessionState || undefined); + + if (result.success) { + // Redirect to the success URL + if (result.redirectUrl) { + router.push(result.redirectUrl); + } else { + // Refresh the page to update authentication state + window.location.reload(); + } + } else { + router.push(`/signin?error=authentication_failed&error_description=${encodeURIComponent(result.error || 'Authentication failed')}`); + } + } + } catch (error) { + console.error('[AsgardeoClientProvider] Failed to handle OAuth callback:', error); + router.push('/signin?error=authentication_failed'); + } + }; + + processOAuthCallback(); + }, [searchParams, router, isSignedIn, handleOAuthCallback]); useEffect(() => { if (!preferences?.theme?.mode || preferences.theme.mode === 'system') { @@ -55,51 +121,67 @@ const AsgardeoClientProvider: FC> }, [preferences?.theme?.mode]); useEffect(() => { - const fetchUserData = async () => { - try { - setIsLoading(true); + // Set loading to false when server has resolved authentication state + setIsLoading(false); + }, [isSignedIn, user]); + + const handleSignIn = async ( + payload: EmbeddedSignInFlowHandleRequestPayload, + request: EmbeddedFlowExecuteRequestConfig, + ) => { + try { + const result = await signIn(payload, request); - const sessionResult = await getIsSignedInAction(); + // Redirect based flow URL is sent as `signInUrl` in the response. + if (result?.data?.signInUrl) { + router.push(result.data.signInUrl); - setIsSignedIn(sessionResult.isSignedIn); + return; + } - if (sessionResult.isSignedIn) { - const userResult = await getUserAction(); + // After the Embedded flow is successful, the URL to navigate next is sent as `afterSignInUrl` in the response. + if (result?.data?.afterSignInUrl) { + router.push(result.data.afterSignInUrl); - if (userResult.user) { - setUser(userResult.user); - } - } else { - setUser(null); - } - } catch (error) { - setUser(null); - setIsSignedIn(false); - } finally { - setIsLoading(false); + return; } - }; - fetchUserData(); - }, []); + if (result?.error) { + throw new Error(result.error); + } - const handleSignIn = async ( - payload: EmbeddedSignInFlowHandleRequestPayload, + return result?.data ?? result; + } catch (error) { + throw error; + } + }; + + const handleSignUp = async ( + payload: EmbeddedFlowExecuteRequestPayload, request: EmbeddedFlowExecuteRequestConfig, ) => { try { - const result = await signIn(payload, request); + const result = await signUp(payload, request); + + // Redirect based flow URL is sent as `signUpUrl` in the response. + if (result?.data?.signUpUrl) { + router.push(result.data.signUpUrl); + + return; + } + + // After the Embedded flow is successful, the URL to navigate next is sent as `afterSignUpUrl` in the response. + if (result?.data?.afterSignUpUrl) { + router.push(result.data.afterSignUpUrl); - if (result?.afterSignInUrl) { - router.push(result.afterSignInUrl); - return {redirected: true, location: result.afterSignInUrl}; + return; } if (result?.error) { throw new Error(result.error); } - return result; + return result?.data ?? result; } catch (error) { throw error; } @@ -109,16 +191,16 @@ const AsgardeoClientProvider: FC> try { const result = await signOut(); - if (result?.afterSignOutUrl) { - router.push(result.afterSignOutUrl); - return {redirected: true, location: result.afterSignOutUrl}; + if (result?.data?.afterSignOutUrl) { + router.push(result.data.afterSignOutUrl); + return {redirected: true, location: result.data.afterSignOutUrl}; } if (result?.error) { throw new Error(result.error); } - return result; + return result?.data ?? result; } catch (error) { throw error; } @@ -126,14 +208,17 @@ const AsgardeoClientProvider: FC> const contextValue = useMemo( () => ({ + baseUrl, user, isSignedIn, isLoading, signIn: handleSignIn, signOut: handleSignOut, + signUp: handleSignUp, signInUrl, + signUpUrl, }), - [user, isSignedIn, isLoading], + [baseUrl, user, isSignedIn, isLoading, signInUrl, signUpUrl], ); return ( @@ -141,15 +226,7 @@ const AsgardeoClientProvider: FC> - - {children} - + {children} diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index 056d69ed..ae8cccb6 100644 --- a/packages/nextjs/src/index.ts +++ b/packages/nextjs/src/index.ts @@ -24,6 +24,8 @@ export * from './client/contexts/Asgardeo/useAsgardeo'; export {default as isSignedIn} from './server/actions/isSignedIn'; +export {default as handleOAuthCallback} from './server/actions/handleOAuthCallbackAction'; + export {default as SignedIn} from './client/components/control/SignedIn/SignedIn'; export {SignedInProps} from './client/components/control/SignedIn/SignedIn'; @@ -33,6 +35,9 @@ export {SignedOutProps} from './client/components/control/SignedOut/SignedOut'; export {default as SignInButton} from './client/components/actions/SignInButton/SignInButton'; export type {SignInButtonProps} from './client/components/actions/SignInButton/SignInButton'; +export {default as SignUpButton} from './client/components/actions/SignUpButton/SignUpButton'; +export type {SignUpButtonProps} from './client/components/actions/SignUpButton/SignUpButton'; + export {default as SignIn} from './client/components/presentation/SignIn/SignIn'; export type {SignInProps} from './client/components/presentation/SignIn/SignIn'; @@ -42,6 +47,15 @@ export type {SignOutButtonProps} from './client/components/actions/SignOutButton export {default as User} from './client/components/presentation/User/User'; export type {UserProps} from './client/components/presentation/User/User'; +export {default as SignUp} from './client/components/presentation/SignUp/SignUp'; +export type {SignUpProps} from './client/components/presentation/SignUp/SignUp'; + +export {default as UserDropdown} from './client/components/presentation/UserDropdown/UserDropdown'; +export type {UserDropdownProps} from './client/components/presentation/UserDropdown/UserDropdown'; + +export {default as UserProfile} from './client/components/presentation/UserProfile/UserProfile'; +export type {UserProfileProps} from './client/components/presentation/UserProfile/UserProfile'; + export {default as AsgardeoNext} from './AsgardeoNextClient'; export {default as asgardeoMiddleware} from './middleware/asgardeoMiddleware'; diff --git a/packages/nextjs/src/server/AsgardeoProvider.tsx b/packages/nextjs/src/server/AsgardeoProvider.tsx index 4e3ff230..21efda1d 100644 --- a/packages/nextjs/src/server/AsgardeoProvider.tsx +++ b/packages/nextjs/src/server/AsgardeoProvider.tsx @@ -17,18 +17,24 @@ */ import {FC, PropsWithChildren, ReactElement} from 'react'; -import {AsgardeoRuntimeError} from '@asgardeo/node'; +import {AsgardeoRuntimeError, User, UserProfile} from '@asgardeo/node'; import AsgardeoClientProvider, {AsgardeoClientProviderProps} from '../client/contexts/Asgardeo/AsgardeoProvider'; import AsgardeoNextClient from '../AsgardeoNextClient'; -import {AsgardeoNextConfig} from '../models/config'; -import {signInAction, getUserAction, getIsSignedInAction} from './actions/authActions'; -import gerClientOrigin from './actions/gerClientOrigin'; +import signInAction from './actions/signInAction'; import signOutAction from './actions/signOutAction'; +import {AsgardeoNextConfig} from '../models/config'; +import isSignedIn from './actions/isSignedIn'; +import getUserAction from './actions/getUserAction'; +import getSessionId from './actions/getSessionId'; +import getUserProfileAction from './actions/getUserProfileAction'; +import signUpAction from './actions/signUpAction'; +import handleOAuthCallbackAction from './actions/handleOAuthCallbackAction'; +import {AsgardeoProviderProps} from '@asgardeo/react'; /** * Props interface of {@link AsgardeoServerProvider} */ -export type AsgardeoServerProviderProps = AsgardeoClientProviderProps & { +export type AsgardeoServerProviderProps = Partial & { clientSecret?: string; }; @@ -52,19 +58,14 @@ const AsgardeoServerProvider: FC> children, afterSignInUrl, afterSignOutUrl, - ...config + ..._config }: PropsWithChildren): Promise => { const asgardeoClient = AsgardeoNextClient.getInstance(); - console.log('Initializing Asgardeo client with config:', config); - - const origin = await gerClientOrigin(); + let config: Partial = {}; try { - asgardeoClient.initialize({ - afterSignInUrl: afterSignInUrl ?? origin, - afterSignOutUrl: afterSignOutUrl ?? origin, - ...config, - }); + await asgardeoClient.initialize(_config as AsgardeoNextConfig); + config = await asgardeoClient.getConfiguration(); } catch (error) { throw new AsgardeoRuntimeError( `Failed to initialize Asgardeo client: ${error?.toString()}`, @@ -74,17 +75,42 @@ const AsgardeoServerProvider: FC> ); } - const configuration = await asgardeoClient.getConfiguration(); - console.log('Asgardeo client initialized with configuration:', configuration); + if (!asgardeoClient.isInitialized) { + return <>; + } + + const sessionId: string = (await getSessionId()) as string; + const _isSignedIn: boolean = await isSignedIn(sessionId); + + let user: User = {}; + let userProfile: UserProfile = { + schemas: [], + profile: {}, + flattenedProfile: {}, + }; + + if (_isSignedIn) { + const userResponse = await getUserAction(sessionId); + const userProfileResponse = await getUserProfileAction(sessionId); + + user = userResponse.data?.user || {}; + userProfile = userProfileResponse.data?.userProfile; + } return ( {children} diff --git a/packages/nextjs/src/server/actions/gerClientOrigin.ts b/packages/nextjs/src/server/actions/getClientOrigin.ts similarity index 77% rename from packages/nextjs/src/server/actions/gerClientOrigin.ts rename to packages/nextjs/src/server/actions/getClientOrigin.ts index b3f8e74e..2d8d6547 100644 --- a/packages/nextjs/src/server/actions/gerClientOrigin.ts +++ b/packages/nextjs/src/server/actions/getClientOrigin.ts @@ -2,11 +2,11 @@ import {headers} from 'next/headers'; -const gerClientOrigin = async () => { +const getClientOrigin = async () => { const headersList = await headers(); const host = headersList.get('host'); const protocol = headersList.get('x-forwarded-proto') ?? 'http'; return `${protocol}://${host}`; }; -export default gerClientOrigin; +export default getClientOrigin; diff --git a/packages/nextjs/src/server/actions/handleSessionRequest.ts b/packages/nextjs/src/server/actions/getUserAction.ts similarity index 56% rename from packages/nextjs/src/server/actions/handleSessionRequest.ts rename to packages/nextjs/src/server/actions/getUserAction.ts index 16470880..3abcb9b4 100644 --- a/packages/nextjs/src/server/actions/handleSessionRequest.ts +++ b/packages/nextjs/src/server/actions/getUserAction.ts @@ -16,23 +16,22 @@ * under the License. */ -import {NextRequest, NextResponse} from 'next/server'; -import getIsSignedIn from './isSignedIn'; +'use server'; + +import AsgardeoNextClient from '../../AsgardeoNextClient'; /** - * Handles session status requests. - * - * @param req - The Next.js request object - * @returns NextResponse with session status + * Server action to get the current user. + * Returns the user profile if signed in. */ -export async function handleSessionRequest(req: NextRequest): Promise { +const getUserAction = async (sessionId: string) => { try { - const isSignedIn: boolean = await getIsSignedIn(); - - return NextResponse.json({isSignedIn}); + const client = AsgardeoNextClient.getInstance(); + const user = await client.getUser(sessionId); + return {success: true, data: {user}, error: null}; } catch (error) { - return NextResponse.json({error: 'Failed to check session'}, {status: 500}); + return {success: false, data: {user: null}, error: 'Failed to get user'}; } -} +}; -export default handleSessionRequest; +export default getUserAction; diff --git a/packages/nextjs/src/server/actions/handleUserRequest.ts b/packages/nextjs/src/server/actions/getUserProfileAction.ts similarity index 55% rename from packages/nextjs/src/server/actions/handleUserRequest.ts rename to packages/nextjs/src/server/actions/getUserProfileAction.ts index 82e08055..3b64bf50 100644 --- a/packages/nextjs/src/server/actions/handleUserRequest.ts +++ b/packages/nextjs/src/server/actions/getUserProfileAction.ts @@ -16,28 +16,33 @@ * under the License. */ -import {NextRequest, NextResponse} from 'next/server'; -import {User} from '@asgardeo/node'; +'use server'; + +import {UserProfile} from '@asgardeo/node'; import AsgardeoNextClient from '../../AsgardeoNextClient'; /** - * Handles user profile requests. - * - * @param req - The Next.js request object - * @returns NextResponse with user profile data + * Server action to get the current user. + * Returns the user profile if signed in. */ -export async function handleUserRequest(req: NextRequest): Promise { +const getUserProfileAction = async (sessionId: string) => { try { const client = AsgardeoNextClient.getInstance(); - const user: User = await client.getUser(); - - console.log('[AsgardeoNextClient] User fetched successfully:', user); - - return NextResponse.json({user}); + const updatedProfile: UserProfile = await client.getUserProfile(sessionId); + return {success: true, data: {userProfile: updatedProfile}, error: null}; } catch (error) { - console.error('[AsgardeoNextClient] Failed to get user:', error); - return NextResponse.json({error: 'Failed to get user'}, {status: 500}); + return { + success: false, + data: { + userProfile: { + schemas: [], + profile: {}, + flattenedProfile: {}, + }, + }, + error: 'Failed to get user profile', + }; } -} +}; -export default handleUserRequest; +export default getUserProfileAction; diff --git a/packages/nextjs/src/server/actions/handleGetSignIn.ts b/packages/nextjs/src/server/actions/handleGetSignIn.ts deleted file mode 100644 index bd81aa9f..00000000 --- a/packages/nextjs/src/server/actions/handleGetSignIn.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 {NextRequest, NextResponse} from 'next/server'; -import AsgardeoNextClient from '../../AsgardeoNextClient'; - -/** - * Handles GET sign-in requests and OAuth callbacks. - * - * @param req - The Next.js request object - * @returns NextResponse with appropriate redirect or continuation - */ -export async function handleGetSignIn(req: NextRequest): Promise { - try { - const client = AsgardeoNextClient.getInstance(); - const {searchParams} = req.nextUrl; - - if (searchParams.get('code')) { - // Handle OAuth callback - await client.signIn(); - - const cleanUrl: URL = new URL(req.url); - cleanUrl.searchParams.delete('code'); - cleanUrl.searchParams.delete('state'); - cleanUrl.searchParams.delete('session_state'); - - return NextResponse.redirect(cleanUrl.toString()); - } - - // Regular GET sign-in request - await client.signIn(); - return NextResponse.next(); - } catch (error) { - console.error('[AsgardeoNextClient] Sign-in failed:', error); - return NextResponse.json({error: 'Sign-in failed'}, {status: 500}); - } -} - -export default handleGetSignIn; diff --git a/packages/nextjs/src/server/actions/handleOAuthCallbackAction.ts b/packages/nextjs/src/server/actions/handleOAuthCallbackAction.ts new file mode 100644 index 00000000..10cea44f --- /dev/null +++ b/packages/nextjs/src/server/actions/handleOAuthCallbackAction.ts @@ -0,0 +1,102 @@ +/** + * 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 { cookies } from 'next/headers'; +import { CookieConfig } from '@asgardeo/node'; +import AsgardeoNextClient from '../../AsgardeoNextClient'; + +/** + * Server action to handle OAuth callback with authorization code. + * This action processes the authorization code received from the OAuth provider + * and exchanges it for tokens to complete the authentication flow. + * + * @param code - Authorization code from OAuth provider + * @param state - State parameter from OAuth provider for CSRF protection + * @param sessionState - Session state parameter from OAuth provider + * @returns Promise that resolves with success status and optional error message + */ +const handleOAuthCallbackAction = async ( + code: string, + state: string, + sessionState?: string +): Promise<{ + success: boolean; + error?: string; + redirectUrl?: string; +}> => { + try { + if (!code || !state) { + return { + success: false, + error: 'Missing required OAuth parameters: code and state are required' + }; + } + + // Get the Asgardeo client instance + const asgardeoClient = AsgardeoNextClient.getInstance(); + + if (!asgardeoClient.isInitialized) { + return { + success: false, + error: 'Asgardeo client is not initialized' + }; + } + + // Get the session ID from cookies + const cookieStore = await cookies(); + const sessionId = cookieStore.get(CookieConfig.SESSION_COOKIE_NAME)?.value; + + if (!sessionId) { + return { + success: false, + error: 'No session found. Please start the authentication flow again.' + }; + } + + // Exchange the authorization code for tokens + await asgardeoClient.signIn( + { + code, + session_state: sessionState, + state, + } as any, + {}, + sessionId + ); + + // Get the after sign-in URL from configuration + const config = await asgardeoClient.getConfiguration(); + const afterSignInUrl = config.afterSignInUrl || '/'; + + return { + success: true, + redirectUrl: afterSignInUrl + }; + } catch (error) { + console.error('[handleOAuthCallbackAction] OAuth callback error:', error); + + return { + success: false, + error: error instanceof Error ? error.message : 'Authentication failed' + }; + } +}; + +export default handleOAuthCallbackAction; diff --git a/packages/nextjs/src/server/actions/handlePostSignIn.ts b/packages/nextjs/src/server/actions/handlePostSignIn.ts deleted file mode 100644 index bc722f08..00000000 --- a/packages/nextjs/src/server/actions/handlePostSignIn.ts +++ /dev/null @@ -1,102 +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 {NextRequest, NextResponse} from 'next/server'; -import {CookieConfig, generateSessionId, EmbeddedSignInFlowStatus} from '@asgardeo/node'; -import AsgardeoNextClient from '../../AsgardeoNextClient'; -import deleteSessionId from './deleteSessionId'; - -/** - * Handles POST sign-in requests for embedded sign-in flow. - * - * @param req - The Next.js request object - * @returns NextResponse with sign-in result or redirect - */ -export async function handlePostSignIn(req: NextRequest): Promise { - try { - const client = AsgardeoNextClient.getInstance(); - - // Get session ID from cookies directly since we're in middleware context - let userId: string | undefined = req.cookies.get(CookieConfig.SESSION_COOKIE_NAME)?.value; - - // Generate session ID if not present - if (!userId) { - userId = generateSessionId(); - } - - const signInUrl: URL = new URL(await client.getSignInUrlWithConfig({response_mode: 'direct'}, userId)); - const {pathname: urlPathname, origin, searchParams: urlSearchParams} = signInUrl; - - console.log('[AsgardeoNextClient] Sign-in URL:', signInUrl.toString()); - console.log('[AsgardeoNextClient] Search Params:', Object.fromEntries(urlSearchParams.entries())); - - const body = await req.json(); - console.log('[AsgardeoNextClient] Sign-in request:', body); - - const {payload, request} = body; - - const response: any = await client.signIn( - payload, - { - url: request?.url ?? `${origin}${urlPathname}`, - payload: request?.payload ?? Object.fromEntries(urlSearchParams.entries()), - }, - userId, - ); - - // Clean the response to remove any non-serializable properties - const cleanResponse = response ? JSON.parse(JSON.stringify(response)) : {success: true}; - - // Create response with session cookie - const nextResponse = NextResponse.json(cleanResponse); - - // Set session cookie if it was generated - if (!req.cookies.get(CookieConfig.SESSION_COOKIE_NAME)) { - nextResponse.cookies.set(CookieConfig.SESSION_COOKIE_NAME, userId, { - httpOnly: CookieConfig.DEFAULT_HTTP_ONLY, - maxAge: CookieConfig.DEFAULT_MAX_AGE, - sameSite: CookieConfig.DEFAULT_SAME_SITE, - secure: CookieConfig.DEFAULT_SECURE, - }); - } - - if (response.flowStatus === EmbeddedSignInFlowStatus.SuccessCompleted) { - const res = await client.signIn( - { - code: response?.authData?.code, - session_state: response?.authData?.session_state, - state: response?.authData?.state, - } as any, - {}, - userId, - (afterSignInUrl: string) => null, - ); - - const afterSignInUrl: string = await (await client.getStorageManager()).getConfigDataParameter('afterSignInUrl'); - - return NextResponse.redirect(afterSignInUrl, 303); - } - - return nextResponse; - } catch (error) { - console.error('[AsgardeoNextClient] Failed to initialize embedded sign-in flow:', error); - return NextResponse.json({error: 'Failed to initialize sign-in flow'}, {status: 500}); - } -} - -export default handlePostSignIn; diff --git a/packages/nextjs/src/server/actions/isSignedIn.ts b/packages/nextjs/src/server/actions/isSignedIn.ts index 46f5a153..746b6784 100644 --- a/packages/nextjs/src/server/actions/isSignedIn.ts +++ b/packages/nextjs/src/server/actions/isSignedIn.ts @@ -18,14 +18,14 @@ 'use server'; -import {CookieConfig} from '@asgardeo/node'; -import {ReadonlyRequestCookies} from 'next/dist/server/web/spec-extension/adapters/request-cookies'; -import {cookies} from 'next/headers'; +import AsgardeoNextClient from '../../AsgardeoNextClient'; +import getSessionId from './getSessionId'; -const isSignedIn = async (): Promise => { - const cookieStore: ReadonlyRequestCookies = await cookies(); +const isSignedIn = async (sessionId: string): Promise => { + const client = AsgardeoNextClient.getInstance(); + const accessToken: string | undefined = await client.getAccessToken(sessionId); - return !!cookieStore.get(CookieConfig.SESSION_COOKIE_NAME)?.value; + return !!accessToken; }; export default isSignedIn; diff --git a/packages/nextjs/src/server/actions/authActions.ts b/packages/nextjs/src/server/actions/signInAction.ts similarity index 61% rename from packages/nextjs/src/server/actions/authActions.ts rename to packages/nextjs/src/server/actions/signInAction.ts index 9e969122..4edc3da1 100644 --- a/packages/nextjs/src/server/actions/authActions.ts +++ b/packages/nextjs/src/server/actions/signInAction.ts @@ -18,7 +18,6 @@ 'use server'; -import {redirect} from 'next/navigation'; import {cookies} from 'next/headers'; import { CookieConfig, @@ -26,9 +25,9 @@ import { EmbeddedSignInFlowStatus, EmbeddedSignInFlowHandleRequestPayload, EmbeddedFlowExecuteRequestConfig, + EmbeddedSignInFlowInitiateResponse, } from '@asgardeo/node'; import AsgardeoNextClient from '../../AsgardeoNextClient'; -import deleteSessionId from './deleteSessionId'; /** * Server action for signing in a user. @@ -38,16 +37,23 @@ import deleteSessionId from './deleteSessionId'; * @param request - The embedded flow execute request config * @returns Promise that resolves when sign-in is complete */ -export async function signInAction( +const signInAction = async ( payload?: EmbeddedSignInFlowHandleRequestPayload, request?: EmbeddedFlowExecuteRequestConfig, -): Promise<{success: boolean; afterSignInUrl?: string; error?: string}> { - console.log('[AsgardeoNextClient] signInAction called with payload:', payload); +): Promise<{ + success: boolean; + data?: + | { + afterSignInUrl?: string; + signInUrl?: string; + } + | EmbeddedSignInFlowInitiateResponse; + error?: string; +}> => { try { const client = AsgardeoNextClient.getInstance(); const cookieStore = await cookies(); - // Get or generate session ID let userId: string | undefined = cookieStore.get(CookieConfig.SESSION_COOKIE_NAME)?.value; if (!userId) { @@ -60,13 +66,13 @@ export async function signInAction( }); } - // If no payload provided, redirect to sign-in URL + // If no payload provided, redirect to sign-in URL for redirect-based sign-in. + // If there's a payload, handle the embedded sign-in flow. if (!payload) { - const afterSignInUrl = await client.getSignInUrl(userId); + const defaultSignInUrl = await client.getAuthorizeRequestUrl({}, userId); - return {success: true, afterSignInUrl: String(afterSignInUrl)}; + return {success: true, data: {signInUrl: String(defaultSignInUrl)}}; } else { - // Handle embedded sign-in flow const response: any = await client.signIn(payload, request!, userId); if (response.flowStatus === EmbeddedSignInFlowStatus.SuccessCompleted) { @@ -83,41 +89,15 @@ export async function signInAction( const afterSignInUrl = await (await client.getStorageManager()).getConfigDataParameter('afterSignInUrl'); - return {success: true, afterSignInUrl: String(afterSignInUrl)}; + return {success: true, data: {afterSignInUrl: String(afterSignInUrl)}}; } - return {success: true}; + return {success: true, data: response as EmbeddedSignInFlowInitiateResponse}; } } catch (error) { - return {success: false, error: 'Sign-in failed'}; - } -} - -/** - * Server action to get the current user. - * Returns the user profile if signed in. - */ -export async function getUserAction() { - try { - const client = AsgardeoNextClient.getInstance(); - const user = await client.getUser(); - return {user, error: null}; - } catch (error) { - console.error('[AsgardeoNextClient] Failed to get user:', error); - return {user: null, error: 'Failed to get user'}; + console.error('[signInAction] Error during sign-in:', error); + return {success: false, error: String(error)}; } -} +}; -/** - * Server action to check if user is signed in. - */ -export async function getIsSignedInAction() { - try { - const cookieStore = await cookies(); - const sessionId = cookieStore.get(CookieConfig.SESSION_COOKIE_NAME)?.value; - return {isSignedIn: !!sessionId, error: null}; - } catch (error) { - console.error('[AsgardeoNextClient] Failed to check session:', error); - return {isSignedIn: false, error: 'Failed to check session'}; - } -} +export default signInAction; diff --git a/packages/nextjs/src/server/actions/signOutAction.ts b/packages/nextjs/src/server/actions/signOutAction.ts index 5aaa73ee..d49a20f1 100644 --- a/packages/nextjs/src/server/actions/signOutAction.ts +++ b/packages/nextjs/src/server/actions/signOutAction.ts @@ -22,14 +22,14 @@ import {NextRequest, NextResponse} from 'next/server'; import AsgardeoNextClient from '../../AsgardeoNextClient'; import deleteSessionId from './deleteSessionId'; -const signOutAction = async (): Promise<{success: boolean; afterSignOutUrl?: string; error?: unknown}> => { +const signOutAction = async (): Promise<{success: boolean; data?: {afterSignOutUrl?: string}; error?: unknown}> => { try { const client = AsgardeoNextClient.getInstance(); const afterSignOutUrl: string = await client.signOut(); await deleteSessionId(); - return {success: true, afterSignOutUrl}; + return {success: true, data: {afterSignOutUrl}}; } catch (error) { return {success: false, error}; } diff --git a/packages/nextjs/src/server/actions/signUpAction.ts b/packages/nextjs/src/server/actions/signUpAction.ts new file mode 100644 index 00000000..2e9dd7f6 --- /dev/null +++ b/packages/nextjs/src/server/actions/signUpAction.ts @@ -0,0 +1,75 @@ +/** + * 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 { + EmbeddedFlowExecuteRequestConfig, + EmbeddedFlowExecuteRequestPayload, + EmbeddedFlowExecuteResponse, + EmbeddedFlowStatus, +} from '@asgardeo/node'; +import AsgardeoNextClient from '../../AsgardeoNextClient'; + +/** + * Server action for signing in a user. + * Handles the embedded sign-in flow and manages session cookies. + * + * @param payload - The embedded sign-in flow payload + * @param request - The embedded flow execute request config + * @returns Promise that resolves when sign-in is complete + */ +const signUpAction = async ( + payload?: EmbeddedFlowExecuteRequestPayload, + request?: EmbeddedFlowExecuteRequestConfig, +): Promise<{ + success: boolean; + data?: + | { + afterSignUpUrl?: string; + signUpUrl?: string; + } + | EmbeddedFlowExecuteResponse; + error?: string; +}> => { + try { + const client = AsgardeoNextClient.getInstance(); + + // If no payload provided, redirect to sign-in URL for redirect-based sign-in. + // If there's a payload, handle the embedded sign-in flow. + if (!payload) { + const defaultSignUpUrl = ''; + + return {success: true, data: {signUpUrl: String(defaultSignUpUrl)}}; + } else { + const response: any = await client.signUp(payload); + + if (response.flowStatus === EmbeddedFlowStatus.Complete) { + const afterSignUpUrl = await (await client.getStorageManager()).getConfigDataParameter('afterSignInUrl'); + + return {success: true, data: {afterSignUpUrl: String(afterSignUpUrl)}}; + } + + return {success: true, data: response as EmbeddedFlowExecuteResponse}; + } + } catch (error) { + return {success: false, error: String(error)}; + } +}; + +export default signUpAction; diff --git a/packages/nextjs/src/server/actions/updateUserProfileAction.ts b/packages/nextjs/src/server/actions/updateUserProfileAction.ts new file mode 100644 index 00000000..dabda4f3 --- /dev/null +++ b/packages/nextjs/src/server/actions/updateUserProfileAction.ts @@ -0,0 +1,44 @@ +/** + * 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 {User, UserProfile} from '@asgardeo/node'; +import AsgardeoNextClient from '../../AsgardeoNextClient'; + +/** + * Server action to get the current user. + * Returns the user profile if signed in. + */ +const updateUserProfileAction = async (payload: any, sessionId: string) => { + try { + const client = AsgardeoNextClient.getInstance(); + const user: User = await client.updateUserProfile(payload, sessionId); + return {success: true, data: {user}, error: null}; + } catch (error) { + return { + success: false, + data: { + user: {}, + }, + error: 'Failed to get user profile', + }; + } +}; + +export default updateUserProfileAction; diff --git a/packages/react/src/AsgardeoReactClient.ts b/packages/react/src/AsgardeoReactClient.ts index b20393e6..19af191c 100644 --- a/packages/react/src/AsgardeoReactClient.ts +++ b/packages/react/src/AsgardeoReactClient.ts @@ -37,9 +37,9 @@ import { EmbeddedFlowExecuteRequestConfig, } from '@asgardeo/browser'; import AuthAPI from './__temp__/api'; -import getMeOrganizations from './api/scim2/getMeOrganizations'; +import getMeOrganizations from './api/getMeOrganizations'; import getScim2Me from './api/getScim2Me'; -import getSchemas from './api/scim2/getSchemas'; +import getSchemas from './api/getSchemas'; import {AsgardeoReactConfig} from './models/config'; /** @@ -68,7 +68,7 @@ class AsgardeoReactClient e const baseUrl = configData?.baseUrl; const profile = await getScim2Me({baseUrl}); - const schemas = await getSchemas({url: `${baseUrl}/scim2/Schemas`}); + const schemas = await getSchemas({baseUrl}); return generateUserProfile(profile, flattenUserSchema(schemas)); } catch (error) { @@ -82,7 +82,7 @@ class AsgardeoReactClient e const baseUrl = configData?.baseUrl; const profile = await getScim2Me({baseUrl}); - const schemas = await getSchemas({url: `${baseUrl}/scim2/Schemas`}); + const schemas = await getSchemas({baseUrl}); const processedSchemas = flattenUserSchema(schemas); diff --git a/packages/react/src/api/createOrganization.ts b/packages/react/src/api/createOrganization.ts new file mode 100644 index 00000000..8e0dfa74 --- /dev/null +++ b/packages/react/src/api/createOrganization.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 { + Organization, + HttpInstance, + AsgardeoSPAClient, + HttpRequestConfig, + createOrganization as baseCreateOrganization, + CreateOrganizationConfig as BaseCreateOrganizationConfig, + CreateOrganizationPayload, +} from '@asgardeo/browser'; + +const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); + +/** + * Configuration for the createOrganization request (React-specific) + */ +export interface CreateOrganizationConfig 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 organization. + * 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 organization information. + * @example + * ```typescript + * // Using default Asgardeo SPA client httpClient + * try { + * const organization = await createOrganization({ + * baseUrl: "https://api.asgardeo.io/t/", + * 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 AsgardeoAPIError) { + * console.error('Failed to create organization:', error.message); + * } + * } + * ``` + * + * @example + * ```typescript + * // Using custom fetcher + * try { + * const organization = await createOrganization({ + * baseUrl: "https://api.asgardeo.io/t/", + * 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 AsgardeoAPIError) { + * console.error('Failed to create organization:', error.message); + * } + * } + * ``` + */ +const createOrganization = async ({fetcher, ...requestConfig}: CreateOrganizationConfig): 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 baseCreateOrganization({ + ...requestConfig, + fetcher: fetcher || defaultFetcher, + }); +}; + +export default createOrganization; diff --git a/packages/react/src/api/getAllOrganizations.ts b/packages/react/src/api/getAllOrganizations.ts new file mode 100644 index 00000000..452755fe --- /dev/null +++ b/packages/react/src/api/getAllOrganizations.ts @@ -0,0 +1,110 @@ +/** + * 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 { + HttpInstance, + AsgardeoSPAClient, + HttpRequestConfig, + getAllOrganizations as baseGetAllOrganizations, + GetAllOrganizationsConfig as BaseGetAllOrganizationsConfig, + PaginatedOrganizationsResponse, +} from '@asgardeo/browser'; + +const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); + +/** + * Configuration for the getAllOrganizations request (React-specific) + */ +export interface GetAllOrganizationsConfig 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; +} + +/** + * Retrieves all organizations with pagination support. + * This function uses the Asgardeo 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 Asgardeo SPA client httpClient + * try { + * const response = await getAllOrganizations({ + * baseUrl: "https://api.asgardeo.io/t/", + * filter: "", + * limit: 10, + * recursive: false + * }); + * console.log(response.organizations); + * } catch (error) { + * if (error instanceof AsgardeoAPIError) { + * console.error('Failed to get organizations:', error.message); + * } + * } + * ``` + * + * @example + * ```typescript + * // Using custom fetcher + * try { + * const response = await getAllOrganizations({ + * baseUrl: "https://api.asgardeo.io/t/", + * filter: "", + * limit: 10, + * recursive: false, + * fetcher: customFetchFunction + * }); + * console.log(response.organizations); + * } catch (error) { + * if (error instanceof AsgardeoAPIError) { + * console.error('Failed to get organizations:', error.message); + * } + * } + * ``` + */ +const getAllOrganizations = async ({ + fetcher, + ...requestConfig +}: GetAllOrganizationsConfig): 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 baseGetAllOrganizations({ + ...requestConfig, + fetcher: fetcher || defaultFetcher, + }); +}; + +export default getAllOrganizations; diff --git a/packages/react/src/api/getMeOrganizations.ts b/packages/react/src/api/getMeOrganizations.ts new file mode 100644 index 00000000..9ed9a56e --- /dev/null +++ b/packages/react/src/api/getMeOrganizations.ts @@ -0,0 +1,111 @@ +/** + * 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, + HttpInstance, + AsgardeoSPAClient, + HttpRequestConfig, + getMeOrganizations as baseGetMeOrganizations, + GetMeOrganizationsConfig as BaseGetMeOrganizationsConfig, +} from '@asgardeo/browser'; + +const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); + +/** + * Configuration for the getMeOrganizations request (React-specific) + */ +export interface GetMeOrganizationsConfig 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; +} + +/** + * Retrieves the organizations associated with the current user. + * This function uses the Asgardeo 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 Asgardeo SPA client httpClient + * try { + * const organizations = await getMeOrganizations({ + * baseUrl: "https://api.asgardeo.io/t/", + * after: "", + * before: "", + * filter: "", + * limit: 10, + * recursive: false + * }); + * console.log(organizations); + * } catch (error) { + * if (error instanceof AsgardeoAPIError) { + * console.error('Failed to get organizations:', error.message); + * } + * } + * ``` + * + * @example + * ```typescript + * // Using custom fetcher + * try { + * const organizations = await getMeOrganizations({ + * baseUrl: "https://api.asgardeo.io/t/", + * after: "", + * before: "", + * filter: "", + * limit: 10, + * recursive: false, + * fetcher: customFetchFunction + * }); + * console.log(organizations); + * } catch (error) { + * if (error instanceof AsgardeoAPIError) { + * console.error('Failed to get organizations:', error.message); + * } + * } + * ``` + */ +const getMeOrganizations = async ({fetcher, ...requestConfig}: GetMeOrganizationsConfig): 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 baseGetMeOrganizations({ + ...requestConfig, + fetcher: fetcher || defaultFetcher, + }); +}; + +export default getMeOrganizations; diff --git a/packages/react/src/api/getOrganization.ts b/packages/react/src/api/getOrganization.ts new file mode 100644 index 00000000..47741545 --- /dev/null +++ b/packages/react/src/api/getOrganization.ts @@ -0,0 +1,103 @@ +/** + * 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 { + HttpInstance, + AsgardeoSPAClient, + HttpRequestConfig, + getOrganization as baseGetOrganization, + GetOrganizationConfig as BaseGetOrganizationConfig, + OrganizationDetails, +} from '@asgardeo/browser'; + +const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); + +/** + * Configuration for the getOrganization request (React-specific) + */ +export interface GetOrganizationConfig 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; +} + +/** + * Retrieves detailed information for a specific organization. + * This function uses the Asgardeo 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 Asgardeo SPA client httpClient + * try { + * const organization = await getOrganization({ + * baseUrl: "https://api.asgardeo.io/t/dxlab", + * organizationId: "0d5e071b-d3d3-475d-b3c6-1a20ee2fa9b1" + * }); + * console.log(organization); + * } catch (error) { + * if (error instanceof AsgardeoAPIError) { + * console.error('Failed to get organization:', error.message); + * } + * } + * ``` + * + * @example + * ```typescript + * // Using custom fetcher + * try { + * const organization = await getOrganization({ + * baseUrl: "https://api.asgardeo.io/t/dxlab", + * organizationId: "0d5e071b-d3d3-475d-b3c6-1a20ee2fa9b1", + * fetcher: customFetchFunction + * }); + * console.log(organization); + * } catch (error) { + * if (error instanceof AsgardeoAPIError) { + * console.error('Failed to get organization:', error.message); + * } + * } + * ``` + */ +const getOrganization = async ({fetcher, ...requestConfig}: GetOrganizationConfig): 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 baseGetOrganization({ + ...requestConfig, + fetcher: fetcher || defaultFetcher, + }); +}; + +export default getOrganization; diff --git a/packages/react/src/api/getSchemas.ts b/packages/react/src/api/getSchemas.ts new file mode 100644 index 00000000..ff1e25a3 --- /dev/null +++ b/packages/react/src/api/getSchemas.ts @@ -0,0 +1,101 @@ +/** + * 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, + HttpInstance, + AsgardeoSPAClient, + HttpRequestConfig, + getSchemas as baseGetSchemas, + GetSchemasConfig as BaseGetSchemasConfig, +} from '@asgardeo/browser'; + +const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); + +/** + * Configuration for the getSchemas request (React-specific) + */ +export interface GetSchemasConfig 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; +} + +/** + * Retrieves the SCIM2 schemas 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 SCIM2 schemas information. + * @example + * ```typescript + * // Using default Asgardeo SPA client httpClient + * try { + * const schemas = await getSchemas({ + * url: "https://api.asgardeo.io/t//scim2/Schemas", + * }); + * console.log(schemas); + * } catch (error) { + * if (error instanceof AsgardeoAPIError) { + * console.error('Failed to get schemas:', error.message); + * } + * } + * ``` + * + * @example + * ```typescript + * // Using custom fetcher + * try { + * const schemas = await getSchemas({ + * url: "https://api.asgardeo.io/t//scim2/Schemas", + * fetcher: customFetchFunction + * }); + * console.log(schemas); + * } catch (error) { + * if (error instanceof AsgardeoAPIError) { + * console.error('Failed to get schemas:', error.message); + * } + * } + * ``` + */ +const getSchemas = async ({fetcher, ...requestConfig}: GetSchemasConfig): 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 baseGetSchemas({ + ...requestConfig, + fetcher: fetcher || defaultFetcher, + }); +}; + +export default getSchemas; diff --git a/packages/react/src/api/scim2/createOrganization.ts b/packages/react/src/api/scim2/createOrganization.ts deleted file mode 100644 index e25daa59..00000000 --- a/packages/react/src/api/scim2/createOrganization.ts +++ /dev/null @@ -1,135 +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 {AsgardeoAPIError, HttpInstance, AsgardeoSPAClient, HttpRequestConfig, Organization} from '@asgardeo/browser'; - -const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); - -/** - * Interface for organization creation payload. - */ -export interface CreateOrganizationPayload { - /** - * Organization description. - */ - description: string; - /** - * Organization handle/slug. - */ - orgHandle?: string; - /** - * Organization name. - */ - name: string; - /** - * Parent organization ID. - */ - parentId: string; - /** - * Organization type. - */ - type: 'TENANT'; -} - -/** - * 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 - * try { - * const organization = await createOrganization({ - * baseUrl: "https://api.asgardeo.io/t/", - * 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 AsgardeoAPIError) { - * console.error('Failed to create organization:', error.message); - * } - * } - * ``` - */ -const createOrganization = async ({ - baseUrl, - payload, - ...requestConfig -}: Partial & { - baseUrl: string; - payload: CreateOrganizationPayload; -}): Promise => { - if (!baseUrl) { - throw new AsgardeoAPIError( - 'Base URL is required', - 'createOrganization-ValidationError-001', - 'javascript', - 400, - 'Invalid Request', - ); - } - - if (!payload) { - throw new AsgardeoAPIError( - 'Organization payload is required', - 'createOrganization-ValidationError-002', - 'javascript', - 400, - 'Invalid Request', - ); - } - - // Always set type to TENANT for now - const organizationPayload = { - ...payload, - type: 'TENANT' as const, - }; - - const response: any = await httpClient({ - data: JSON.stringify(organizationPayload), - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - method: 'POST', - url: `${baseUrl}/api/server/v1/organizations`, - ...requestConfig, - } as HttpRequestConfig); - - if (!response.data) { - const errorText: string = await response.text(); - - throw new AsgardeoAPIError( - `Failed to create organization: ${errorText}`, - 'createOrganization-ResponseError-001', - 'javascript', - response.status, - response.statusText, - ); - } - - return response.data; -}; - -export default createOrganization; diff --git a/packages/react/src/api/scim2/getAllOrganizations.ts b/packages/react/src/api/scim2/getAllOrganizations.ts deleted file mode 100644 index df76918c..00000000 --- a/packages/react/src/api/scim2/getAllOrganizations.ts +++ /dev/null @@ -1,119 +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 {AsgardeoAPIError, HttpInstance, AsgardeoSPAClient, HttpRequestConfig, Organization} from '@asgardeo/browser'; - -const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); - -/** - * Interface for paginated organization response. - */ -export interface PaginatedOrganizationsResponse { - hasMore?: boolean; - nextCursor?: string; - organizations: Organization[]; - totalCount?: number; -} - -/** - * 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 - * try { - * const response = await getAllOrganizations({ - * baseUrl: "https://api.asgardeo.io/t/", - * filter: "", - * limit: 10, - * recursive: false - * }); - * console.log(response.organizations); - * } catch (error) { - * if (error instanceof AsgardeoAPIError) { - * console.error('Failed to get organizations:', error.message); - * } - * } - * ``` - */ -const getAllOrganizations = async ({ - baseUrl, - filter = '', - limit = 10, - recursive = false, - ...requestConfig -}: Partial & { - baseUrl: string; - filter?: string; - limit?: number; - recursive?: boolean; -}): Promise => { - if (!baseUrl) { - throw new AsgardeoAPIError( - 'Base URL is required', - 'getAllOrganizations-ValidationError-001', - 'javascript', - 400, - 'Invalid Request', - ); - } - - const queryParams: URLSearchParams = new URLSearchParams( - Object.fromEntries( - Object.entries({ - filter, - limit: limit.toString(), - recursive: recursive.toString(), - }).filter(([, value]: [string, string]) => Boolean(value)), - ), - ); - - const response: any = await httpClient({ - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - method: 'GET', - url: `${baseUrl}/api/server/v1/organizations?${queryParams.toString()}`, - ...requestConfig, - } as HttpRequestConfig); - - if (!response.data) { - const errorText: string = await response.text(); - - throw new AsgardeoAPIError( - errorText || 'Failed to get organizations', - 'getAllOrganizations-NetworkError-001', - 'javascript', - response.status, - response.statusText, - ); - } - - const {data}: any = response; - - return { - hasMore: data.hasMore, - nextCursor: data.nextCursor, - organizations: data.organizations || [], - totalCount: data.totalCount, - }; -}; - -export default getAllOrganizations; diff --git a/packages/react/src/api/scim2/getMeOrganizations.ts b/packages/react/src/api/scim2/getMeOrganizations.ts deleted file mode 100644 index 601baa6d..00000000 --- a/packages/react/src/api/scim2/getMeOrganizations.ts +++ /dev/null @@ -1,113 +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 {AsgardeoAPIError, HttpInstance, AsgardeoSPAClient, HttpRequestConfig, Organization} from '@asgardeo/browser'; - -const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); - -/** - * 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 - * try { - * const organizations = await getMeOrganizations({ - * baseUrl: "https://api.asgardeo.io/t/", - * after: "", - * before: "", - * filter: "", - * limit: 10, - * recursive: false - * }); - * console.log(organizations); - * } catch (error) { - * if (error instanceof AsgardeoAPIError) { - * console.error('Failed to get organizations:', error.message); - * } - * } - * ``` - */ -const getMeOrganizations = async ({ - baseUrl, - after = '', - authorizedAppName = '', - before = '', - filter = '', - limit = 10, - recursive = false, - ...requestConfig -}: Partial & { - baseUrl: string; - after?: string; - authorizedAppName?: string; - before?: string; - filter?: string; - limit?: number; - recursive?: boolean; -}): Promise => { - if (!baseUrl) { - throw new AsgardeoAPIError( - 'Base URL is required', - 'getMeOrganizations-ValidationError-001', - 'javascript', - 400, - 'Invalid Request', - ); - } - - const queryParams = new URLSearchParams( - Object.fromEntries( - Object.entries({ - after, - authorizedAppName, - before, - filter, - limit: limit.toString(), - recursive: recursive.toString(), - }).filter(([, value]) => Boolean(value)) - ) - ); - - const response: any = await httpClient({ - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - method: 'GET', - url: `${baseUrl}/api/users/v1/me/organizations?${queryParams.toString()}`, - ...requestConfig, - } as HttpRequestConfig); - - if (!response.data) { - const errorText: string = await response.text(); - - throw new AsgardeoAPIError( - `Failed to fetch associated organizations of the user: ${errorText}`, - 'getMeOrganizations-ResponseError-001', - 'javascript', - response.status, - response.statusText, - ); - } - - return response.data.organizations || []; -}; - -export default getMeOrganizations; diff --git a/packages/react/src/api/scim2/getMeProfile.ts b/packages/react/src/api/scim2/getMeProfile.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/react/src/api/scim2/getOrganization.ts b/packages/react/src/api/scim2/getOrganization.ts deleted file mode 100644 index d2fc8cc8..00000000 --- a/packages/react/src/api/scim2/getOrganization.ts +++ /dev/null @@ -1,116 +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 {AsgardeoAPIError, HttpInstance, AsgardeoSPAClient, HttpRequestConfig} from '@asgardeo/browser'; - -const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); - -/** - * 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; -} - -/** - * 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 - * try { - * const organization = await getOrganization({ - * baseUrl: "https://api.asgardeo.io/t/dxlab", - * organizationId: "0d5e071b-d3d3-475d-b3c6-1a20ee2fa9b1" - * }); - * console.log(organization); - * } catch (error) { - * if (error instanceof AsgardeoAPIError) { - * console.error('Failed to get organization:', error.message); - * } - * } - * ``` - */ -const getOrganization = async ({ - baseUrl, - organizationId, - ...requestConfig -}: Partial & { - baseUrl: string; - organizationId: string; -}): Promise => { - if (!baseUrl) { - throw new AsgardeoAPIError( - 'Base URL is required', - 'getOrganization-ValidationError-001', - 'javascript', - 400, - 'Invalid Request', - ); - } - - if (!organizationId) { - throw new AsgardeoAPIError( - 'Organization ID is required', - 'getOrganization-ValidationError-002', - 'javascript', - 400, - 'Invalid Request', - ); - } - - const response: any = await httpClient({ - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - method: 'GET', - url: `${baseUrl}/api/server/v1/organizations/${organizationId}`, - ...requestConfig, - } as HttpRequestConfig); - - if (!response.data) { - const errorText: string = await response.text(); - - throw new AsgardeoAPIError( - `Failed to fetch organization details: ${errorText}`, - 'getOrganization-ResponseError-001', - 'javascript', - response.status, - response.statusText, - ); - } - - return response.data; -}; - -export default getOrganization; diff --git a/packages/react/src/api/scim2/getSchemas.ts b/packages/react/src/api/scim2/getSchemas.ts deleted file mode 100644 index c419c86b..00000000 --- a/packages/react/src/api/scim2/getSchemas.ts +++ /dev/null @@ -1,77 +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, AsgardeoAPIError, HttpInstance, AsgardeoSPAClient, HttpRequestConfig} from '@asgardeo/browser'; - -const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); - -/** - * Retrieves the SCIM2 schemas from the specified endpoint. - * - * @param requestConfig - Request configuration object. - * @returns A promise that resolves with the SCIM2 schemas information. - * @example - * ```typescript - * try { - * const schemas = await getSchemas({ - * url: "https://api.asgardeo.io/t//scim2/Schemas", - * }); - * console.log(schemas); - * } catch (error) { - * if (error instanceof AsgardeoAPIError) { - * console.error('Failed to get schemas:', error.message); - * } - * } - * ``` - */ -const getSchemas = async ({url}: Partial): Promise => { - try { - new URL(url); - } catch (error) { - throw new AsgardeoAPIError( - 'Invalid endpoint URL provided', - 'getSchemas-ValidationError-001', - 'javascript', - 400, - 'Invalid Request', - ); - } - - const response = await httpClient({ - url, - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - } - } as HttpRequestConfig); - - if (!response.data) { - throw new AsgardeoAPIError( - `Failed to fetch SCIM2 schemas`, - 'getSchemas-ResponseError-001', - 'javascript', - response.status, - response.statusText, - ); - } - - return response.data; -}; - -export default getSchemas; diff --git a/packages/react/src/api/scim2/updateMeProfile.ts b/packages/react/src/api/scim2/updateMeProfile.ts deleted file mode 100644 index cdf29718..00000000 --- a/packages/react/src/api/scim2/updateMeProfile.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 {User, AsgardeoAPIError, HttpInstance, AsgardeoSPAClient, HttpRequestConfig} from '@asgardeo/browser'; - -const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); - -/** - * Updates the user profile information at the specified SCIM2 Me endpoint. - * - * @param url - The SCIM2 Me endpoint URL. - * @param value - The value object to patch (SCIM2 PATCH value). - * @param requestConfig - Additional request config if needed. - * @returns A promise that resolves with the updated user profile information. - * @example - * ```typescript - * await updateMeProfile({ - * url: "https://api.asgardeo.io/t//scim2/Me", - * value: { "urn:scim:wso2:schema": { mobileNumbers: ["0777933830"] } } - * }); - * ``` - */ -const updateMeProfile = async ({ - url, - payload, - ...requestConfig -}: {url: string; payload: any} & Partial): Promise => { - try { - new URL(url); - } catch (error) { - throw new AsgardeoAPIError( - 'Invalid endpoint URL provided', - 'updateMeProfile-ValidationError-001', - 'javascript', - 400, - 'Invalid Request', - ); - } - - const data = { - Operations: [ - { - op: 'replace', - value: payload, - }, - ], - schemas: ['urn:ietf:params:scim:api:messages:2.0:PatchOp'], - }; - - const response: any = await httpClient({ - url, - method: 'PATCH', - headers: { - 'Content-Type': 'application/scim+json', - Accept: 'application/json', - }, - data, - ...requestConfig, - } as HttpRequestConfig); - - if (!response.data) { - const errorText = await response.text(); - - throw new AsgardeoAPIError( - `Failed to update user profile: ${errorText}`, - 'updateMeProfile-ResponseError-001', - 'javascript', - response.status, - response.statusText, - ); - } - - return response.data; -}; - -export default updateMeProfile; diff --git a/packages/react/src/api/updateMeProfile.ts b/packages/react/src/api/updateMeProfile.ts new file mode 100644 index 00000000..61285a77 --- /dev/null +++ b/packages/react/src/api/updateMeProfile.ts @@ -0,0 +1,90 @@ +/** + * 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, + updateMeProfile as baseUpdateMeProfile, + UpdateMeProfileConfig as BaseUpdateMeProfileConfig, +} from '@asgardeo/browser'; + +const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); + +/** + * Configuration for the updateMeProfile request (React-specific) + */ +export interface UpdateMeProfileConfig 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; +} + +/** + * Updates the user profile information at the specified SCIM2 Me endpoint. + * This function uses the Asgardeo SPA client's httpClient by default, but allows for custom fetchers. + * + * @param config - Configuration object with URL, payload and optional request config. + * @returns A promise that resolves with the updated user profile information. + * @example + * ```typescript + * // Using default Asgardeo SPA client httpClient + * await updateMeProfile({ + * url: "https://api.asgardeo.io/t//scim2/Me", + * payload: { "urn:scim:wso2:schema": { mobileNumbers: ["0777933830"] } } + * }); + * ``` + * + * @example + * ```typescript + * // Using custom fetcher + * await updateMeProfile({ + * url: "https://api.asgardeo.io/t//scim2/Me", + * payload: { "urn:scim:wso2:schema": { mobileNumbers: ["0777933830"] } }, + * fetcher: customFetchFunction + * }); + * ``` + */ +const updateMeProfile = async ({fetcher, ...requestConfig}: UpdateMeProfileConfig): Promise => { + const defaultFetcher = async (url: string, config: RequestInit): Promise => { + const response = await httpClient({ + url, + method: config.method || 'PATCH', + 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 baseUpdateMeProfile({ + ...requestConfig, + fetcher: fetcher || defaultFetcher, + }); +}; + +export default updateMeProfile; diff --git a/packages/react/src/api/updateOrganization.ts b/packages/react/src/api/updateOrganization.ts new file mode 100644 index 00000000..4a94efb1 --- /dev/null +++ b/packages/react/src/api/updateOrganization.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 { + HttpInstance, + AsgardeoSPAClient, + HttpRequestConfig, + updateOrganization as baseUpdateOrganization, + UpdateOrganizationConfig as BaseUpdateOrganizationConfig, + OrganizationDetails, + createPatchOperations, +} from '@asgardeo/browser'; + +const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); + +/** + * Configuration for the updateOrganization request (React-specific) + */ +export interface UpdateOrganizationConfig 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; +} + +/** + * Updates the organization information using the Organizations Management API. + * This function uses the Asgardeo 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://api.asgardeo.io/t/", + * organizationId: "0d5e071b-d3d3-475d-b3c6-1a20ee2fa9b1", + * operations + * }); + * + * // Or manually specify operations + * await updateOrganization({ + * baseUrl: "https://api.asgardeo.io/t/", + * 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://api.asgardeo.io/t/", + * organizationId: "0d5e071b-d3d3-475d-b3c6-1a20ee2fa9b1", + * operations: [ + * { operation: "REPLACE", path: "/name", value: "Updated Organization Name" } + * ], + * fetcher: customFetchFunction + * }); + * ``` + */ +const updateOrganization = async ({ + fetcher, + ...requestConfig +}: UpdateOrganizationConfig): Promise => { + const defaultFetcher = async (url: string, config: RequestInit): Promise => { + const response = await httpClient({ + url, + method: config.method || 'PATCH', + 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 baseUpdateOrganization({ + ...requestConfig, + fetcher: fetcher || defaultFetcher, + }); +}; + +// Re-export the helper function +export {createPatchOperations}; + +export default updateOrganization; diff --git a/packages/react/src/components/actions/SignUpButton/SignUpButton.tsx b/packages/react/src/components/actions/SignUpButton/SignUpButton.tsx index c03d4042..e86506c2 100644 --- a/packages/react/src/components/actions/SignUpButton/SignUpButton.tsx +++ b/packages/react/src/components/actions/SignUpButton/SignUpButton.tsx @@ -72,7 +72,7 @@ const SignUpButton: ForwardRefExoticComponent(({children, onClick, preferences, ...rest}: SignUpButtonProps, ref: Ref): ReactElement => { - const {signUp} = useAsgardeo(); + const {signUp, signUpUrl} = useAsgardeo(); const {t} = useTranslation(preferences?.i18n); const [isLoading, setIsLoading] = useState(false); @@ -81,7 +81,14 @@ const SignUpButton: ForwardRefExoticComponent; /** * A styled SignIn component that provides native authentication flow with pre-built styling. diff --git a/packages/react/src/components/presentation/UserDropdown/UserDropdown.tsx b/packages/react/src/components/presentation/UserDropdown/UserDropdown.tsx index 611762a0..49cf7910 100644 --- a/packages/react/src/components/presentation/UserDropdown/UserDropdown.tsx +++ b/packages/react/src/components/presentation/UserDropdown/UserDropdown.tsx @@ -123,7 +123,7 @@ const UserDropdown: FC = ({ const handleSignOut = () => { signOut(); - onSignOut(); + onSignOut && onSignOut(); }; const closeProfile = () => { @@ -133,7 +133,7 @@ const UserDropdown: FC = ({ // Prepare render props data const renderProps: UserDropdownRenderProps = { user, - isLoading, + isLoading: isLoading as boolean, openProfile: handleManageProfile, signOut: handleSignOut, isProfileOpen, diff --git a/packages/react/src/components/presentation/UserProfile/UserProfile.tsx b/packages/react/src/components/presentation/UserProfile/UserProfile.tsx index 266759d9..25d13895 100644 --- a/packages/react/src/components/presentation/UserProfile/UserProfile.tsx +++ b/packages/react/src/components/presentation/UserProfile/UserProfile.tsx @@ -18,7 +18,7 @@ import {FC, ReactElement} from 'react'; import BaseUserProfile, {BaseUserProfileProps} from './BaseUserProfile'; -import updateMeProfile from '../../../api/scim2/updateMeProfile'; +import updateMeProfile from '../../../api/updateMeProfile'; import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; import useUser from '../../../contexts/User/useUser'; @@ -56,7 +56,7 @@ const UserProfile: FC = ({...rest}: UserProfileProps): ReactEl const {profile, flattenedProfile, schemas, revalidateProfile} = useUser(); const handleProfileUpdate = async (payload: any): Promise => { - await updateMeProfile({url: `${baseUrl}/scim2/Me`, payload}); + await updateMeProfile({baseUrl, payload}); await revalidateProfile(); }; diff --git a/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts b/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts index db4f5ad1..2c4fb194 100644 --- a/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts +++ b/packages/react/src/contexts/Asgardeo/AsgardeoContext.ts @@ -24,6 +24,7 @@ import {Organization} from '@asgardeo/browser'; */ export type AsgardeoContextProps = { signInUrl: string | undefined; + signUpUrl: string | undefined; afterSignInUrl: string | undefined; baseUrl: string | undefined; isInitialized: boolean; @@ -62,6 +63,7 @@ export type AsgardeoContextProps = { */ const AsgardeoContext: Context = createContext({ signInUrl: undefined, + signUpUrl: undefined, afterSignInUrl: undefined, baseUrl: undefined, isInitialized: false, diff --git a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx index 52fb7d85..c974e0e1 100644 --- a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx @@ -51,6 +51,7 @@ const AsgardeoProvider: FC> = ({ scopes, preferences, signInUrl, + signUpUrl, ...rest }: PropsWithChildren): ReactElement => { const reRenderCheckRef: RefObject = useRef(false); @@ -72,6 +73,8 @@ const AsgardeoProvider: FC> = ({ baseUrl, clientId, scopes, + signUpUrl, + signInUrl, ...rest, }); })(); @@ -232,6 +235,7 @@ const AsgardeoProvider: FC> = ({ Teamspace -

Welcome back

-

Sign in to your account to continue

diff --git a/samples/teamspace-nextjs/app/signup/page.tsx b/samples/teamspace-nextjs/app/signup/page.tsx index b910105a..a4327e0a 100644 --- a/samples/teamspace-nextjs/app/signup/page.tsx +++ b/samples/teamspace-nextjs/app/signup/page.tsx @@ -1,50 +1,12 @@ -"use client" +'use client'; -import type React from "react" +import type React from 'react'; -import { useState } from "react" -import Link from "next/link" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Alert, AlertDescription } from "@/components/ui/alert" -import { Checkbox } from "@/components/ui/checkbox" -import { Users, Eye, EyeOff } from "lucide-react" -import { useAuth } from "@/hooks/use-auth" +import Link from 'next/link'; +import {Users} from 'lucide-react'; +import {SignUp} from '@asgardeo/nextjs'; export default function SignUpPage() { - const { signUp } = useAuth() - const [showPassword, setShowPassword] = useState(false) - const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState("") - const [formData, setFormData] = useState({ - name: "", - email: "", - password: "", - acceptTerms: false, - }) - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setIsLoading(true) - setError("") - - if (!formData.acceptTerms) { - setError("Please accept the terms and conditions") - setIsLoading(false) - return - } - - try { - await signUp(formData.name, formData.email, formData.password) - } catch (err) { - setError("Failed to create account") - } finally { - setIsLoading(false) - } - } - return (
@@ -55,109 +17,9 @@ export default function SignUpPage() {
Teamspace -

Create your account

-

Start collaborating with your team today

- - - Sign up - Create your account to get started with Teamspace - - -
-
- - setFormData({ ...formData, name: e.target.value })} - required - disabled={isLoading} - /> -
-
- - setFormData({ ...formData, email: e.target.value })} - required - disabled={isLoading} - /> -
-
- -
- setFormData({ ...formData, password: e.target.value })} - required - disabled={isLoading} - minLength={8} - /> - -
-

Password must be at least 8 characters long

-
- -
- setFormData({ ...formData, acceptTerms: checked as boolean })} - /> - -
- - {error && ( - - {error} - - )} - - -
- -
- Already have an account? - - Sign in - -
-
-
+
@@ -166,5 +28,5 @@ export default function SignUpPage() {
- ) + ); } diff --git a/samples/teamspace-nextjs/components/Header/AuthenticatedActions.tsx b/samples/teamspace-nextjs/components/Header/AuthenticatedActions.tsx index c909d6a7..48fe0595 100644 --- a/samples/teamspace-nextjs/components/Header/AuthenticatedActions.tsx +++ b/samples/teamspace-nextjs/components/Header/AuthenticatedActions.tsx @@ -1,6 +1,6 @@ import OrganizationSwitcher from './OrganizationSwitcher'; -import UserDropdown from './UserDropdown'; -import {SignOutButton} from '@asgardeo/nextjs'; +// import UserDropdown from './UserDropdown'; +import {SignOutButton, UserDropdown} from '@asgardeo/nextjs'; interface AuthenticatedActionsProps { className?: string; diff --git a/samples/teamspace-nextjs/components/Header/PublicActions.tsx b/samples/teamspace-nextjs/components/Header/PublicActions.tsx index c11011d7..9e79bd91 100644 --- a/samples/teamspace-nextjs/components/Header/PublicActions.tsx +++ b/samples/teamspace-nextjs/components/Header/PublicActions.tsx @@ -1,6 +1,6 @@ import Link from 'next/link'; import {Button} from '@/components/ui/button'; -import {SignInButton} from '@asgardeo/nextjs'; +import {SignInButton, SignUpButton} from '@asgardeo/nextjs'; interface PublicActionsProps { className?: string; @@ -12,12 +12,8 @@ export default function PublicActions({className = '', showMobileActions = false // Mobile menu actions return (
- - + +
); } @@ -25,12 +21,7 @@ export default function PublicActions({className = '', showMobileActions = false return (
- - +
); }