From 13ad6a1babbc904e5a55876c01f25e2c1ec669d1 Mon Sep 17 00:00:00 2001 From: Brion Date: Sun, 29 Jun 2025 16:17:00 +0530 Subject: [PATCH 01/15] feat: refactor Asgardeo client and authentication flow; enhance SignIn component props; implement getClientOrigin function; remove deprecated session handling actions --- packages/nextjs/src/AsgardeoNextClient.ts | 49 +++++---- .../components/presentation/SignIn/SignIn.tsx | 28 ++--- .../contexts/Asgardeo/AsgardeoProvider.tsx | 55 +++++----- .../nextjs/src/server/AsgardeoProvider.tsx | 22 ++-- .../nextjs/src/server/actions/authActions.ts | 83 +------------- ...{gerClientOrigin.ts => getClientOrigin.ts} | 4 +- .../src/server/actions/handleGetSignIn.ts | 54 ---------- .../src/server/actions/handlePostSignIn.ts | 102 ------------------ .../server/actions/handleSessionRequest.ts | 38 ------- .../src/server/actions/handleUserRequest.ts | 43 -------- .../nextjs/src/server/actions/signInAction.ts | 101 +++++++++++++++++ .../src/server/actions/signOutAction.ts | 4 +- .../components/presentation/SignIn/SignIn.tsx | 20 +--- .../components/Header/PublicActions.tsx | 3 - 14 files changed, 176 insertions(+), 430 deletions(-) rename packages/nextjs/src/server/actions/{gerClientOrigin.ts => getClientOrigin.ts} (77%) delete mode 100644 packages/nextjs/src/server/actions/handleGetSignIn.ts delete mode 100644 packages/nextjs/src/server/actions/handlePostSignIn.ts delete mode 100644 packages/nextjs/src/server/actions/handleSessionRequest.ts delete mode 100644 packages/nextjs/src/server/actions/handleUserRequest.ts create mode 100644 packages/nextjs/src/server/actions/signInAction.ts diff --git a/packages/nextjs/src/AsgardeoNextClient.ts b/packages/nextjs/src/AsgardeoNextClient.ts index c93b996d..d920b1c9 100644 --- a/packages/nextjs/src/AsgardeoNextClient.ts +++ b/packages/nextjs/src/AsgardeoNextClient.ts @@ -32,14 +32,13 @@ import { EmbeddedSignInFlowHandleRequestPayload, executeEmbeddedSignInFlow, EmbeddedFlowExecuteRequestConfig, - CookieConfig, - generateSessionId, - EmbeddedSignInFlowStatus, + ExtendedAuthorizeRequestUrlParams, } 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); /** @@ -83,7 +82,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); @@ -106,22 +105,24 @@ class AsgardeoNextClient exte ...rest, }); + const origin: string = await getClientOrigin(); + return this.asgardeo.initialize({ baseUrl, clientId, 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); } @@ -170,11 +171,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', + ...(arg3 ?? {}), + }), + ); + return initializeEmbeddedSignInFlow({ - payload: arg2.payload, - url: arg2.url, + url: `${defaultSignInUrl.origin}${defaultSignInUrl.pathname}`, + payload: Object.fromEntries(defaultSignInUrl.searchParams.entries()), }); } @@ -225,25 +233,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/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 ( > & Pick & { - signOut: AsgardeoContextProps['signOut']; - signIn: AsgardeoContextProps['signIn']; -}; +export type AsgardeoClientProviderProps = Partial> & + Pick & { + signOut: AsgardeoContextProps['signOut']; + signIn: AsgardeoContextProps['signIn']; + isSignedIn: boolean; + }; const AsgardeoClientProvider: FC> = ({ children, signIn, signOut, preferences, + isSignedIn, signInUrl, }: PropsWithChildren) => { const router = useRouter(); const [isDarkMode, setIsDarkMode] = useState(false); const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); - const [isSignedIn, setIsSignedIn] = useState(false); useEffect(() => { if (!preferences?.theme?.mode || preferences.theme.mode === 'system') { @@ -55,33 +57,30 @@ const AsgardeoClientProvider: FC> }, [preferences?.theme?.mode]); useEffect(() => { - const fetchUserData = async () => { + if (!isSignedIn) { + return; + } + + (async () => { try { setIsLoading(true); - const sessionResult = await getIsSignedInAction(); - - setIsSignedIn(sessionResult.isSignedIn); - - if (sessionResult.isSignedIn) { + if (isSignedIn) { + console.log('[AsgardeoClientProvider] Fetching user data...'); const userResult = await getUserAction(); - if (userResult.user) { - setUser(userResult.user); - } + console.log('[AsgardeoClientProvider] User fetched:', userResult); + setUser(userResult?.data?.user); } else { setUser(null); } } catch (error) { setUser(null); - setIsSignedIn(false); } finally { setIsLoading(false); } - }; - - fetchUserData(); - }, []); + })(); + }, [isSignedIn]); const handleSignIn = async ( payload: EmbeddedSignInFlowHandleRequestPayload, @@ -90,16 +89,16 @@ const AsgardeoClientProvider: FC> try { const result = await signIn(payload, request); - if (result?.afterSignInUrl) { - router.push(result.afterSignInUrl); - return {redirected: true, location: result.afterSignInUrl}; + if (result?.data?.afterSignInUrl) { + router.push(result.data.afterSignInUrl); + return {redirected: true, location: result.data.afterSignInUrl}; } if (result?.error) { throw new Error(result.error); } - return result; + return result?.data ?? result; } catch (error) { throw error; } @@ -109,16 +108,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; } diff --git a/packages/nextjs/src/server/AsgardeoProvider.tsx b/packages/nextjs/src/server/AsgardeoProvider.tsx index 4e3ff230..5bc28aea 100644 --- a/packages/nextjs/src/server/AsgardeoProvider.tsx +++ b/packages/nextjs/src/server/AsgardeoProvider.tsx @@ -20,10 +20,10 @@ import {FC, PropsWithChildren, ReactElement} from 'react'; import {AsgardeoRuntimeError} 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'; /** * Props interface of {@link AsgardeoServerProvider} @@ -55,16 +55,12 @@ const AsgardeoServerProvider: FC> ...config }: PropsWithChildren): Promise => { const asgardeoClient = AsgardeoNextClient.getInstance(); + let configuration: Partial = {}; console.log('Initializing Asgardeo client with config:', config); - const origin = await gerClientOrigin(); - try { - asgardeoClient.initialize({ - afterSignInUrl: afterSignInUrl ?? origin, - afterSignOutUrl: afterSignOutUrl ?? origin, - ...config, - }); + await asgardeoClient.initialize(config); + configuration = await asgardeoClient.getConfiguration(); } catch (error) { throw new AsgardeoRuntimeError( `Failed to initialize Asgardeo client: ${error?.toString()}`, @@ -74,17 +70,15 @@ const AsgardeoServerProvider: FC> ); } - const configuration = await asgardeoClient.getConfiguration(); - console.log('Asgardeo client initialized with configuration:', configuration); - return ( {children} diff --git a/packages/nextjs/src/server/actions/authActions.ts b/packages/nextjs/src/server/actions/authActions.ts index 9e969122..3f7feca8 100644 --- a/packages/nextjs/src/server/actions/authActions.ts +++ b/packages/nextjs/src/server/actions/authActions.ts @@ -26,73 +26,11 @@ import { EmbeddedSignInFlowStatus, EmbeddedSignInFlowHandleRequestPayload, EmbeddedFlowExecuteRequestConfig, + EmbeddedSignInFlowInitiateResponse, } from '@asgardeo/node'; import AsgardeoNextClient from '../../AsgardeoNextClient'; import deleteSessionId from './deleteSessionId'; -/** - * 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 - */ -export async function signInAction( - payload?: EmbeddedSignInFlowHandleRequestPayload, - request?: EmbeddedFlowExecuteRequestConfig, -): Promise<{success: boolean; afterSignInUrl?: string; error?: string}> { - console.log('[AsgardeoNextClient] signInAction called with payload:', payload); - 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) { - userId = generateSessionId(); - cookieStore.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 no payload provided, redirect to sign-in URL - if (!payload) { - const afterSignInUrl = await client.getSignInUrl(userId); - - return {success: true, afterSignInUrl: String(afterSignInUrl)}; - } else { - // Handle embedded sign-in flow - const response: any = await client.signIn(payload, request!, userId); - - if (response.flowStatus === EmbeddedSignInFlowStatus.SuccessCompleted) { - // Complete the sign-in process - await client.signIn( - { - code: response?.authData?.code, - session_state: response?.authData?.session_state, - state: response?.authData?.state, - } as any, - {}, - userId, - ); - - const afterSignInUrl = await (await client.getStorageManager()).getConfigDataParameter('afterSignInUrl'); - - return {success: true, afterSignInUrl: String(afterSignInUrl)}; - } - - return {success: true}; - } - } catch (error) { - return {success: false, error: 'Sign-in failed'}; - } -} - /** * Server action to get the current user. * Returns the user profile if signed in. @@ -101,23 +39,8 @@ 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'}; - } -} - -/** - * 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}; + return {data: {user}, error: null}; } catch (error) { - console.error('[AsgardeoNextClient] Failed to check session:', error); - return {isSignedIn: false, error: 'Failed to check session'}; + return {data: {user: null}, error: 'Failed to get user'}; } } 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/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/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/handleSessionRequest.ts b/packages/nextjs/src/server/actions/handleSessionRequest.ts deleted file mode 100644 index 16470880..00000000 --- a/packages/nextjs/src/server/actions/handleSessionRequest.ts +++ /dev/null @@ -1,38 +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 getIsSignedIn from './isSignedIn'; - -/** - * Handles session status requests. - * - * @param req - The Next.js request object - * @returns NextResponse with session status - */ -export async function handleSessionRequest(req: NextRequest): Promise { - try { - const isSignedIn: boolean = await getIsSignedIn(); - - return NextResponse.json({isSignedIn}); - } catch (error) { - return NextResponse.json({error: 'Failed to check session'}, {status: 500}); - } -} - -export default handleSessionRequest; diff --git a/packages/nextjs/src/server/actions/handleUserRequest.ts b/packages/nextjs/src/server/actions/handleUserRequest.ts deleted file mode 100644 index 82e08055..00000000 --- a/packages/nextjs/src/server/actions/handleUserRequest.ts +++ /dev/null @@ -1,43 +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 {User} from '@asgardeo/node'; -import AsgardeoNextClient from '../../AsgardeoNextClient'; - -/** - * Handles user profile requests. - * - * @param req - The Next.js request object - * @returns NextResponse with user profile data - */ -export async function handleUserRequest(req: NextRequest): Promise { - try { - const client = AsgardeoNextClient.getInstance(); - const user: User = await client.getUser(); - - console.log('[AsgardeoNextClient] User fetched successfully:', user); - - return NextResponse.json({user}); - } catch (error) { - console.error('[AsgardeoNextClient] Failed to get user:', error); - return NextResponse.json({error: 'Failed to get user'}, {status: 500}); - } -} - -export default handleUserRequest; diff --git a/packages/nextjs/src/server/actions/signInAction.ts b/packages/nextjs/src/server/actions/signInAction.ts new file mode 100644 index 00000000..a832707a --- /dev/null +++ b/packages/nextjs/src/server/actions/signInAction.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. + */ + +'use server'; + +import {cookies} from 'next/headers'; +import { + CookieConfig, + generateSessionId, + EmbeddedSignInFlowStatus, + EmbeddedSignInFlowHandleRequestPayload, + EmbeddedFlowExecuteRequestConfig, + EmbeddedSignInFlowInitiateResponse, +} 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 signInAction = async ( + payload?: EmbeddedSignInFlowHandleRequestPayload, + request?: EmbeddedFlowExecuteRequestConfig, +): Promise<{ + success: boolean; + data?: + | { + afterSignInUrl?: string; + } + | EmbeddedSignInFlowInitiateResponse; + error?: string; +}> => { + try { + const client = AsgardeoNextClient.getInstance(); + const cookieStore = await cookies(); + + let userId: string | undefined = cookieStore.get(CookieConfig.SESSION_COOKIE_NAME)?.value; + + if (!userId) { + userId = generateSessionId(); + cookieStore.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 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 defaultSignInUrl = await client.getAuthorizeRequestUrl({}, userId); + + return {success: true, data: {afterSignInUrl: String(defaultSignInUrl)}}; + } else { + const response: any = await client.signIn(payload, request!, userId); + + if (response.flowStatus === EmbeddedSignInFlowStatus.SuccessCompleted) { + // Complete the sign-in process + await client.signIn( + { + code: response?.authData?.code, + session_state: response?.authData?.session_state, + state: response?.authData?.state, + } as any, + {}, + userId, + ); + + const afterSignInUrl = await (await client.getStorageManager()).getConfigDataParameter('afterSignInUrl'); + + return {success: true, data: {afterSignInUrl: String(afterSignInUrl)}}; + } + + return {success: true, data: response as EmbeddedSignInFlowInitiateResponse}; + } + } catch (error) { + return {success: false, error: String(error)}; + } +}; + +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/react/src/components/presentation/SignIn/SignIn.tsx b/packages/react/src/components/presentation/SignIn/SignIn.tsx index 49f96ff6..8a170bae 100644 --- a/packages/react/src/components/presentation/SignIn/SignIn.tsx +++ b/packages/react/src/components/presentation/SignIn/SignIn.tsx @@ -22,29 +22,15 @@ import { EmbeddedSignInFlowHandleRequestPayload, } from '@asgardeo/browser'; import {FC} from 'react'; -import BaseSignIn from './BaseSignIn'; +import BaseSignIn, {BaseSignInProps} from './BaseSignIn'; import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; import {CardProps} from '../../primitives/Card/Card'; /** * Props for the SignIn component. + * Extends BaseSignInProps for full compatibility with the React BaseSignIn component */ -export interface SignInProps { - /** - * Additional CSS class names for customization. - */ - className?: string; - - /** - * Size variant for the component. - */ - size?: 'small' | 'medium' | 'large'; - - /** - * Theme variant for the component. - */ - variant?: CardProps['variant']; -} +export type SignInProps = Pick; /** * A styled SignIn component that provides native authentication flow with pre-built styling. diff --git a/samples/teamspace-nextjs/components/Header/PublicActions.tsx b/samples/teamspace-nextjs/components/Header/PublicActions.tsx index c11011d7..3a7a4d1d 100644 --- a/samples/teamspace-nextjs/components/Header/PublicActions.tsx +++ b/samples/teamspace-nextjs/components/Header/PublicActions.tsx @@ -25,9 +25,6 @@ export default function PublicActions({className = '', showMobileActions = false return (
- From f5739d161fa6238d018d068fa5fb14dfe729e415 Mon Sep 17 00:00:00 2001 From: Brion Date: Sun, 29 Jun 2025 21:11:14 +0530 Subject: [PATCH 02/15] feat: add getSchemas API function and related documentation; refactor Asgardeo client to integrate schema retrieval; enhance error handling and logging in authentication flows --- packages/javascript/src/IsomorphicCrypto.ts | 1 + packages/javascript/src/__legacy__/client.ts | 9 +- .../helpers/authentication-helper.ts | 1 + packages/javascript/src/api/getSchemas.ts | 163 ++++++++++++++++++ .../api/scim2/__tests__/getMeProfile.test.ts | 0 .../javascript/src/api/scim2/getMeProfile.ts | 0 packages/javascript/src/api/scim2/index.ts | 0 packages/javascript/src/index.ts | 1 + .../src/utils/getAuthorizeRequestUrlParams.ts | 5 - packages/nextjs/src/AsgardeoNextClient.ts | 22 ++- .../contexts/Asgardeo/AsgardeoProvider.tsx | 8 +- .../nextjs/src/server/AsgardeoProvider.tsx | 4 + .../nextjs/src/server/actions/signInAction.ts | 7 +- packages/node/src/__legacy__/client.ts | 1 + .../src/__legacy__/core/authentication.ts | 1 + packages/react/src/AsgardeoReactClient.ts | 4 +- packages/react/src/api/getSchemas.ts | 102 +++++++++++ packages/react/src/api/scim2/getMeProfile.ts | 0 18 files changed, 313 insertions(+), 16 deletions(-) create mode 100644 packages/javascript/src/api/getSchemas.ts delete mode 100644 packages/javascript/src/api/scim2/__tests__/getMeProfile.test.ts delete mode 100644 packages/javascript/src/api/scim2/getMeProfile.ts delete mode 100644 packages/javascript/src/api/scim2/index.ts create mode 100644 packages/react/src/api/getSchemas.ts delete mode 100644 packages/react/src/api/scim2/getMeProfile.ts diff --git a/packages/javascript/src/IsomorphicCrypto.ts b/packages/javascript/src/IsomorphicCrypto.ts index 2ff20aed..edf8e9fe 100644 --- a/packages/javascript/src/IsomorphicCrypto.ts +++ b/packages/javascript/src/IsomorphicCrypto.ts @@ -138,6 +138,7 @@ export class IsomorphicCrypto { */ public decodeIdToken(idToken: string): IdToken { try { + console.log('[IsomorphicCrypto] Decoding ID token:', idToken); const utf8String: string = this._cryptoUtils.base64URLDecode(idToken?.split('.')[1]); const payload: IdToken = JSON.parse(utf8String); diff --git a/packages/javascript/src/__legacy__/client.ts b/packages/javascript/src/__legacy__/client.ts index 42f3975f..ef2ce1dd 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, @@ -360,6 +361,8 @@ export class AsgardeoAuthClient { let tokenResponse: Response; + console.log('[AsgardeoAuthClient] Requesting access token from:', tokenEndpoint); + try { tokenResponse = await fetch(tokenEndpoint, { body: body, @@ -631,7 +634,9 @@ export class AsgardeoAuthClient { * @preserve */ public async getUser(userId?: string): Promise { + console.log('[AsgardeoAuthClient] Getting user with userId:', userId); const sessionData: SessionData = await this._storageManager.getSessionData(userId); + console.log('[AsgardeoAuthClient] Session data:', sessionData); const authenticatedUser: User = this._authenticationHelper.getAuthenticatedUserInfo(sessionData?.id_token); Object.keys(authenticatedUser).forEach((key: string) => { diff --git a/packages/javascript/src/__legacy__/helpers/authentication-helper.ts b/packages/javascript/src/__legacy__/helpers/authentication-helper.ts index 87cdcdb7..e9bc279b 100644 --- a/packages/javascript/src/__legacy__/helpers/authentication-helper.ts +++ b/packages/javascript/src/__legacy__/helpers/authentication-helper.ts @@ -288,6 +288,7 @@ export class AuthenticationHelper { await this._storageManager.setSessionData(parsedResponse, userId); + console.log('[AuthenticationHelper] Token response handled successfully:', userId, tokenResponse); return Promise.resolve(tokenResponse); } } diff --git a/packages/javascript/src/api/getSchemas.ts b/packages/javascript/src/api/getSchemas.ts new file mode 100644 index 00000000..58cd5c76 --- /dev/null +++ b/packages/javascript/src/api/getSchemas.ts @@ -0,0 +1,163 @@ +/** + * 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/scim+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, + ); + } + + const responseData = await response.json(); + + // Handle both array response and Resources array response + if (Array.isArray(responseData)) { + return responseData as Schema[]; + } else if ( + responseData && + typeof responseData === 'object' && + 'Resources' in responseData && + Array.isArray(responseData.Resources) + ) { + return responseData.Resources as Schema[]; + } else { + return [responseData] 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/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/index.ts b/packages/javascript/src/index.ts index 2a110aed..baf3f979 100644 --- a/packages/javascript/src/index.ts +++ b/packages/javascript/src/index.ts @@ -26,6 +26,7 @@ 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 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 d920b1c9..ef86bdd3 100644 --- a/packages/nextjs/src/AsgardeoNextClient.ts +++ b/packages/nextjs/src/AsgardeoNextClient.ts @@ -33,6 +33,9 @@ import { executeEmbeddedSignInFlow, EmbeddedFlowExecuteRequestConfig, ExtendedAuthorizeRequestUrlParams, + generateUserProfile, + flattenUserSchema, + getScim2Me, } from '@asgardeo/node'; import {NextRequest, NextResponse} from 'next/server'; import {AsgardeoNextConfig} from './models/config'; @@ -52,7 +55,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(); @@ -123,7 +126,18 @@ class AsgardeoNextClient exte 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}); + const schemas = await getSchemas({url: `${baseUrl}/scim2/Schemas`}); + + return generateUserProfile(profile, flattenUserSchema(schemas)); + } catch (error) { + return this.asgardeo.getUser(resolvedSessionId); + } } override async getOrganizations(): Promise { @@ -176,10 +190,12 @@ class AsgardeoNextClient exte const defaultSignInUrl: URL = new URL( await this.getAuthorizeRequestUrl({ response_mode: 'direct', - ...(arg3 ?? {}), + client_secret: '{{clientSecret}}', }), ); + console.log('[AsgardeoNextClient] Redirecting to sign-in URL:', defaultSignInUrl); + return initializeEmbeddedSignInFlow({ url: `${defaultSignInUrl.origin}${defaultSignInUrl.pathname}`, payload: Object.fromEntries(defaultSignInUrl.searchParams.entries()), diff --git a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx index d77342e0..19e01104 100644 --- a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx @@ -89,9 +89,11 @@ const AsgardeoClientProvider: FC> try { const result = await signIn(payload, request); - if (result?.data?.afterSignInUrl) { - router.push(result.data.afterSignInUrl); - return {redirected: true, location: result.data.afterSignInUrl}; + // Redirect based flow URL is sent as `signInUrl` in the response. + if (result?.data?.signInUrl) { + router.push(result.data.signInUrl); + + return; } if (result?.error) { diff --git a/packages/nextjs/src/server/AsgardeoProvider.tsx b/packages/nextjs/src/server/AsgardeoProvider.tsx index 5bc28aea..25b8a9a4 100644 --- a/packages/nextjs/src/server/AsgardeoProvider.tsx +++ b/packages/nextjs/src/server/AsgardeoProvider.tsx @@ -70,6 +70,10 @@ const AsgardeoServerProvider: FC> ); } + if (!asgardeoClient.isInitialized) { + return <>; + } + return ( { * */ public async getUser(userId: string): Promise { + console.log('[LegacyAsgardeoNodeClient] Getting user with userId:', this._authCore); return this._authCore.getUser(userId); } diff --git a/packages/node/src/__legacy__/core/authentication.ts b/packages/node/src/__legacy__/core/authentication.ts index 00e33fdb..71052c3c 100644 --- a/packages/node/src/__legacy__/core/authentication.ts +++ b/packages/node/src/__legacy__/core/authentication.ts @@ -212,6 +212,7 @@ export class AsgardeoNodeCore { } public async getUser(userId: string): Promise { + console.log(`[AsgardeoNodeCore] Getting user with userId: ${userId}`); return this._auth.getUser(userId); } diff --git a/packages/react/src/AsgardeoReactClient.ts b/packages/react/src/AsgardeoReactClient.ts index b20393e6..9205bfa8 100644 --- a/packages/react/src/AsgardeoReactClient.ts +++ b/packages/react/src/AsgardeoReactClient.ts @@ -39,7 +39,7 @@ import { import AuthAPI from './__temp__/api'; import getMeOrganizations from './api/scim2/getMeOrganizations'; import getScim2Me from './api/getScim2Me'; -import getSchemas from './api/scim2/getSchemas'; +import getSchemas from './api/getSchemas'; import {AsgardeoReactConfig} from './models/config'; /** @@ -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/getSchemas.ts b/packages/react/src/api/getSchemas.ts new file mode 100644 index 00000000..e6718548 --- /dev/null +++ b/packages/react/src/api/getSchemas.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. + */ + +import { + Schema, + AsgardeoAPIError, + 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 requestConfig - 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/getMeProfile.ts b/packages/react/src/api/scim2/getMeProfile.ts deleted file mode 100644 index e69de29b..00000000 From e3b2e03ed4156997ec4f2046c57759331ff6b992 Mon Sep 17 00:00:00 2001 From: Brion Date: Sun, 29 Jun 2025 22:57:12 +0530 Subject: [PATCH 03/15] feat: integrate user retrieval and authentication state management; add getUserAction and isSignedIn actions; refactor AsgardeoNextClient and AsgardeoProvider components --- packages/nextjs/src/AsgardeoNextClient.ts | 20 +++++++++- .../contexts/Asgardeo/AsgardeoProvider.tsx | 40 ++++++------------- .../nextjs/src/server/AsgardeoProvider.tsx | 16 +++++++- .../{authActions.ts => getUserAction.ts} | 12 +++--- .../nextjs/src/server/actions/isSignedIn.ts | 11 ++--- 5 files changed, 58 insertions(+), 41 deletions(-) rename packages/nextjs/src/server/actions/{authActions.ts => getUserAction.ts} (82%) diff --git a/packages/nextjs/src/AsgardeoNextClient.ts b/packages/nextjs/src/AsgardeoNextClient.ts index ef86bdd3..2a7fcf6c 100644 --- a/packages/nextjs/src/AsgardeoNextClient.ts +++ b/packages/nextjs/src/AsgardeoNextClient.ts @@ -36,6 +36,7 @@ import { generateUserProfile, flattenUserSchema, getScim2Me, + getSchemas, } from '@asgardeo/node'; import {NextRequest, NextResponse} from 'next/server'; import {AsgardeoNextConfig} from './models/config'; @@ -131,8 +132,19 @@ class AsgardeoNextClient exte const configData = await this.asgardeo.getConfigData(); const baseUrl = configData?.baseUrl; - const profile = await getScim2Me({baseUrl}); - const schemas = await getSchemas({url: `${baseUrl}/scim2/Schemas`}); + 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) { @@ -164,6 +176,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; } diff --git a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx index 19e01104..7dd56289 100644 --- a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx @@ -23,7 +23,6 @@ import {I18nProvider, FlowProvider, UserProvider, ThemeProvider, AsgardeoProvide import {FC, PropsWithChildren, useEffect, useMemo, useState} from 'react'; import {useRouter} from 'next/navigation'; import AsgardeoContext, {AsgardeoContextProps} from './AsgardeoContext'; -import {getUserAction} from '../../../server/actions/authActions'; /** * Props interface of {@link AsgardeoClientProvider} @@ -33,6 +32,7 @@ export type AsgardeoClientProviderProps = Partial> = ({ @@ -42,10 +42,10 @@ const AsgardeoClientProvider: FC> preferences, isSignedIn, signInUrl, + user }: PropsWithChildren) => { const router = useRouter(); const [isDarkMode, setIsDarkMode] = useState(false); - const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { @@ -57,30 +57,9 @@ const AsgardeoClientProvider: FC> }, [preferences?.theme?.mode]); useEffect(() => { - if (!isSignedIn) { - return; - } - - (async () => { - try { - setIsLoading(true); - - if (isSignedIn) { - console.log('[AsgardeoClientProvider] Fetching user data...'); - const userResult = await getUserAction(); - - console.log('[AsgardeoClientProvider] User fetched:', userResult); - setUser(userResult?.data?.user); - } else { - setUser(null); - } - } catch (error) { - setUser(null); - } finally { - setIsLoading(false); - } - })(); - }, [isSignedIn]); + // Set loading to false when server has resolved authentication state + setIsLoading(false); + }, [isSignedIn, user]); const handleSignIn = async ( payload: EmbeddedSignInFlowHandleRequestPayload, @@ -96,6 +75,13 @@ const AsgardeoClientProvider: FC> return; } + // 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); + + return; + } + if (result?.error) { throw new Error(result.error); } @@ -134,7 +120,7 @@ const AsgardeoClientProvider: FC> signOut: handleSignOut, signInUrl, }), - [user, isSignedIn, isLoading], + [user, isSignedIn, isLoading, signInUrl], ); return ( diff --git a/packages/nextjs/src/server/AsgardeoProvider.tsx b/packages/nextjs/src/server/AsgardeoProvider.tsx index 25b8a9a4..acebe78e 100644 --- a/packages/nextjs/src/server/AsgardeoProvider.tsx +++ b/packages/nextjs/src/server/AsgardeoProvider.tsx @@ -17,13 +17,15 @@ */ import {FC, PropsWithChildren, ReactElement} from 'react'; -import {AsgardeoRuntimeError} from '@asgardeo/node'; +import {AsgardeoRuntimeError, User} from '@asgardeo/node'; import AsgardeoClientProvider, {AsgardeoClientProviderProps} from '../client/contexts/Asgardeo/AsgardeoProvider'; import AsgardeoNextClient from '../AsgardeoNextClient'; 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'; /** * Props interface of {@link AsgardeoServerProvider} @@ -74,6 +76,15 @@ const AsgardeoServerProvider: FC> return <>; } + const _isSignedIn: boolean = await isSignedIn(); + let user: User = {}; + + if (_isSignedIn) { + const response = await getUserAction((await getSessionId()) as string); + + user = response.data?.user || {}; + } + return ( > signInUrl={configuration?.signInUrl} preferences={config.preferences} clientId={config.clientId} - isSignedIn={await isSignedIn()} + user={user} + isSignedIn={_isSignedIn} > {children} diff --git a/packages/nextjs/src/server/actions/authActions.ts b/packages/nextjs/src/server/actions/getUserAction.ts similarity index 82% rename from packages/nextjs/src/server/actions/authActions.ts rename to packages/nextjs/src/server/actions/getUserAction.ts index 3f7feca8..17c084ac 100644 --- a/packages/nextjs/src/server/actions/authActions.ts +++ b/packages/nextjs/src/server/actions/getUserAction.ts @@ -35,12 +35,14 @@ import deleteSessionId from './deleteSessionId'; * Server action to get the current user. * Returns the user profile if signed in. */ -export async function getUserAction() { +const getUserAction = async (sessionId: string) => { try { const client = AsgardeoNextClient.getInstance(); - const user = await client.getUser(); - return {data: {user}, error: null}; + const user = await client.getUser(sessionId); + return {success: true, data: {user}, error: null}; } catch (error) { - return {data: {user: null}, error: 'Failed to get user'}; + return {success: false, data: {user: null}, error: 'Failed to get user'}; } -} +}; + +export default getUserAction; diff --git a/packages/nextjs/src/server/actions/isSignedIn.ts b/packages/nextjs/src/server/actions/isSignedIn.ts index 46f5a153..08f2e05d 100644 --- a/packages/nextjs/src/server/actions/isSignedIn.ts +++ b/packages/nextjs/src/server/actions/isSignedIn.ts @@ -18,14 +18,15 @@ '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 sessionId: string | undefined = await getSessionId(); + 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; From a446ff053d184f8e51ef46548caa25ca3e560ec4 Mon Sep 17 00:00:00 2001 From: Brion Date: Mon, 30 Jun 2025 01:10:11 +0530 Subject: [PATCH 04/15] feat: Implement new API functions for organization management and user profile updates - Added `getMeOrganizations` and `getOrganization` API functions to retrieve user organizations and organization details respectively. - Introduced `createOrganization` and `updateOrganization` functions for creating and updating organizations. - Implemented `updateMeProfile` function to update user profile information. - Refactored existing SCIM2 API functions to improve structure and maintainability. - Removed deprecated SCIM2 API functions and updated imports across components. - Enhanced documentation with examples for new API functions. --- .../javascript/src/api/createOrganization.ts | 212 ++++++++++++++++++ .../javascript/src/api/getAllOrganizations.ts | 197 ++++++++++++++++ .../javascript/src/api/getMeOrganizations.ts | 203 +++++++++++++++++ .../javascript/src/api/getOrganization.ts | 185 +++++++++++++++ packages/javascript/src/api/getSchemas.ts | 18 +- .../javascript/src/api/updateMeProfile.ts | 159 +++++++++++++ .../src/api}/updateOrganization.ts | 122 +++++++--- packages/javascript/src/index.ts | 12 + .../UserDropdown/UserDropdown.tsx | 193 ++++++++++++++++ .../presentation/UserProfile/UserProfile.tsx | 74 ++++++ .../contexts/Asgardeo/AsgardeoProvider.tsx | 2 +- packages/react/src/AsgardeoReactClient.ts | 2 +- packages/react/src/api/createOrganization.ts | 117 ++++++++++ packages/react/src/api/getAllOrganizations.ts | 110 +++++++++ packages/react/src/api/getMeOrganizations.ts | 111 +++++++++ packages/react/src/api/getOrganization.ts | 103 +++++++++ packages/react/src/api/getSchemas.ts | 3 +- .../react/src/api/scim2/createOrganization.ts | 135 ----------- .../src/api/scim2/getAllOrganizations.ts | 119 ---------- .../react/src/api/scim2/getMeOrganizations.ts | 113 ---------- .../react/src/api/scim2/getOrganization.ts | 116 ---------- packages/react/src/api/scim2/getSchemas.ts | 77 ------- .../react/src/api/scim2/updateMeProfile.ts | 91 -------- packages/react/src/api/updateMeProfile.ts | 90 ++++++++ packages/react/src/api/updateOrganization.ts | 117 ++++++++++ .../BaseCreateOrganization.tsx | 3 +- .../CreateOrganization/CreateOrganization.tsx | 3 +- .../BaseOrganizationProfile.tsx | 3 +- .../OrganizationProfile.tsx | 5 +- .../presentation/UserProfile/UserProfile.tsx | 2 +- .../Organization/OrganizationProvider.tsx | 6 +- packages/react/src/index.ts | 25 ++- 32 files changed, 2006 insertions(+), 722 deletions(-) create mode 100644 packages/javascript/src/api/createOrganization.ts create mode 100644 packages/javascript/src/api/getAllOrganizations.ts create mode 100644 packages/javascript/src/api/getMeOrganizations.ts create mode 100644 packages/javascript/src/api/getOrganization.ts create mode 100644 packages/javascript/src/api/updateMeProfile.ts rename packages/{react/src/api/scim2 => javascript/src/api}/updateOrganization.ts (56%) create mode 100644 packages/nextjs/src/client/components/presentation/UserDropdown/UserDropdown.tsx create mode 100644 packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx create mode 100644 packages/react/src/api/createOrganization.ts create mode 100644 packages/react/src/api/getAllOrganizations.ts create mode 100644 packages/react/src/api/getMeOrganizations.ts create mode 100644 packages/react/src/api/getOrganization.ts delete mode 100644 packages/react/src/api/scim2/createOrganization.ts delete mode 100644 packages/react/src/api/scim2/getAllOrganizations.ts delete mode 100644 packages/react/src/api/scim2/getMeOrganizations.ts delete mode 100644 packages/react/src/api/scim2/getOrganization.ts delete mode 100644 packages/react/src/api/scim2/getSchemas.ts delete mode 100644 packages/react/src/api/scim2/updateMeProfile.ts create mode 100644 packages/react/src/api/updateMeProfile.ts create mode 100644 packages/react/src/api/updateOrganization.ts 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 index 58cd5c76..495872f2 100644 --- a/packages/javascript/src/api/getSchemas.ts +++ b/packages/javascript/src/api/getSchemas.ts @@ -108,7 +108,7 @@ const getSchemas = async ({url, baseUrl, fetcher, ...requestConfig}: GetSchemasC const requestInit: RequestInit = { method: 'GET', headers: { - 'Content-Type': 'application/scim+json', + 'Content-Type': 'application/json', Accept: 'application/json', ...requestConfig.headers, }, @@ -130,21 +130,7 @@ const getSchemas = async ({url, baseUrl, fetcher, ...requestConfig}: GetSchemasC ); } - const responseData = await response.json(); - - // Handle both array response and Resources array response - if (Array.isArray(responseData)) { - return responseData as Schema[]; - } else if ( - responseData && - typeof responseData === 'object' && - 'Resources' in responseData && - Array.isArray(responseData.Resources) - ) { - return responseData.Resources as Schema[]; - } else { - return [responseData] as Schema[]; - } + return (await response.json()) as Schema[]; } catch (error) { if (error instanceof AsgardeoAPIError) { throw error; 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/index.ts b/packages/javascript/src/index.ts index baf3f979..6c1aa6e2 100644 --- a/packages/javascript/src/index.ts +++ b/packages/javascript/src/index.ts @@ -27,6 +27,18 @@ export {default as executeEmbeddedSignUpFlow} from './api/executeEmbeddedSignUpF 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/nextjs/src/client/components/presentation/UserDropdown/UserDropdown.tsx b/packages/nextjs/src/client/components/presentation/UserDropdown/UserDropdown.tsx new file mode 100644 index 00000000..161392ec --- /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(); + }; + + const closeProfile = () => { + setIsProfileOpen(false); + }; + + // Prepare render props data + const renderProps: UserDropdownRenderProps = { + user, + isLoading, + 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..58d1f0cf --- /dev/null +++ b/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {FC, ReactElement} from 'react'; +import {BaseUserProfile, BaseUserProfileProps} from '@asgardeo/react'; +import updateMeProfile from '../../../api/scim2/updateMeProfile'; +import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; +import useUser from '../../../contexts/User/useUser'; + +/** + * 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 updateMeProfile({url: `${baseUrl}/scim2/Me`, payload}); + await revalidateProfile(); + }; + + return ( + + ); +}; + +export default UserProfile; diff --git a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx index 7dd56289..7b23a1a8 100644 --- a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx @@ -42,7 +42,7 @@ const AsgardeoClientProvider: FC> preferences, isSignedIn, signInUrl, - user + user, }: PropsWithChildren) => { const router = useRouter(); const [isDarkMode, setIsDarkMode] = useState(false); diff --git a/packages/react/src/AsgardeoReactClient.ts b/packages/react/src/AsgardeoReactClient.ts index 9205bfa8..a621ef7f 100644 --- a/packages/react/src/AsgardeoReactClient.ts +++ b/packages/react/src/AsgardeoReactClient.ts @@ -37,7 +37,7 @@ 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/getSchemas'; import {AsgardeoReactConfig} from './models/config'; 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 index e6718548..ff1e25a3 100644 --- a/packages/react/src/api/getSchemas.ts +++ b/packages/react/src/api/getSchemas.ts @@ -18,7 +18,6 @@ import { Schema, - AsgardeoAPIError, HttpInstance, AsgardeoSPAClient, HttpRequestConfig, @@ -43,7 +42,7 @@ export interface GetSchemasConfig extends Omit * 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 requestConfig - Request configuration object. + * @param config - Request configuration object. * @returns A promise that resolves with the SCIM2 schemas information. * @example * ```typescript 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/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/presentation/CreateOrganization/BaseCreateOrganization.tsx b/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.tsx index 1dfce0d7..b5a234e7 100644 --- a/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.tsx +++ b/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.tsx @@ -16,10 +16,9 @@ * under the License. */ -import {withVendorCSSClassPrefix} from '@asgardeo/browser'; +import {withVendorCSSClassPrefix, CreateOrganizationPayload} from '@asgardeo/browser'; import clsx from 'clsx'; import {ChangeEvent, CSSProperties, FC, ReactElement, ReactNode, useMemo, useState} from 'react'; -import {CreateOrganizationPayload} from '../../../api/scim2/createOrganization'; import useTheme from '../../../contexts/Theme/useTheme'; import useTranslation from '../../../hooks/useTranslation'; import Button from '../../primitives/Button/Button'; diff --git a/packages/react/src/components/presentation/CreateOrganization/CreateOrganization.tsx b/packages/react/src/components/presentation/CreateOrganization/CreateOrganization.tsx index 8fb5aba0..a6f9aca8 100644 --- a/packages/react/src/components/presentation/CreateOrganization/CreateOrganization.tsx +++ b/packages/react/src/components/presentation/CreateOrganization/CreateOrganization.tsx @@ -19,7 +19,8 @@ import {FC, ReactElement, useState} from 'react'; import {BaseCreateOrganization, BaseCreateOrganizationProps} from './BaseCreateOrganization'; -import createOrganization, {CreateOrganizationPayload} from '../../../api/scim2/createOrganization'; +import {CreateOrganizationPayload} from '@asgardeo/browser'; +import createOrganization from '../../../api/createOrganization'; import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; import useOrganization from '../../../contexts/Organization/useOrganization'; diff --git a/packages/react/src/components/presentation/OrganizationProfile/BaseOrganizationProfile.tsx b/packages/react/src/components/presentation/OrganizationProfile/BaseOrganizationProfile.tsx index 7669ea59..1a1851c5 100644 --- a/packages/react/src/components/presentation/OrganizationProfile/BaseOrganizationProfile.tsx +++ b/packages/react/src/components/presentation/OrganizationProfile/BaseOrganizationProfile.tsx @@ -16,10 +16,9 @@ * under the License. */ -import {withVendorCSSClassPrefix} from '@asgardeo/browser'; +import {withVendorCSSClassPrefix, OrganizationDetails} from '@asgardeo/browser'; import clsx from 'clsx'; import {FC, ReactElement, useMemo, CSSProperties, useState, useCallback, useRef} from 'react'; -import {OrganizationDetails} from '../../../api/scim2/getOrganization'; import useTheme from '../../../contexts/Theme/useTheme'; import {Avatar} from '../../primitives/Avatar/Avatar'; import Button from '../../primitives/Button/Button'; diff --git a/packages/react/src/components/presentation/OrganizationProfile/OrganizationProfile.tsx b/packages/react/src/components/presentation/OrganizationProfile/OrganizationProfile.tsx index 921a1164..e7f6eb0b 100644 --- a/packages/react/src/components/presentation/OrganizationProfile/OrganizationProfile.tsx +++ b/packages/react/src/components/presentation/OrganizationProfile/OrganizationProfile.tsx @@ -18,8 +18,9 @@ import {FC, ReactElement, useEffect, useState} from 'react'; import BaseOrganizationProfile, {BaseOrganizationProfileProps} from './BaseOrganizationProfile'; -import getOrganization, {OrganizationDetails} from '../../../api/scim2/getOrganization'; -import updateOrganization, {createPatchOperations} from '../../../api/scim2/updateOrganization'; +import {OrganizationDetails} from '@asgardeo/browser'; +import getOrganization from '../../../api/getOrganization'; +import updateOrganization, {createPatchOperations} from '../../../api/updateOrganization'; import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; import useTranslation from '../../../hooks/useTranslation'; import {Dialog, DialogContent, DialogHeading} from '../../primitives/Popover/Popover'; diff --git a/packages/react/src/components/presentation/UserProfile/UserProfile.tsx b/packages/react/src/components/presentation/UserProfile/UserProfile.tsx index 266759d9..c8d5a7e1 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'; diff --git a/packages/react/src/contexts/Organization/OrganizationProvider.tsx b/packages/react/src/contexts/Organization/OrganizationProvider.tsx index 9e9f1bcb..3f3dd320 100644 --- a/packages/react/src/contexts/Organization/OrganizationProvider.tsx +++ b/packages/react/src/contexts/Organization/OrganizationProvider.tsx @@ -16,12 +16,12 @@ * under the License. */ -import {AsgardeoRuntimeError, Organization} from '@asgardeo/browser'; +import {AsgardeoRuntimeError, Organization, PaginatedOrganizationsResponse} from '@asgardeo/browser'; import {FC, PropsWithChildren, ReactElement, useCallback, useEffect, useMemo, useState} from 'react'; import OrganizationContext, {OrganizationContextProps, OrganizationWithSwitchAccess} from './OrganizationContext'; import useAsgardeo from '../Asgardeo/useAsgardeo'; -import getAllOrganizations, {PaginatedOrganizationsResponse} from '../../api/scim2/getAllOrganizations'; -import getMeOrganizations from '../../api/scim2/getMeOrganizations'; +import getAllOrganizations from '../../api/getAllOrganizations'; +import getMeOrganizations from '../../api/getMeOrganizations'; /** * Props interface of {@link OrganizationProvider} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index cf52b5b9..a0205b60 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -236,22 +236,27 @@ export {default as Info} from './components/primitives/Icons/Info'; export {default as UserIcon} from './components/primitives/Icons/User'; export {default as LogOut} from './components/primitives/Icons/LogOut'; -export { - createField, - FieldFactory, - validateFieldValue, -} from './components/factories/FieldFactory'; +export {createField, FieldFactory, validateFieldValue} from './components/factories/FieldFactory'; export * from './components/factories/FieldFactory'; export type {FlowStep, FlowMessage, FlowContextValue} from './contexts/Flow/FlowContext'; export type {FlowProviderProps} from './contexts/Flow/FlowProvider'; -// API Functions -export {default as createOrganization} from './api/scim2/createOrganization'; -export * from './api/scim2/createOrganization'; - -export {default as getMeOrganizations} from './api/scim2/getMeOrganizations'; +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 getSchemas} from './api/getSchemas'; +export {default as updateMeProfile} from './api/updateMeProfile'; +export type {GetAllOrganizationsConfig} from './api/getAllOrganizations'; +export type {CreateOrganizationConfig} from './api/createOrganization'; +export type {GetMeOrganizationsConfig} from './api/getMeOrganizations'; +export type {GetOrganizationConfig} from './api/getOrganization'; +export type {UpdateOrganizationConfig} from './api/updateOrganization'; +export type {GetSchemasConfig} from './api/getSchemas'; +export type {UpdateMeProfileConfig} from './api/updateMeProfile'; export {default as getMeProfile} from './api/getScim2Me'; export * from './api/getScim2Me'; From 560abda289a38707ad4a5d0fa5c68fe2e459593c Mon Sep 17 00:00:00 2001 From: Brion Date: Mon, 30 Jun 2025 02:30:40 +0530 Subject: [PATCH 05/15] feat: Add SCIM2 API functions for organization and user profile management; implement user profile update and retrieval actions --- .../src/api/scim2/createOrganization.ts | 0 .../src/api/scim2/getAllOrganizations.ts | 0 .../src/api/scim2/getMeOrganizations.ts | 0 .../src/api/scim2/getOrganization.ts | 0 .../javascript/src/api/scim2/getSchemas.ts | 0 packages/javascript/src/api/scim2/index.ts | 0 .../src/api/scim2/updateMeProfile.ts | 0 .../src/api/scim2/updateOrganization.ts | 0 packages/nextjs/src/AsgardeoNextClient.ts | 67 ++++++++++++++++++- .../UserDropdown/UserDropdown.tsx | 4 +- .../presentation/UserProfile/UserProfile.tsx | 18 +++-- .../contexts/Asgardeo/AsgardeoProvider.tsx | 26 +++---- packages/nextjs/src/index.ts | 6 ++ .../nextjs/src/server/AsgardeoProvider.tsx | 25 ++++--- .../src/server/actions/getUserAction.ts | 11 --- .../server/actions/getUserProfileAction.ts | 48 +++++++++++++ .../server/actions/updateUserProfileAction.ts | 44 ++++++++++++ packages/react/src/AsgardeoReactClient.ts | 2 +- .../react/src/api/scim2/createOrganization.ts | 0 .../src/api/scim2/getAllOrganizations.ts | 0 .../react/src/api/scim2/getMeOrganizations.ts | 0 .../react/src/api/scim2/getOrganization.ts | 0 packages/react/src/api/scim2/getSchemas.ts | 0 packages/react/src/api/scim2/index.ts | 0 .../react/src/api/scim2/updateMeProfile.ts | 0 .../react/src/api/scim2/updateOrganization.ts | 0 .../UserDropdown/UserDropdown.tsx | 4 +- .../presentation/UserProfile/UserProfile.tsx | 2 +- .../Header/AuthenticatedActions.tsx | 4 +- 29 files changed, 212 insertions(+), 49 deletions(-) create mode 100644 packages/javascript/src/api/scim2/createOrganization.ts create mode 100644 packages/javascript/src/api/scim2/getAllOrganizations.ts create mode 100644 packages/javascript/src/api/scim2/getMeOrganizations.ts create mode 100644 packages/javascript/src/api/scim2/getOrganization.ts create mode 100644 packages/javascript/src/api/scim2/getSchemas.ts create mode 100644 packages/javascript/src/api/scim2/index.ts create mode 100644 packages/javascript/src/api/scim2/updateMeProfile.ts create mode 100644 packages/javascript/src/api/scim2/updateOrganization.ts create mode 100644 packages/nextjs/src/server/actions/getUserProfileAction.ts create mode 100644 packages/nextjs/src/server/actions/updateUserProfileAction.ts create mode 100644 packages/react/src/api/scim2/createOrganization.ts create mode 100644 packages/react/src/api/scim2/getAllOrganizations.ts create mode 100644 packages/react/src/api/scim2/getMeOrganizations.ts create mode 100644 packages/react/src/api/scim2/getOrganization.ts create mode 100644 packages/react/src/api/scim2/getSchemas.ts create mode 100644 packages/react/src/api/scim2/index.ts create mode 100644 packages/react/src/api/scim2/updateMeProfile.ts create mode 100644 packages/react/src/api/scim2/updateOrganization.ts diff --git a/packages/javascript/src/api/scim2/createOrganization.ts b/packages/javascript/src/api/scim2/createOrganization.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/javascript/src/api/scim2/getAllOrganizations.ts b/packages/javascript/src/api/scim2/getAllOrganizations.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/javascript/src/api/scim2/getMeOrganizations.ts b/packages/javascript/src/api/scim2/getMeOrganizations.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/javascript/src/api/scim2/getOrganization.ts b/packages/javascript/src/api/scim2/getOrganization.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/javascript/src/api/scim2/getSchemas.ts b/packages/javascript/src/api/scim2/getSchemas.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/javascript/src/api/scim2/index.ts b/packages/javascript/src/api/scim2/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/javascript/src/api/scim2/updateMeProfile.ts b/packages/javascript/src/api/scim2/updateMeProfile.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/javascript/src/api/scim2/updateOrganization.ts b/packages/javascript/src/api/scim2/updateOrganization.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/nextjs/src/AsgardeoNextClient.ts b/packages/nextjs/src/AsgardeoNextClient.ts index 2a7fcf6c..8a7738de 100644 --- a/packages/nextjs/src/AsgardeoNextClient.ts +++ b/packages/nextjs/src/AsgardeoNextClient.ts @@ -37,6 +37,8 @@ import { flattenUserSchema, getScim2Me, getSchemas, + generateFlattenedUserProfile, + updateMeProfile, } from '@asgardeo/node'; import {NextRequest, NextResponse} from 'next/server'; import {AsgardeoNextConfig} from './models/config'; @@ -152,11 +154,70 @@ class AsgardeoNextClient exte } } - 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(), + }; + } } - override getUserProfile(): Promise { + 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 async getOrganizations(): Promise { throw new Error('Method not implemented.'); } diff --git a/packages/nextjs/src/client/components/presentation/UserDropdown/UserDropdown.tsx b/packages/nextjs/src/client/components/presentation/UserDropdown/UserDropdown.tsx index 161392ec..44045c81 100644 --- a/packages/nextjs/src/client/components/presentation/UserDropdown/UserDropdown.tsx +++ b/packages/nextjs/src/client/components/presentation/UserDropdown/UserDropdown.tsx @@ -125,7 +125,7 @@ const UserDropdown: FC = ({ const handleSignOut = () => { signOut(); - onSignOut(); + onSignOut && onSignOut(); }; const closeProfile = () => { @@ -135,7 +135,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/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx b/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx index 58d1f0cf..f064f37a 100644 --- a/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx +++ b/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx @@ -16,11 +16,14 @@ * under the License. */ +'use client'; + import {FC, ReactElement} from 'react'; -import {BaseUserProfile, BaseUserProfileProps} from '@asgardeo/react'; -import updateMeProfile from '../../../api/scim2/updateMeProfile'; +import {BaseUserProfile, BaseUserProfileProps, useUser} from '@asgardeo/react'; import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; -import useUser from '../../../contexts/User/useUser'; +import getSessionId from '../../../../server/actions/getSessionId'; +import updateUserProfileAction from '../../../../server/actions/updateUserProfileAction'; +import { Schema, User } from '@asgardeo/node'; /** * Props for the UserProfile component. @@ -56,15 +59,16 @@ 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}); + console.log('[UserProfile] handleProfileUpdate', baseUrl); + await updateUserProfileAction(payload, (await getSessionId()) as string); await revalidateProfile(); }; return ( diff --git a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx index 7b23a1a8..efe712a7 100644 --- a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx @@ -18,7 +18,12 @@ 'use client'; -import {EmbeddedFlowExecuteRequestConfig, EmbeddedSignInFlowHandleRequestPayload, User} from '@asgardeo/node'; +import { + EmbeddedFlowExecuteRequestConfig, + 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'; @@ -32,10 +37,12 @@ export type AsgardeoClientProviderProps = Partial> = ({ + baseUrl, children, signIn, signOut, @@ -43,10 +50,12 @@ const AsgardeoClientProvider: FC> isSignedIn, signInUrl, user, + userProfile, }: PropsWithChildren) => { const router = useRouter(); const [isDarkMode, setIsDarkMode] = useState(false); - const [isLoading, setIsLoading] = useState(true); + const [isLoading, setIsLoading] = useState(true); + const [_userProfile, setUserProfile] = useState(userProfile); useEffect(() => { if (!preferences?.theme?.mode || preferences.theme.mode === 'system') { @@ -113,6 +122,7 @@ const AsgardeoClientProvider: FC> const contextValue = useMemo( () => ({ + baseUrl, user, isSignedIn, isLoading, @@ -120,7 +130,7 @@ const AsgardeoClientProvider: FC> signOut: handleSignOut, signInUrl, }), - [user, isSignedIn, isLoading, signInUrl], + [baseUrl, user, isSignedIn, isLoading, signInUrl], ); return ( @@ -128,15 +138,7 @@ const AsgardeoClientProvider: FC> - - {children} - + {children} diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index 056d69ed..42d0070e 100644 --- a/packages/nextjs/src/index.ts +++ b/packages/nextjs/src/index.ts @@ -42,6 +42,12 @@ 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 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 acebe78e..46ff2954 100644 --- a/packages/nextjs/src/server/AsgardeoProvider.tsx +++ b/packages/nextjs/src/server/AsgardeoProvider.tsx @@ -17,7 +17,7 @@ */ import {FC, PropsWithChildren, ReactElement} from 'react'; -import {AsgardeoRuntimeError, User} from '@asgardeo/node'; +import {AsgardeoRuntimeError, User, UserProfile} from '@asgardeo/node'; import AsgardeoClientProvider, {AsgardeoClientProviderProps} from '../client/contexts/Asgardeo/AsgardeoProvider'; import AsgardeoNextClient from '../AsgardeoNextClient'; import signInAction from './actions/signInAction'; @@ -26,6 +26,7 @@ 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'; /** * Props interface of {@link AsgardeoServerProvider} @@ -54,15 +55,15 @@ const AsgardeoServerProvider: FC> children, afterSignInUrl, afterSignOutUrl, - ...config + ..._config }: PropsWithChildren): Promise => { const asgardeoClient = AsgardeoNextClient.getInstance(); - let configuration: Partial = {}; + let config: Partial = {}; console.log('Initializing Asgardeo client with config:', config); try { - await asgardeoClient.initialize(config); - configuration = await asgardeoClient.getConfiguration(); + await asgardeoClient.initialize(_config); + config = await asgardeoClient.getConfiguration(); } catch (error) { throw new AsgardeoRuntimeError( `Failed to initialize Asgardeo client: ${error?.toString()}`, @@ -78,11 +79,18 @@ const AsgardeoServerProvider: FC> const _isSignedIn: boolean = await isSignedIn(); let user: User = {}; + let userProfile: UserProfile = { + schemas: [], + profile: {}, + flattenedProfile: {}, + }; if (_isSignedIn) { - const response = await getUserAction((await getSessionId()) as string); + const userResponse = await getUserAction((await getSessionId()) as string); + const userProfileResponse = await getUserProfileAction((await getSessionId()) as string); - user = response.data?.user || {}; + user = userResponse.data?.user || {}; + userProfile = userProfileResponse.data?.userProfile; } return ( @@ -90,10 +98,11 @@ const AsgardeoServerProvider: FC> baseUrl={config.baseUrl} signIn={signInAction} signOut={signOutAction} - signInUrl={configuration?.signInUrl} + signInUrl={config.signInUrl} preferences={config.preferences} clientId={config.clientId} user={user} + userProfile={userProfile} isSignedIn={_isSignedIn} > {children} diff --git a/packages/nextjs/src/server/actions/getUserAction.ts b/packages/nextjs/src/server/actions/getUserAction.ts index 17c084ac..3abcb9b4 100644 --- a/packages/nextjs/src/server/actions/getUserAction.ts +++ b/packages/nextjs/src/server/actions/getUserAction.ts @@ -18,18 +18,7 @@ 'use server'; -import {redirect} from 'next/navigation'; -import {cookies} from 'next/headers'; -import { - CookieConfig, - generateSessionId, - EmbeddedSignInFlowStatus, - EmbeddedSignInFlowHandleRequestPayload, - EmbeddedFlowExecuteRequestConfig, - EmbeddedSignInFlowInitiateResponse, -} from '@asgardeo/node'; import AsgardeoNextClient from '../../AsgardeoNextClient'; -import deleteSessionId from './deleteSessionId'; /** * Server action to get the current user. diff --git a/packages/nextjs/src/server/actions/getUserProfileAction.ts b/packages/nextjs/src/server/actions/getUserProfileAction.ts new file mode 100644 index 00000000..3b64bf50 --- /dev/null +++ b/packages/nextjs/src/server/actions/getUserProfileAction.ts @@ -0,0 +1,48 @@ +/** + * 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 {UserProfile} from '@asgardeo/node'; +import AsgardeoNextClient from '../../AsgardeoNextClient'; + +/** + * Server action to get the current user. + * Returns the user profile if signed in. + */ +const getUserProfileAction = async (sessionId: string) => { + try { + const client = AsgardeoNextClient.getInstance(); + const updatedProfile: UserProfile = await client.getUserProfile(sessionId); + return {success: true, data: {userProfile: updatedProfile}, error: null}; + } catch (error) { + return { + success: false, + data: { + userProfile: { + schemas: [], + profile: {}, + flattenedProfile: {}, + }, + }, + error: 'Failed to get user profile', + }; + } +}; + +export default getUserProfileAction; 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 a621ef7f..19af191c 100644 --- a/packages/react/src/AsgardeoReactClient.ts +++ b/packages/react/src/AsgardeoReactClient.ts @@ -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) { diff --git a/packages/react/src/api/scim2/createOrganization.ts b/packages/react/src/api/scim2/createOrganization.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/react/src/api/scim2/getAllOrganizations.ts b/packages/react/src/api/scim2/getAllOrganizations.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/react/src/api/scim2/getMeOrganizations.ts b/packages/react/src/api/scim2/getMeOrganizations.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/react/src/api/scim2/getOrganization.ts b/packages/react/src/api/scim2/getOrganization.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/react/src/api/scim2/getSchemas.ts b/packages/react/src/api/scim2/getSchemas.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/react/src/api/scim2/index.ts b/packages/react/src/api/scim2/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/react/src/api/scim2/updateMeProfile.ts b/packages/react/src/api/scim2/updateMeProfile.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/react/src/api/scim2/updateOrganization.ts b/packages/react/src/api/scim2/updateOrganization.ts new file mode 100644 index 00000000..e69de29b 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 c8d5a7e1..25d13895 100644 --- a/packages/react/src/components/presentation/UserProfile/UserProfile.tsx +++ b/packages/react/src/components/presentation/UserProfile/UserProfile.tsx @@ -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/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; From eda115b7b9606d2f1bdb5857c851911b5698e7dd Mon Sep 17 00:00:00 2001 From: Brion Date: Mon, 30 Jun 2025 03:45:48 +0530 Subject: [PATCH 06/15] feat: Implement embedded sign-up flow; add SignUp component and related actions; enhance SignUpButton and context management --- packages/nextjs/src/AsgardeoNextClient.ts | 39 +++-- .../actions/SignInButton/SignInButton.tsx | 2 +- .../actions/SignUpButton/SignUpButton.tsx | 115 ++++++++++--- .../components/presentation/SignUp/SignUp.tsx | 102 ++++++++++++ .../contexts/Asgardeo/AsgardeoContext.ts | 1 + .../contexts/Asgardeo/AsgardeoProvider.tsx | 40 ++++- packages/nextjs/src/index.ts | 6 + .../nextjs/src/server/AsgardeoProvider.tsx | 3 + .../nextjs/src/server/actions/signUpAction.ts | 77 +++++++++ .../actions/SignUpButton/SignUpButton.tsx | 11 +- .../src/contexts/Asgardeo/AsgardeoContext.ts | 2 + .../contexts/Asgardeo/AsgardeoProvider.tsx | 4 + samples/teamspace-nextjs/app/signin/page.tsx | 2 - samples/teamspace-nextjs/app/signup/page.tsx | 152 +----------------- .../components/Header/PublicActions.tsx | 14 +- 15 files changed, 370 insertions(+), 200 deletions(-) create mode 100644 packages/nextjs/src/client/components/presentation/SignUp/SignUp.tsx create mode 100644 packages/nextjs/src/server/actions/signUpAction.ts diff --git a/packages/nextjs/src/AsgardeoNextClient.ts b/packages/nextjs/src/AsgardeoNextClient.ts index 8a7738de..3c1428a2 100644 --- a/packages/nextjs/src/AsgardeoNextClient.ts +++ b/packages/nextjs/src/AsgardeoNextClient.ts @@ -39,6 +39,7 @@ import { getSchemas, generateFlattenedUserProfile, updateMeProfile, + executeEmbeddedSignUpFlow, } from '@asgardeo/node'; import {NextRequest, NextResponse} from 'next/server'; import {AsgardeoNextConfig} from './models/config'; @@ -99,18 +100,6 @@ 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({ @@ -314,11 +303,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.', ); } 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/presentation/SignUp/SignUp.tsx b/packages/nextjs/src/client/components/presentation/SignUp/SignUp.tsx new file mode 100644 index 00000000..17dba8e9 --- /dev/null +++ b/packages/nextjs/src/client/components/presentation/SignUp/SignUp.tsx @@ -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 client'; + +import { + EmbeddedFlowExecuteRequestPayload, + EmbeddedFlowExecuteResponse, + EmbeddedFlowResponseType, + EmbeddedFlowType, +} from '@asgardeo/node'; +import {FC} from 'react'; +import {BaseSignUp, BaseSignUpProps} from '@asgardeo/react'; +import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; + +/** + * Props for the SignUp component. + */ +export type SignUpProps = BaseSignUpProps; + +/** + * A styled SignUp component that provides embedded sign-up flow with pre-built styling. + * This component handles the API calls for sign-up and delegates UI logic to BaseSignUp. + * + * @example + * ```tsx + * import { SignUp } from '@asgardeo/react'; + * + * const App = () => { + * 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/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 efe712a7..38d3897b 100644 --- a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx @@ -20,6 +20,7 @@ import { EmbeddedFlowExecuteRequestConfig, + EmbeddedFlowExecuteRequestPayload, EmbeddedSignInFlowHandleRequestPayload, User, UserProfile, @@ -36,6 +37,7 @@ export type AsgardeoClientProviderProps = Partial & { signOut: AsgardeoContextProps['signOut']; signIn: AsgardeoContextProps['signIn']; + signUp: AsgardeoContextProps['signUp']; isSignedIn: boolean; userProfile: UserProfile; user: User | null; @@ -46,9 +48,11 @@ const AsgardeoClientProvider: FC> children, signIn, signOut, + signUp, preferences, isSignedIn, signInUrl, + signUpUrl, user, userProfile, }: PropsWithChildren) => { @@ -101,6 +105,38 @@ const AsgardeoClientProvider: FC> } }; + const handleSignUp = async ( + payload: EmbeddedFlowExecuteRequestPayload, + request: EmbeddedFlowExecuteRequestConfig, + ) => { + console.log('[AsgardeoClientProvider] Executing sign-up action with payload', payload); + try { + 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); + + return; + } + + if (result?.error) { + throw new Error(result.error); + } + + return result?.data ?? result; + } catch (error) { + throw error; + } + }; + const handleSignOut = async () => { try { const result = await signOut(); @@ -128,9 +164,11 @@ const AsgardeoClientProvider: FC> isLoading, signIn: handleSignIn, signOut: handleSignOut, + signUp: handleSignUp, signInUrl, + signUpUrl, }), - [baseUrl, user, isSignedIn, isLoading, signInUrl], + [baseUrl, user, isSignedIn, isLoading, signInUrl, signUpUrl], ); return ( diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index 42d0070e..c4d5551a 100644 --- a/packages/nextjs/src/index.ts +++ b/packages/nextjs/src/index.ts @@ -33,6 +33,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 +45,9 @@ 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'; diff --git a/packages/nextjs/src/server/AsgardeoProvider.tsx b/packages/nextjs/src/server/AsgardeoProvider.tsx index 46ff2954..49a98e64 100644 --- a/packages/nextjs/src/server/AsgardeoProvider.tsx +++ b/packages/nextjs/src/server/AsgardeoProvider.tsx @@ -27,6 +27,7 @@ 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'; /** * Props interface of {@link AsgardeoServerProvider} @@ -98,7 +99,9 @@ const AsgardeoServerProvider: FC> baseUrl={config.baseUrl} signIn={signInAction} signOut={signOutAction} + signUp={signUpAction} signInUrl={config.signInUrl} + signUpUrl={config.signUpUrl} preferences={config.preferences} clientId={config.clientId} user={user} diff --git a/packages/nextjs/src/server/actions/signUpAction.ts b/packages/nextjs/src/server/actions/signUpAction.ts new file mode 100644 index 00000000..fb6982ef --- /dev/null +++ b/packages/nextjs/src/server/actions/signUpAction.ts @@ -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 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(); + + console.log('Executing sign-up action with payload:', payload); + + // 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/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 = 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/PublicActions.tsx b/samples/teamspace-nextjs/components/Header/PublicActions.tsx index 3a7a4d1d..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,9 +21,7 @@ export default function PublicActions({className = '', showMobileActions = false return (
- +
); } From be5600c44427cb84f44ff744b4b6363f88630b3c Mon Sep 17 00:00:00 2001 From: Brion Date: Mon, 30 Jun 2025 03:48:34 +0530 Subject: [PATCH 07/15] feat: Remove deprecated SCIM2 API files; clean up organization and profile management endpoints --- .github/instructions/asgardeo.instructions.md | 28 +++++++++++++++++++ .../src/api/scim2/createOrganization.ts | 0 .../src/api/scim2/getAllOrganizations.ts | 0 .../src/api/scim2/getMeOrganizations.ts | 0 .../src/api/scim2/getOrganization.ts | 0 .../javascript/src/api/scim2/getSchemas.ts | 0 packages/javascript/src/api/scim2/index.ts | 0 .../src/api/scim2/updateMeProfile.ts | 0 .../src/api/scim2/updateOrganization.ts | 0 .../react/src/api/scim2/createOrganization.ts | 0 .../src/api/scim2/getAllOrganizations.ts | 0 .../react/src/api/scim2/getMeOrganizations.ts | 0 .../react/src/api/scim2/getOrganization.ts | 0 packages/react/src/api/scim2/getSchemas.ts | 0 packages/react/src/api/scim2/index.ts | 0 .../react/src/api/scim2/updateMeProfile.ts | 0 .../react/src/api/scim2/updateOrganization.ts | 0 17 files changed, 28 insertions(+) create mode 100644 .github/instructions/asgardeo.instructions.md delete mode 100644 packages/javascript/src/api/scim2/createOrganization.ts delete mode 100644 packages/javascript/src/api/scim2/getAllOrganizations.ts delete mode 100644 packages/javascript/src/api/scim2/getMeOrganizations.ts delete mode 100644 packages/javascript/src/api/scim2/getOrganization.ts delete mode 100644 packages/javascript/src/api/scim2/getSchemas.ts delete mode 100644 packages/javascript/src/api/scim2/index.ts delete mode 100644 packages/javascript/src/api/scim2/updateMeProfile.ts delete mode 100644 packages/javascript/src/api/scim2/updateOrganization.ts delete mode 100644 packages/react/src/api/scim2/createOrganization.ts delete mode 100644 packages/react/src/api/scim2/getAllOrganizations.ts delete mode 100644 packages/react/src/api/scim2/getMeOrganizations.ts delete mode 100644 packages/react/src/api/scim2/getOrganization.ts delete mode 100644 packages/react/src/api/scim2/getSchemas.ts delete mode 100644 packages/react/src/api/scim2/index.ts delete mode 100644 packages/react/src/api/scim2/updateMeProfile.ts delete mode 100644 packages/react/src/api/scim2/updateOrganization.ts diff --git a/.github/instructions/asgardeo.instructions.md b/.github/instructions/asgardeo.instructions.md new file mode 100644 index 00000000..83a5d1a7 --- /dev/null +++ b/.github/instructions/asgardeo.instructions.md @@ -0,0 +1,28 @@ +--- +applyTo: '**' +--- + +# `@asgardeo/react` SDK Documentation + +## Quick Start + +1. Add `` to your app + +```tsx +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' +import { AsgardeoProvider } from '@asgardeo/react' + +createRoot(document.getElementById('root')).render( + + ' + clientId: '' + > + + + +) +``` diff --git a/packages/javascript/src/api/scim2/createOrganization.ts b/packages/javascript/src/api/scim2/createOrganization.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/javascript/src/api/scim2/getAllOrganizations.ts b/packages/javascript/src/api/scim2/getAllOrganizations.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/javascript/src/api/scim2/getMeOrganizations.ts b/packages/javascript/src/api/scim2/getMeOrganizations.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/javascript/src/api/scim2/getOrganization.ts b/packages/javascript/src/api/scim2/getOrganization.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/javascript/src/api/scim2/getSchemas.ts b/packages/javascript/src/api/scim2/getSchemas.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/scim2/updateMeProfile.ts b/packages/javascript/src/api/scim2/updateMeProfile.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/javascript/src/api/scim2/updateOrganization.ts b/packages/javascript/src/api/scim2/updateOrganization.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/react/src/api/scim2/createOrganization.ts b/packages/react/src/api/scim2/createOrganization.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/react/src/api/scim2/getAllOrganizations.ts b/packages/react/src/api/scim2/getAllOrganizations.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/react/src/api/scim2/getMeOrganizations.ts b/packages/react/src/api/scim2/getMeOrganizations.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 e69de29b..00000000 diff --git a/packages/react/src/api/scim2/getSchemas.ts b/packages/react/src/api/scim2/getSchemas.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/react/src/api/scim2/index.ts b/packages/react/src/api/scim2/index.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/react/src/api/scim2/updateMeProfile.ts b/packages/react/src/api/scim2/updateMeProfile.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/react/src/api/scim2/updateOrganization.ts b/packages/react/src/api/scim2/updateOrganization.ts deleted file mode 100644 index e69de29b..00000000 From bc360c1bed85736d6f80a7c4e21c94f0bf1b1513 Mon Sep 17 00:00:00 2001 From: Brion Date: Mon, 30 Jun 2025 04:16:15 +0530 Subject: [PATCH 08/15] feat: Implement OAuth callback handling; add handleOAuthCallbackAction and integrate with AsgardeoProvider --- .../contexts/Asgardeo/AsgardeoProvider.tsx | 55 +++++++++- packages/nextjs/src/index.ts | 2 + .../nextjs/src/server/AsgardeoProvider.tsx | 2 + .../actions/handleOAuthCallbackAction.ts | 102 ++++++++++++++++++ .../nextjs/src/server/actions/signInAction.ts | 3 - samples/teamspace-nextjs/app/actions/auth.ts | 29 ----- 6 files changed, 160 insertions(+), 33 deletions(-) create mode 100644 packages/nextjs/src/server/actions/handleOAuthCallbackAction.ts delete mode 100644 samples/teamspace-nextjs/app/actions/auth.ts diff --git a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx index 38d3897b..f5a3b590 100644 --- a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx @@ -27,7 +27,7 @@ import { } 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'; /** @@ -38,6 +38,7 @@ export type AsgardeoClientProviderProps = Partial Promise<{success: boolean; error?: string; redirectUrl?: string}>; isSignedIn: boolean; userProfile: UserProfile; user: User | null; @@ -49,6 +50,7 @@ const AsgardeoClientProvider: FC> signIn, signOut, signUp, + handleOAuthCallback, preferences, isSignedIn, signInUrl, @@ -57,10 +59,61 @@ const AsgardeoClientProvider: FC> userProfile, }: PropsWithChildren) => { const router = useRouter(); + const searchParams = useSearchParams(); const [isDarkMode, setIsDarkMode] = 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) { + console.log('[AsgardeoClientProvider] Handling OAuth callback'); + 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 { + console.error('[AsgardeoClientProvider] OAuth callback failed:', result.error); + 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') { setIsDarkMode(window.matchMedia('(prefers-color-scheme: dark)').matches); diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index c4d5551a..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'; diff --git a/packages/nextjs/src/server/AsgardeoProvider.tsx b/packages/nextjs/src/server/AsgardeoProvider.tsx index 49a98e64..eeb1a081 100644 --- a/packages/nextjs/src/server/AsgardeoProvider.tsx +++ b/packages/nextjs/src/server/AsgardeoProvider.tsx @@ -28,6 +28,7 @@ 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'; /** * Props interface of {@link AsgardeoServerProvider} @@ -100,6 +101,7 @@ const AsgardeoServerProvider: FC> signIn={signInAction} signOut={signOutAction} signUp={signUpAction} + handleOAuthCallback={handleOAuthCallbackAction} signInUrl={config.signInUrl} signUpUrl={config.signUpUrl} preferences={config.preferences} 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/signInAction.ts b/packages/nextjs/src/server/actions/signInAction.ts index 436a3fb9..4edc3da1 100644 --- a/packages/nextjs/src/server/actions/signInAction.ts +++ b/packages/nextjs/src/server/actions/signInAction.ts @@ -73,9 +73,6 @@ const signInAction = async ( return {success: true, data: {signInUrl: String(defaultSignInUrl)}}; } else { - console.log('[signInAction] Handling embedded sign-in flow with payload:', payload); - console.log('[signInAction] Request config:', request); - console.log('[signInAction] User ID:', userId); const response: any = await client.signIn(payload, request!, userId); if (response.flowStatus === EmbeddedSignInFlowStatus.SuccessCompleted) { diff --git a/samples/teamspace-nextjs/app/actions/auth.ts b/samples/teamspace-nextjs/app/actions/auth.ts deleted file mode 100644 index 5e591ac5..00000000 --- a/samples/teamspace-nextjs/app/actions/auth.ts +++ /dev/null @@ -1,29 +0,0 @@ -"use server" - -import { redirect } from "next/navigation" -import { login, signup, logout } from "@/lib/auth" - -export async function loginAction(formData: FormData) { - const result = await login(formData) - - if (result.success) { - redirect("/dashboard") - } - - return result -} - -export async function signupAction(formData: FormData) { - const result = await signup(formData) - - if (result.success) { - redirect("/dashboard") - } - - return result -} - -export async function logoutAction() { - await logout() - redirect("/") -} From 226fd19be4b7cee4af5d2908763fa4ef37ae5e7c Mon Sep 17 00:00:00 2001 From: Brion Date: Mon, 30 Jun 2025 04:44:32 +0530 Subject: [PATCH 09/15] chore(nextjs): fix signin issue --- .../components/control/SignedIn/SignedIn.tsx | 22 ++++--------------- .../control/SignedOut/SignedOut.tsx | 22 ++++--------------- .../nextjs/src/server/AsgardeoProvider.tsx | 8 ++++--- .../nextjs/src/server/actions/isSignedIn.ts | 3 +-- 4 files changed, 14 insertions(+), 41 deletions(-) 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/server/AsgardeoProvider.tsx b/packages/nextjs/src/server/AsgardeoProvider.tsx index eeb1a081..f1f9eb93 100644 --- a/packages/nextjs/src/server/AsgardeoProvider.tsx +++ b/packages/nextjs/src/server/AsgardeoProvider.tsx @@ -79,7 +79,9 @@ const AsgardeoServerProvider: FC> return <>; } - const _isSignedIn: boolean = await isSignedIn(); + const sessionId: string = await getSessionId() as string; + const _isSignedIn: boolean = await isSignedIn(sessionId); + let user: User = {}; let userProfile: UserProfile = { schemas: [], @@ -88,8 +90,8 @@ const AsgardeoServerProvider: FC> }; if (_isSignedIn) { - const userResponse = await getUserAction((await getSessionId()) as string); - const userProfileResponse = await getUserProfileAction((await getSessionId()) as string); + const userResponse = await getUserAction((sessionId)); + const userProfileResponse = await getUserProfileAction((sessionId)); user = userResponse.data?.user || {}; userProfile = userProfileResponse.data?.userProfile; diff --git a/packages/nextjs/src/server/actions/isSignedIn.ts b/packages/nextjs/src/server/actions/isSignedIn.ts index 08f2e05d..746b6784 100644 --- a/packages/nextjs/src/server/actions/isSignedIn.ts +++ b/packages/nextjs/src/server/actions/isSignedIn.ts @@ -21,8 +21,7 @@ import AsgardeoNextClient from '../../AsgardeoNextClient'; import getSessionId from './getSessionId'; -const isSignedIn = async (): Promise => { - const sessionId: string | undefined = await getSessionId(); +const isSignedIn = async (sessionId: string): Promise => { const client = AsgardeoNextClient.getInstance(); const accessToken: string | undefined = await client.getAccessToken(sessionId); From 96bd0bc177a812c6623661e43153ed276a8de19b Mon Sep 17 00:00:00 2001 From: Brion Date: Mon, 30 Jun 2025 04:47:19 +0530 Subject: [PATCH 10/15] chore: update versioning for @asgardeo/nextjs and @asgardeo/react to minor --- .changeset/metal-colts-lose.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/metal-colts-lose.md b/.changeset/metal-colts-lose.md index 1690cee8..7bc6c981 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': minor --- Stabilize the SDK From 5cb9da56f3269c31aad50e945d9777f072a99430 Mon Sep 17 00:00:00 2001 From: Brion Date: Mon, 30 Jun 2025 04:52:46 +0530 Subject: [PATCH 11/15] feat: remove Asgardeo SDK documentation instructions --- .github/instructions/asgardeo.instructions.md | 28 ------------------- 1 file changed, 28 deletions(-) delete mode 100644 .github/instructions/asgardeo.instructions.md diff --git a/.github/instructions/asgardeo.instructions.md b/.github/instructions/asgardeo.instructions.md deleted file mode 100644 index 83a5d1a7..00000000 --- a/.github/instructions/asgardeo.instructions.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -applyTo: '**' ---- - -# `@asgardeo/react` SDK Documentation - -## Quick Start - -1. Add `` to your app - -```tsx -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' -import { AsgardeoProvider } from '@asgardeo/react' - -createRoot(document.getElementById('root')).render( - - ' - clientId: '' - > - - - -) -``` From 17700ebbc652e9440cd0c1a2ed6a944c7e3debd6 Mon Sep 17 00:00:00 2001 From: Brion Date: Mon, 30 Jun 2025 04:55:22 +0530 Subject: [PATCH 12/15] chore: remove console.log statements for cleaner code --- packages/javascript/src/IsomorphicCrypto.ts | 1 - packages/javascript/src/__legacy__/client.ts | 4 ---- .../src/__legacy__/helpers/authentication-helper.ts | 1 - .../javascript/src/api/initializeEmbeddedSignInFlow.ts | 2 -- packages/nextjs/src/AsgardeoNextClient.ts | 2 -- .../components/presentation/UserProfile/UserProfile.tsx | 1 - .../src/client/contexts/Asgardeo/AsgardeoProvider.tsx | 7 ++----- packages/nextjs/src/server/AsgardeoProvider.tsx | 1 - packages/nextjs/src/server/actions/signUpAction.ts | 2 -- packages/node/src/__legacy__/client.ts | 1 - packages/node/src/__legacy__/core/authentication.ts | 1 - .../src/components/presentation/SignIn/BaseSignIn.tsx | 6 ------ 12 files changed, 2 insertions(+), 27 deletions(-) diff --git a/packages/javascript/src/IsomorphicCrypto.ts b/packages/javascript/src/IsomorphicCrypto.ts index edf8e9fe..2ff20aed 100644 --- a/packages/javascript/src/IsomorphicCrypto.ts +++ b/packages/javascript/src/IsomorphicCrypto.ts @@ -138,7 +138,6 @@ export class IsomorphicCrypto { */ public decodeIdToken(idToken: string): IdToken { try { - console.log('[IsomorphicCrypto] Decoding ID token:', idToken); const utf8String: string = this._cryptoUtils.base64URLDecode(idToken?.split('.')[1]); const payload: IdToken = JSON.parse(utf8String); diff --git a/packages/javascript/src/__legacy__/client.ts b/packages/javascript/src/__legacy__/client.ts index ef2ce1dd..0cfd1ef4 100644 --- a/packages/javascript/src/__legacy__/client.ts +++ b/packages/javascript/src/__legacy__/client.ts @@ -361,8 +361,6 @@ export class AsgardeoAuthClient { let tokenResponse: Response; - console.log('[AsgardeoAuthClient] Requesting access token from:', tokenEndpoint); - try { tokenResponse = await fetch(tokenEndpoint, { body: body, @@ -634,9 +632,7 @@ export class AsgardeoAuthClient { * @preserve */ public async getUser(userId?: string): Promise { - console.log('[AsgardeoAuthClient] Getting user with userId:', userId); const sessionData: SessionData = await this._storageManager.getSessionData(userId); - console.log('[AsgardeoAuthClient] Session data:', sessionData); const authenticatedUser: User = this._authenticationHelper.getAuthenticatedUserInfo(sessionData?.id_token); Object.keys(authenticatedUser).forEach((key: string) => { diff --git a/packages/javascript/src/__legacy__/helpers/authentication-helper.ts b/packages/javascript/src/__legacy__/helpers/authentication-helper.ts index e9bc279b..87cdcdb7 100644 --- a/packages/javascript/src/__legacy__/helpers/authentication-helper.ts +++ b/packages/javascript/src/__legacy__/helpers/authentication-helper.ts @@ -288,7 +288,6 @@ export class AuthenticationHelper { await this._storageManager.setSessionData(parsedResponse, userId); - console.log('[AuthenticationHelper] Token response handled successfully:', userId, tokenResponse); return Promise.resolve(tokenResponse); } } 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/nextjs/src/AsgardeoNextClient.ts b/packages/nextjs/src/AsgardeoNextClient.ts index 3c1428a2..9c8de194 100644 --- a/packages/nextjs/src/AsgardeoNextClient.ts +++ b/packages/nextjs/src/AsgardeoNextClient.ts @@ -260,8 +260,6 @@ class AsgardeoNextClient exte }), ); - console.log('[AsgardeoNextClient] Redirecting to sign-in URL:', defaultSignInUrl); - return initializeEmbeddedSignInFlow({ url: `${defaultSignInUrl.origin}${defaultSignInUrl.pathname}`, payload: Object.fromEntries(defaultSignInUrl.searchParams.entries()), diff --git a/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx b/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx index f064f37a..73c24427 100644 --- a/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx +++ b/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx @@ -59,7 +59,6 @@ const UserProfile: FC = ({...rest}: UserProfileProps): ReactEl const {profile, flattenedProfile, schemas, revalidateProfile} = useUser(); const handleProfileUpdate = async (payload: any): Promise => { - console.log('[UserProfile] handleProfileUpdate', baseUrl); await updateUserProfileAction(payload, (await getSessionId()) as string); await revalidateProfile(); }; diff --git a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx index f5a3b590..cc0dcbe2 100644 --- a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx @@ -87,11 +87,10 @@ const AsgardeoClientProvider: FC> // Handle OAuth callback if code and state are present if (code && state) { - console.log('[AsgardeoClientProvider] Handling OAuth callback'); setIsLoading(true); - + const result = await handleOAuthCallback(code, state, sessionState || undefined); - + if (result.success) { // Redirect to the success URL if (result.redirectUrl) { @@ -101,7 +100,6 @@ const AsgardeoClientProvider: FC> window.location.reload(); } } else { - console.error('[AsgardeoClientProvider] OAuth callback failed:', result.error); router.push(`/signin?error=authentication_failed&error_description=${encodeURIComponent(result.error || 'Authentication failed')}`); } } @@ -162,7 +160,6 @@ const AsgardeoClientProvider: FC> payload: EmbeddedFlowExecuteRequestPayload, request: EmbeddedFlowExecuteRequestConfig, ) => { - console.log('[AsgardeoClientProvider] Executing sign-up action with payload', payload); try { const result = await signUp(payload, request); diff --git a/packages/nextjs/src/server/AsgardeoProvider.tsx b/packages/nextjs/src/server/AsgardeoProvider.tsx index f1f9eb93..f6c61a0e 100644 --- a/packages/nextjs/src/server/AsgardeoProvider.tsx +++ b/packages/nextjs/src/server/AsgardeoProvider.tsx @@ -61,7 +61,6 @@ const AsgardeoServerProvider: FC> }: PropsWithChildren): Promise => { const asgardeoClient = AsgardeoNextClient.getInstance(); let config: Partial = {}; - console.log('Initializing Asgardeo client with config:', config); try { await asgardeoClient.initialize(_config); diff --git a/packages/nextjs/src/server/actions/signUpAction.ts b/packages/nextjs/src/server/actions/signUpAction.ts index fb6982ef..2e9dd7f6 100644 --- a/packages/nextjs/src/server/actions/signUpAction.ts +++ b/packages/nextjs/src/server/actions/signUpAction.ts @@ -50,8 +50,6 @@ const signUpAction = async ( try { const client = AsgardeoNextClient.getInstance(); - console.log('Executing sign-up action with payload:', payload); - // 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) { diff --git a/packages/node/src/__legacy__/client.ts b/packages/node/src/__legacy__/client.ts index abe76edd..3b66b8ac 100644 --- a/packages/node/src/__legacy__/client.ts +++ b/packages/node/src/__legacy__/client.ts @@ -202,7 +202,6 @@ export class AsgardeoNodeClient { * */ public async getUser(userId: string): Promise { - console.log('[LegacyAsgardeoNodeClient] Getting user with userId:', this._authCore); return this._authCore.getUser(userId); } diff --git a/packages/node/src/__legacy__/core/authentication.ts b/packages/node/src/__legacy__/core/authentication.ts index 71052c3c..00e33fdb 100644 --- a/packages/node/src/__legacy__/core/authentication.ts +++ b/packages/node/src/__legacy__/core/authentication.ts @@ -212,7 +212,6 @@ export class AsgardeoNodeCore { } public async getUser(userId: string): Promise { - console.log(`[AsgardeoNodeCore] Getting user with userId: ${userId}`); return this._auth.getUser(userId); } diff --git a/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx b/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx index c0e46864..c25fe78f 100644 --- a/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx +++ b/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx @@ -127,12 +127,6 @@ const handleWebAuthnAuthentication = async (challengeData: string): Promise Date: Mon, 30 Jun 2025 05:24:11 +0530 Subject: [PATCH 13/15] chore(javascript): add PROFILE scope to OIDC request constants for enhanced user profile access --- packages/javascript/src/constants/OIDCRequestConstants.ts | 2 +- packages/javascript/src/constants/ScopeConstants.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) 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; From 9dcde7a934efa894722e2c1f3bc323840958d2bc Mon Sep 17 00:00:00 2001 From: Brion Date: Mon, 30 Jun 2025 06:01:46 +0530 Subject: [PATCH 14/15] fix: update AsgardeoServerProviderProps to use Partial and improve type safety chore: exclude teamspace-nextjs from pnpm workspace packages --- packages/nextjs/src/server/AsgardeoProvider.tsx | 11 ++++++----- pnpm-workspace.yaml | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/nextjs/src/server/AsgardeoProvider.tsx b/packages/nextjs/src/server/AsgardeoProvider.tsx index f6c61a0e..21efda1d 100644 --- a/packages/nextjs/src/server/AsgardeoProvider.tsx +++ b/packages/nextjs/src/server/AsgardeoProvider.tsx @@ -29,11 +29,12 @@ 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; }; @@ -63,7 +64,7 @@ const AsgardeoServerProvider: FC> let config: Partial = {}; try { - await asgardeoClient.initialize(_config); + await asgardeoClient.initialize(_config as AsgardeoNextConfig); config = await asgardeoClient.getConfiguration(); } catch (error) { throw new AsgardeoRuntimeError( @@ -78,7 +79,7 @@ const AsgardeoServerProvider: FC> return <>; } - const sessionId: string = await getSessionId() as string; + const sessionId: string = (await getSessionId()) as string; const _isSignedIn: boolean = await isSignedIn(sessionId); let user: User = {}; @@ -89,8 +90,8 @@ const AsgardeoServerProvider: FC> }; if (_isSignedIn) { - const userResponse = await getUserAction((sessionId)); - const userProfileResponse = await getUserProfileAction((sessionId)); + const userResponse = await getUserAction(sessionId); + const userProfileResponse = await getUserProfileAction(sessionId); user = userResponse.data?.user || {}; userProfile = userProfileResponse.data?.userProfile; diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 37fdcd68..6207bdf5 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,6 +5,7 @@ packages: - '!packages/__legacy__' - 'samples/*' - '!samples/__legacy__/*' + - '!samples/teamspace-nextjs' catalog: '@wso2/eslint-plugin': 'https://gitpkg.now.sh/brionmario/wso2-ui-configs/packages/eslint-plugin?a1fc6eb570653c999828aea9f5027cba06af4391' From 8ba9f1ae433d09b329271aa012c586f513f874f9 Mon Sep 17 00:00:00 2001 From: Brion Mario Date: Mon, 30 Jun 2025 08:14:39 +0530 Subject: [PATCH 15/15] Update metal-colts-lose.md --- .changeset/metal-colts-lose.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/metal-colts-lose.md b/.changeset/metal-colts-lose.md index 7bc6c981..5cd94472 100644 --- a/.changeset/metal-colts-lose.md +++ b/.changeset/metal-colts-lose.md @@ -1,6 +1,6 @@ --- '@asgardeo/nextjs': minor -'@asgardeo/react': minor +'@asgardeo/react': patch --- Stabilize the SDK