Skip to content

Commit eab91d2

Browse files
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 <noreply@anthropic.com>
1 parent 409ebae commit eab91d2

5 files changed

Lines changed: 176 additions & 6 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@asgardeo/nextjs': patch
3+
---
4+
5+
Editing an organization through `<OrganizationProfile />` 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.

‎packages/nextjs/src/AsgardeoNextClient.ts‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
Storage,
4040
TokenExchangeRequestConfig,
4141
TokenResponse,
42+
UpdateOrganizationConfig,
4243
User,
4344
UserProfile,
4445
createOrganization,
@@ -56,6 +57,7 @@ import {
5657
getSchemas,
5758
initializeEmbeddedSignInFlow,
5859
updateMeProfile,
60+
updateOrganization,
5961
} from '@asgardeo/node';
6062
import {AsgardeoNextConfig} from './models/config';
6163
import getClientOrigin from './server/actions/getClientOrigin';
@@ -342,6 +344,45 @@ class AsgardeoNextClient<T extends AsgardeoNextConfig = AsgardeoNextConfig> exte
342344
}
343345
}
344346

347+
/**
348+
* Updates an organization with a set of patch operations, using the access token of the session.
349+
*
350+
* @param organizationId - The ID of the organization to update.
351+
* @param operations - The patch operations to apply.
352+
* @param userId - Optional session ID.
353+
* @returns The updated organization.
354+
*/
355+
async updateOrganization(
356+
organizationId: string,
357+
operations: UpdateOrganizationConfig['operations'],
358+
userId?: string,
359+
): Promise<OrganizationDetails> {
360+
try {
361+
const configData: AuthClientConfig<T> = await this.asgardeo.getConfigData();
362+
const baseUrl: string = configData?.baseUrl as string;
363+
364+
const organization: OrganizationDetails = await updateOrganization({
365+
baseUrl,
366+
headers: {
367+
Authorization: `Bearer ${await this.getAccessToken(userId)}`,
368+
},
369+
operations,
370+
organizationId,
371+
});
372+
373+
return organization;
374+
} catch (error) {
375+
throw new AsgardeoRuntimeError(
376+
`Failed to update the organization ${organizationId}: ${
377+
error instanceof Error ? error.message : String(error)
378+
}`,
379+
'AsgardeoNextClient-updateOrganization-RuntimeError-001',
380+
'nextjs',
381+
`An error occurred while updating the organization with the id: ${organizationId}.`,
382+
);
383+
}
384+
}
385+
345386
override async getMyOrganizations(options?: any, userId?: string): Promise<Organization[]> {
346387
try {
347388
const configData: AuthClientConfig<T> = await this.asgardeo.getConfigData();

‎packages/nextjs/src/client/components/presentation/OrganizationProfile/OrganizationProfile.tsx‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@
1818

1919
'use client';
2020

21-
import {OrganizationDetails, updateOrganization, createPatchOperations} from '@asgardeo/node';
21+
import {OrganizationDetails, createPatchOperations} from '@asgardeo/node';
2222
import {BaseOrganizationProfile, BaseOrganizationProfileProps, useTranslation} from '@asgardeo/react';
2323
import {FC, ReactElement, useEffect, useState} from 'react';
2424
import getOrganizationAction from '../../../../server/actions/getOrganizationAction';
2525
import getSessionId from '../../../../server/actions/getSessionId';
26+
import updateOrganizationAction from '../../../../server/actions/updateOrganizationAction';
2627
import logger from '../../../../utils/logger';
2728
import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo';
2829

@@ -188,11 +189,15 @@ const OrganizationProfile: FC<OrganizationProfileProps> = ({
188189
const operations: Array<{operation: 'REPLACE' | 'REMOVE'; path: string; value?: any}> =
189190
createPatchOperations(payload);
190191

191-
await updateOrganization({
192-
baseUrl,
193-
operations,
194-
organizationId,
195-
});
192+
// The access token only exists on the server (HttpOnly session cookie), so the update goes through
193+
// a server action rather than calling the Organizations API from the browser.
194+
const result: {data: {organization?: OrganizationDetails}; error: string | null; success: boolean} =
195+
await updateOrganizationAction(organizationId, operations, (await getSessionId()) as string);
196+
197+
if (!result.success) {
198+
throw new Error(result.error ?? 'Failed to update organization');
199+
}
200+
196201
// Refetch organization data after update
197202
await fetchOrganization();
198203

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
3+
*
4+
* WSO2 LLC. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
19+
import {beforeEach, describe, expect, it, vi, Mock} from 'vitest';
20+
import AsgardeoNextClient from '../../../AsgardeoNextClient';
21+
import updateOrganizationAction from '../updateOrganizationAction';
22+
23+
vi.mock('../../../AsgardeoNextClient', () => ({
24+
default: {
25+
getInstance: vi.fn(),
26+
},
27+
}));
28+
29+
describe('updateOrganizationAction', () => {
30+
type ActionResult = Awaited<ReturnType<typeof updateOrganizationAction>>;
31+
32+
const client: {updateOrganization: Mock} = {updateOrganization: vi.fn()};
33+
const operations: Array<{operation: 'REPLACE'; path: string; value: string}> = [
34+
{operation: 'REPLACE', path: '/name', value: 'Acme Inc.'},
35+
];
36+
37+
beforeEach(() => {
38+
vi.clearAllMocks();
39+
(AsgardeoNextClient.getInstance as unknown as Mock).mockReturnValue(client);
40+
});
41+
42+
it('returns the updated organization when the update succeeds', async () => {
43+
const organization: Record<string, unknown> = {id: 'org-1', name: 'Acme Inc.'};
44+
45+
client.updateOrganization.mockResolvedValue(organization);
46+
47+
const result: ActionResult = await updateOrganizationAction('org-1', operations, 'session-1');
48+
49+
expect(client.updateOrganization).toHaveBeenCalledWith('org-1', operations, 'session-1');
50+
expect(result).toEqual({data: {organization}, error: null, success: true});
51+
});
52+
53+
it('reports the failure reason instead of throwing when the update fails', async () => {
54+
client.updateOrganization.mockRejectedValue(new Error('Failed to update the organization org-1: forbidden'));
55+
56+
const result: ActionResult = await updateOrganizationAction('org-1', operations);
57+
58+
expect(result.success).toBe(false);
59+
expect(result.error).toBe('Failed to update the organization org-1: forbidden');
60+
expect(result.data.organization).toBeUndefined();
61+
});
62+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
3+
*
4+
* WSO2 LLC. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
19+
'use server';
20+
21+
import {OrganizationDetails, UpdateOrganizationConfig} from '@asgardeo/node';
22+
import AsgardeoNextClient from '../../AsgardeoNextClient';
23+
24+
/**
25+
* Server action to update an organization with a set of patch operations.
26+
*
27+
* The access token stays on the server: it is read from the session cookie and attached to the request,
28+
* which is why the browser cannot call the Organizations API directly.
29+
*
30+
* @param organizationId - The ID of the organization to update.
31+
* @param operations - The patch operations to apply (see `createPatchOperations`).
32+
* @param sessionId - Optional session ID; resolved from the session cookie when omitted.
33+
*/
34+
const updateOrganizationAction = async (
35+
organizationId: string,
36+
operations: UpdateOrganizationConfig['operations'],
37+
sessionId?: string,
38+
): Promise<{
39+
data: {organization?: OrganizationDetails};
40+
error: string | null;
41+
success: boolean;
42+
}> => {
43+
try {
44+
const client: AsgardeoNextClient = AsgardeoNextClient.getInstance();
45+
const organization: OrganizationDetails = await client.updateOrganization(organizationId, operations, sessionId);
46+
47+
return {data: {organization}, error: null, success: true};
48+
} catch (error) {
49+
return {
50+
data: {},
51+
error: error instanceof Error ? error.message : String(error),
52+
success: false,
53+
};
54+
}
55+
};
56+
57+
export default updateOrganizationAction;

0 commit comments

Comments
 (0)