From eab91d2950fb163a64232bb74d9a82df895cae09 Mon Sep 17 00:00:00 2001 From: DonOmalVindula Date: Sun, 6 Sep 2026 12:20:23 +0530 Subject: [PATCH] fix(nextjs): update organizations through a server action OrganizationProfile called the Organizations API from the browser without an access token (the token lives in the HttpOnly session cookie), so every save was rejected. - Add AsgardeoNextClient.updateOrganization() and updateOrganizationAction, which attach the token on the server like the other organization calls. - Route the component's saves through the action and surface the failure reason. Co-Authored-By: Claude Fable 5.1 --- .../nextjs-organization-profile-update.md | 5 ++ packages/nextjs/src/AsgardeoNextClient.ts | 41 ++++++++++++ .../OrganizationProfile.tsx | 17 +++-- .../updateOrganizationAction.test.ts | 62 +++++++++++++++++++ .../actions/updateOrganizationAction.ts | 57 +++++++++++++++++ 5 files changed, 176 insertions(+), 6 deletions(-) create mode 100644 .changeset/nextjs-organization-profile-update.md create mode 100644 packages/nextjs/src/server/actions/__tests__/updateOrganizationAction.test.ts create mode 100644 packages/nextjs/src/server/actions/updateOrganizationAction.ts diff --git a/.changeset/nextjs-organization-profile-update.md b/.changeset/nextjs-organization-profile-update.md new file mode 100644 index 000000000..abb72c243 --- /dev/null +++ b/.changeset/nextjs-organization-profile-update.md @@ -0,0 +1,5 @@ +--- +'@asgardeo/nextjs': patch +--- + +Editing an organization through `` works. The component called the Organizations API from the browser without an access token (the token lives in the HttpOnly session cookie), so every save was rejected. Updates now go through a server action (`updateOrganizationAction`, backed by `AsgardeoNextClient.updateOrganization()`) that attaches the token on the server, and a failed save surfaces its reason instead of a bare request error. diff --git a/packages/nextjs/src/AsgardeoNextClient.ts b/packages/nextjs/src/AsgardeoNextClient.ts index 3d4426a3c..5b2097be6 100644 --- a/packages/nextjs/src/AsgardeoNextClient.ts +++ b/packages/nextjs/src/AsgardeoNextClient.ts @@ -39,6 +39,7 @@ import { Storage, TokenExchangeRequestConfig, TokenResponse, + UpdateOrganizationConfig, User, UserProfile, createOrganization, @@ -56,6 +57,7 @@ import { getSchemas, initializeEmbeddedSignInFlow, updateMeProfile, + updateOrganization, } from '@asgardeo/node'; import {AsgardeoNextConfig} from './models/config'; import getClientOrigin from './server/actions/getClientOrigin'; @@ -342,6 +344,45 @@ class AsgardeoNextClient exte } } + /** + * Updates an organization with a set of patch operations, using the access token of the session. + * + * @param organizationId - The ID of the organization to update. + * @param operations - The patch operations to apply. + * @param userId - Optional session ID. + * @returns The updated organization. + */ + async updateOrganization( + organizationId: string, + operations: UpdateOrganizationConfig['operations'], + userId?: string, + ): Promise { + try { + const configData: AuthClientConfig = await this.asgardeo.getConfigData(); + const baseUrl: string = configData?.baseUrl as string; + + const organization: OrganizationDetails = await updateOrganization({ + baseUrl, + headers: { + Authorization: `Bearer ${await this.getAccessToken(userId)}`, + }, + operations, + organizationId, + }); + + return organization; + } catch (error) { + throw new AsgardeoRuntimeError( + `Failed to update the organization ${organizationId}: ${ + error instanceof Error ? error.message : String(error) + }`, + 'AsgardeoNextClient-updateOrganization-RuntimeError-001', + 'nextjs', + `An error occurred while updating the organization with the id: ${organizationId}.`, + ); + } + } + override async getMyOrganizations(options?: any, userId?: string): Promise { try { const configData: AuthClientConfig = await this.asgardeo.getConfigData(); diff --git a/packages/nextjs/src/client/components/presentation/OrganizationProfile/OrganizationProfile.tsx b/packages/nextjs/src/client/components/presentation/OrganizationProfile/OrganizationProfile.tsx index ef441fe95..591ef617a 100644 --- a/packages/nextjs/src/client/components/presentation/OrganizationProfile/OrganizationProfile.tsx +++ b/packages/nextjs/src/client/components/presentation/OrganizationProfile/OrganizationProfile.tsx @@ -18,11 +18,12 @@ 'use client'; -import {OrganizationDetails, updateOrganization, createPatchOperations} from '@asgardeo/node'; +import {OrganizationDetails, createPatchOperations} from '@asgardeo/node'; import {BaseOrganizationProfile, BaseOrganizationProfileProps, useTranslation} from '@asgardeo/react'; import {FC, ReactElement, useEffect, useState} from 'react'; import getOrganizationAction from '../../../../server/actions/getOrganizationAction'; import getSessionId from '../../../../server/actions/getSessionId'; +import updateOrganizationAction from '../../../../server/actions/updateOrganizationAction'; import logger from '../../../../utils/logger'; import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; @@ -188,11 +189,15 @@ const OrganizationProfile: FC = ({ const operations: Array<{operation: 'REPLACE' | 'REMOVE'; path: string; value?: any}> = createPatchOperations(payload); - await updateOrganization({ - baseUrl, - operations, - organizationId, - }); + // The access token only exists on the server (HttpOnly session cookie), so the update goes through + // a server action rather than calling the Organizations API from the browser. + const result: {data: {organization?: OrganizationDetails}; error: string | null; success: boolean} = + await updateOrganizationAction(organizationId, operations, (await getSessionId()) as string); + + if (!result.success) { + throw new Error(result.error ?? 'Failed to update organization'); + } + // Refetch organization data after update await fetchOrganization(); diff --git a/packages/nextjs/src/server/actions/__tests__/updateOrganizationAction.test.ts b/packages/nextjs/src/server/actions/__tests__/updateOrganizationAction.test.ts new file mode 100644 index 000000000..93262275b --- /dev/null +++ b/packages/nextjs/src/server/actions/__tests__/updateOrganizationAction.test.ts @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {beforeEach, describe, expect, it, vi, Mock} from 'vitest'; +import AsgardeoNextClient from '../../../AsgardeoNextClient'; +import updateOrganizationAction from '../updateOrganizationAction'; + +vi.mock('../../../AsgardeoNextClient', () => ({ + default: { + getInstance: vi.fn(), + }, +})); + +describe('updateOrganizationAction', () => { + type ActionResult = Awaited>; + + const client: {updateOrganization: Mock} = {updateOrganization: vi.fn()}; + const operations: Array<{operation: 'REPLACE'; path: string; value: string}> = [ + {operation: 'REPLACE', path: '/name', value: 'Acme Inc.'}, + ]; + + beforeEach(() => { + vi.clearAllMocks(); + (AsgardeoNextClient.getInstance as unknown as Mock).mockReturnValue(client); + }); + + it('returns the updated organization when the update succeeds', async () => { + const organization: Record = {id: 'org-1', name: 'Acme Inc.'}; + + client.updateOrganization.mockResolvedValue(organization); + + const result: ActionResult = await updateOrganizationAction('org-1', operations, 'session-1'); + + expect(client.updateOrganization).toHaveBeenCalledWith('org-1', operations, 'session-1'); + expect(result).toEqual({data: {organization}, error: null, success: true}); + }); + + it('reports the failure reason instead of throwing when the update fails', async () => { + client.updateOrganization.mockRejectedValue(new Error('Failed to update the organization org-1: forbidden')); + + const result: ActionResult = await updateOrganizationAction('org-1', operations); + + expect(result.success).toBe(false); + expect(result.error).toBe('Failed to update the organization org-1: forbidden'); + expect(result.data.organization).toBeUndefined(); + }); +}); diff --git a/packages/nextjs/src/server/actions/updateOrganizationAction.ts b/packages/nextjs/src/server/actions/updateOrganizationAction.ts new file mode 100644 index 000000000..9ad0d11f1 --- /dev/null +++ b/packages/nextjs/src/server/actions/updateOrganizationAction.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +'use server'; + +import {OrganizationDetails, UpdateOrganizationConfig} from '@asgardeo/node'; +import AsgardeoNextClient from '../../AsgardeoNextClient'; + +/** + * Server action to update an organization with a set of patch operations. + * + * The access token stays on the server: it is read from the session cookie and attached to the request, + * which is why the browser cannot call the Organizations API directly. + * + * @param organizationId - The ID of the organization to update. + * @param operations - The patch operations to apply (see `createPatchOperations`). + * @param sessionId - Optional session ID; resolved from the session cookie when omitted. + */ +const updateOrganizationAction = async ( + organizationId: string, + operations: UpdateOrganizationConfig['operations'], + sessionId?: string, +): Promise<{ + data: {organization?: OrganizationDetails}; + error: string | null; + success: boolean; +}> => { + try { + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); + const organization: OrganizationDetails = await client.updateOrganization(organizationId, operations, sessionId); + + return {data: {organization}, error: null, success: true}; + } catch (error) { + return { + data: {}, + error: error instanceof Error ? error.message : String(error), + success: false, + }; + } +}; + +export default updateOrganizationAction;