Skip to content

Commit 267668f

Browse files
committed
chore: migrate OAuth services admin DDP methods to REST (#40737)
1 parent 3aa46a9 commit 267668f

8 files changed

Lines changed: 162 additions & 63 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@rocket.chat/meteor': patch
3+
---
4+
5+
Migrated the Admin → OAuth services group page from `useMethod` (DDP) to `useEndpoint` (REST):
6+
7+
- `addOAuthService` → existing `POST /v1/settings.addCustomOAuth`
8+
- `removeOAuthService` → new `POST /v1/settings.removeCustomOAuth`
9+
- `refreshOAuthService` → new `POST /v1/settings.refreshOAuthServices`
10+
11+
DDP methods stay registered with deprecation logs pointing at the new routes until 9.0.0.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@rocket.chat/rest-typings': minor
3+
'@rocket.chat/meteor': minor
4+
---
5+
6+
Added two new REST endpoints completing the Custom OAuth admin surface:
7+
8+
- `POST /v1/settings.removeCustomOAuth` body `{ name }` → removes all `Accounts_OAuth_Custom-<Name>-*` setting documents (replaces the deprecated `removeOAuthService` DDP method).
9+
- `POST /v1/settings.refreshOAuthServices` (no body) → re-reads ServiceConfiguration entries from settings (replaces the deprecated `refreshOAuthService` DDP method).
10+
11+
Both endpoints reuse the `add-oauth-service` permission and `twoFactorRequired` gates that the DDP methods already enforced. `addOAuthService` was already covered by the existing `POST /v1/settings.addCustomOAuth` — its DDP method now also logs a deprecation. The three legacy DDP methods remain registered until 9.0.0.

apps/meteor/app/api/server/v1/settings.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ import {
1616
isSettingsPublicWithPaginationProps,
1717
isSettingsGetParams,
1818
isSettingsBulkProps,
19+
validateBadRequestErrorResponse,
1920
validateForbiddenErrorResponse,
2021
validateUnauthorizedErrorResponse,
21-
validateBadRequestErrorResponse,
2222
} from '@rocket.chat/rest-typings';
2323
import { Meteor } from 'meteor/meteor';
2424
import type { FindOptions } from 'mongodb';
@@ -31,6 +31,8 @@ import { saveSettingsBulk } from '../../../lib/server/functions/saveSettingsBulk
3131
import { checkSettingValueBounds } from '../../../lib/server/lib/checkSettingValueBonds';
3232
import { notifyOnSettingChanged, notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener';
3333
import { addOAuthServiceMethod } from '../../../lib/server/methods/addOAuthService';
34+
import { refreshOAuthServiceMethod } from '../../../lib/server/methods/refreshOAuthService';
35+
import { removeOAuthServiceMethod } from '../../../lib/server/methods/removeOAuthService';
3436
import { SettingsEvents, settings } from '../../../settings/server';
3537
import { setValue } from '../../../settings/server/raw';
3638
import { API } from '../api';
@@ -243,6 +245,59 @@ API.v1.post(
243245
},
244246
);
245247

248+
API.v1.post(
249+
'settings.removeCustomOAuth',
250+
{
251+
authRequired: true,
252+
twoFactorRequired: true,
253+
body: addCustomOAuthBodySchema,
254+
response: {
255+
200: ajv.compile<void>({
256+
type: 'object',
257+
properties: { success: { type: 'boolean', enum: [true] } },
258+
required: ['success'],
259+
additionalProperties: false,
260+
}),
261+
400: validateBadRequestErrorResponse,
262+
401: validateUnauthorizedErrorResponse,
263+
403: validateForbiddenErrorResponse,
264+
},
265+
},
266+
async function action() {
267+
const { name } = this.bodyParams;
268+
if (!name?.trim()) {
269+
throw new Meteor.Error('error-name-param-not-provided', 'The parameter "name" is required');
270+
}
271+
272+
await removeOAuthServiceMethod(this.userId, name);
273+
274+
return API.v1.success();
275+
},
276+
);
277+
278+
API.v1.post(
279+
'settings.refreshOAuthServices',
280+
{
281+
authRequired: true,
282+
twoFactorRequired: true,
283+
response: {
284+
200: ajv.compile<void>({
285+
type: 'object',
286+
properties: { success: { type: 'boolean', enum: [true] } },
287+
required: ['success'],
288+
additionalProperties: false,
289+
}),
290+
400: validateBadRequestErrorResponse,
291+
401: validateUnauthorizedErrorResponse,
292+
403: validateForbiddenErrorResponse,
293+
},
294+
},
295+
async function action() {
296+
await refreshOAuthServiceMethod(this.userId);
297+
return API.v1.success();
298+
},
299+
);
300+
246301
API.v1.get(
247302
'settings',
248303
{

apps/meteor/app/lib/server/methods/addOAuthService.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Meteor } from 'meteor/meteor';
44

55
import { addOAuthService } from '../../../../server/lib/oauth/addOAuthService';
66
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
7+
import { methodDeprecationLogger } from '../lib/deprecationWarningLogger';
78

89
declare module '@rocket.chat/ddp-client' {
910
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -25,6 +26,7 @@ export const addOAuthServiceMethod = async (userId: string, name: string): Promi
2526

2627
Meteor.methods<ServerMethods>({
2728
async addOAuthService(name) {
29+
methodDeprecationLogger.method('addOAuthService', '9.0.0', '/v1/settings.addCustomOAuth');
2830
check(name, String);
2931

3032
const userId = Meteor.userId();

apps/meteor/app/lib/server/methods/refreshOAuthService.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Meteor } from 'meteor/meteor';
33

44
import { refreshLoginServices } from '../../../../server/lib/refreshLoginServices';
55
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
6+
import { methodDeprecationLogger } from '../lib/deprecationWarningLogger';
67

78
declare module '@rocket.chat/ddp-client' {
89
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -11,8 +12,21 @@ declare module '@rocket.chat/ddp-client' {
1112
}
1213
}
1314

15+
export const refreshOAuthServiceMethod = async (userId: string): Promise<void> => {
16+
if ((await hasPermissionAsync(userId, 'add-oauth-service')) !== true) {
17+
throw new Meteor.Error('error-action-not-allowed', 'Refresh OAuth Services is not allowed', {
18+
method: 'refreshOAuthService',
19+
action: 'Refreshing_OAuth_Services',
20+
});
21+
}
22+
23+
await refreshLoginServices();
24+
};
25+
1426
Meteor.methods<ServerMethods>({
1527
async refreshOAuthService() {
28+
methodDeprecationLogger.method('refreshOAuthService', '9.0.0', '/v1/settings.refreshOAuthServices');
29+
1630
const userId = Meteor.userId();
1731

1832
if (!userId) {
@@ -21,13 +35,6 @@ Meteor.methods<ServerMethods>({
2135
});
2236
}
2337

24-
if ((await hasPermissionAsync(userId, 'add-oauth-service')) !== true) {
25-
throw new Meteor.Error('error-action-not-allowed', 'Refresh OAuth Services is not allowed', {
26-
method: 'refreshOAuthService',
27-
action: 'Refreshing_OAuth_Services',
28-
});
29-
}
30-
31-
await refreshLoginServices();
38+
await refreshOAuthServiceMethod(userId);
3239
},
3340
});

apps/meteor/app/lib/server/methods/removeOAuthService.ts

Lines changed: 52 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { check } from 'meteor/check';
55
import { Meteor } from 'meteor/meteor';
66

77
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
8+
import { methodDeprecationLogger } from '../lib/deprecationWarningLogger';
89
import { notifyOnSettingChangedById } from '../lib/notifyListener';
910

1011
declare module '@rocket.chat/ddp-client' {
@@ -14,8 +15,58 @@ declare module '@rocket.chat/ddp-client' {
1415
}
1516
}
1617

18+
export const removeOAuthServiceMethod = async (userId: string, name: string): Promise<void> => {
19+
if ((await hasPermissionAsync(userId, 'add-oauth-service')) !== true) {
20+
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'removeOAuthService' });
21+
}
22+
23+
const normalized = capitalize(name.toLowerCase().replace(/[^a-z0-9_]/g, ''));
24+
25+
const settingsIds = [
26+
`Accounts_OAuth_Custom-${normalized}`,
27+
`Accounts_OAuth_Custom-${normalized}-url`,
28+
`Accounts_OAuth_Custom-${normalized}-token_path`,
29+
`Accounts_OAuth_Custom-${normalized}-identity_path`,
30+
`Accounts_OAuth_Custom-${normalized}-authorize_path`,
31+
`Accounts_OAuth_Custom-${normalized}-scope`,
32+
`Accounts_OAuth_Custom-${normalized}-access_token_param`,
33+
`Accounts_OAuth_Custom-${normalized}-token_sent_via`,
34+
`Accounts_OAuth_Custom-${normalized}-identity_token_sent_via`,
35+
`Accounts_OAuth_Custom-${normalized}-id`,
36+
`Accounts_OAuth_Custom-${normalized}-secret`,
37+
`Accounts_OAuth_Custom-${normalized}-button_label_text`,
38+
`Accounts_OAuth_Custom-${normalized}-button_label_color`,
39+
`Accounts_OAuth_Custom-${normalized}-button_color`,
40+
`Accounts_OAuth_Custom-${normalized}-login_style`,
41+
`Accounts_OAuth_Custom-${normalized}-key_field`,
42+
`Accounts_OAuth_Custom-${normalized}-username_field`,
43+
`Accounts_OAuth_Custom-${normalized}-email_field`,
44+
`Accounts_OAuth_Custom-${normalized}-name_field`,
45+
`Accounts_OAuth_Custom-${normalized}-avatar_field`,
46+
`Accounts_OAuth_Custom-${normalized}-roles_claim`,
47+
`Accounts_OAuth_Custom-${normalized}-merge_roles`,
48+
`Accounts_OAuth_Custom-${normalized}-roles_to_sync`,
49+
`Accounts_OAuth_Custom-${normalized}-merge_users`,
50+
`Accounts_OAuth_Custom-${normalized}-show_button`,
51+
`Accounts_OAuth_Custom-${normalized}-groups_claim`,
52+
`Accounts_OAuth_Custom-${normalized}-channels_admin`,
53+
`Accounts_OAuth_Custom-${normalized}-map_channels`,
54+
`Accounts_OAuth_Custom-${normalized}-groups_channel_map`,
55+
`Accounts_OAuth_Custom-${normalized}-merge_users_distinct_services`,
56+
];
57+
58+
const promises = settingsIds.map((id) => Settings.removeById(id));
59+
60+
(await Promise.all(promises)).forEach((value, index) => {
61+
if (value?.deletedCount) {
62+
void notifyOnSettingChangedById(settingsIds[index], 'removed');
63+
}
64+
});
65+
};
66+
1767
Meteor.methods<ServerMethods>({
1868
async removeOAuthService(name) {
69+
methodDeprecationLogger.method('removeOAuthService', '9.0.0', '/v1/settings.removeCustomOAuth');
1970
check(name, String);
2071

2172
const userId = Meteor.userId();
@@ -26,52 +77,6 @@ Meteor.methods<ServerMethods>({
2677
});
2778
}
2879

29-
if ((await hasPermissionAsync(userId, 'add-oauth-service')) !== true) {
30-
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'removeOAuthService' });
31-
}
32-
33-
name = name.toLowerCase().replace(/[^a-z0-9_]/g, '');
34-
name = capitalize(name);
35-
36-
const settingsIds = [
37-
`Accounts_OAuth_Custom-${name}`,
38-
`Accounts_OAuth_Custom-${name}-url`,
39-
`Accounts_OAuth_Custom-${name}-token_path`,
40-
`Accounts_OAuth_Custom-${name}-identity_path`,
41-
`Accounts_OAuth_Custom-${name}-authorize_path`,
42-
`Accounts_OAuth_Custom-${name}-scope`,
43-
`Accounts_OAuth_Custom-${name}-access_token_param`,
44-
`Accounts_OAuth_Custom-${name}-token_sent_via`,
45-
`Accounts_OAuth_Custom-${name}-identity_token_sent_via`,
46-
`Accounts_OAuth_Custom-${name}-id`,
47-
`Accounts_OAuth_Custom-${name}-secret`,
48-
`Accounts_OAuth_Custom-${name}-button_label_text`,
49-
`Accounts_OAuth_Custom-${name}-button_label_color`,
50-
`Accounts_OAuth_Custom-${name}-button_color`,
51-
`Accounts_OAuth_Custom-${name}-login_style`,
52-
`Accounts_OAuth_Custom-${name}-key_field`,
53-
`Accounts_OAuth_Custom-${name}-username_field`,
54-
`Accounts_OAuth_Custom-${name}-email_field`,
55-
`Accounts_OAuth_Custom-${name}-name_field`,
56-
`Accounts_OAuth_Custom-${name}-avatar_field`,
57-
`Accounts_OAuth_Custom-${name}-roles_claim`,
58-
`Accounts_OAuth_Custom-${name}-merge_roles`,
59-
`Accounts_OAuth_Custom-${name}-roles_to_sync`,
60-
`Accounts_OAuth_Custom-${name}-merge_users`,
61-
`Accounts_OAuth_Custom-${name}-show_button`,
62-
`Accounts_OAuth_Custom-${name}-groups_claim`,
63-
`Accounts_OAuth_Custom-${name}-channels_admin`,
64-
`Accounts_OAuth_Custom-${name}-map_channels`,
65-
`Accounts_OAuth_Custom-${name}-groups_channel_map`,
66-
`Accounts_OAuth_Custom-${name}-merge_users_distinct_services`,
67-
];
68-
69-
const promises = settingsIds.map((id) => Settings.removeById(id));
70-
71-
(await Promise.all(promises)).forEach((value, index) => {
72-
if (value?.deletedCount) {
73-
void notifyOnSettingChangedById(settingsIds[index], 'removed');
74-
}
75-
});
80+
await removeOAuthServiceMethod(userId, name);
7681
},
7782
});

apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/OAuthGroupPage.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { ISetting } from '@rocket.chat/core-typings';
22
import { Button } from '@rocket.chat/fuselage';
33
import { capitalize } from '@rocket.chat/string-helpers';
44
import { GenericModal } from '@rocket.chat/ui-client';
5-
import { useToastMessageDispatch, useAbsoluteUrl, useMethod, useTranslation, useSetModal } from '@rocket.chat/ui-contexts';
5+
import { useToastMessageDispatch, useAbsoluteUrl, useEndpoint, useTranslation, useSetModal } from '@rocket.chat/ui-contexts';
66
import DOMPurify from 'dompurify';
77
import type { ReactElement } from 'react';
88
import { memo, useEffect, useState } from 'react';
@@ -34,15 +34,15 @@ function OAuthGroupPage({ _id, onClickBack, ...group }: OAuthGroupPageProps): Re
3434
};
3535

3636
const dispatchToastMessage = useToastMessageDispatch();
37-
const refreshOAuthService = useMethod('refreshOAuthService');
38-
const addOAuthService = useMethod('addOAuthService');
39-
const removeOAuthService = useMethod('removeOAuthService');
37+
const refreshOAuthService = useEndpoint('POST', '/v1/settings.refreshOAuthServices');
38+
const addOAuthService = useEndpoint('POST', '/v1/settings.addCustomOAuth');
39+
const removeOAuthService = useEndpoint('POST', '/v1/settings.removeCustomOAuth');
4040
const setModal = useSetModal();
4141

4242
const handleRefreshOAuthServicesButtonClick = async (): Promise<void> => {
4343
dispatchToastMessage({ type: 'info', message: t('Refreshing') });
4444
try {
45-
await refreshOAuthService();
45+
await refreshOAuthService(undefined);
4646
dispatchToastMessage({ type: 'success', message: t('Done') });
4747
} catch (error) {
4848
dispatchToastMessage({ type: 'error', message: error });
@@ -52,7 +52,7 @@ function OAuthGroupPage({ _id, onClickBack, ...group }: OAuthGroupPageProps): Re
5252
const handleAddCustomOAuthButtonClick = (): void => {
5353
const onConfirm = async (text: string): Promise<void> => {
5454
try {
55-
await addOAuthService(text);
55+
await addOAuthService({ name: text });
5656
dispatchToastMessage({ type: 'success', message: t('Custom_OAuth_has_been_added') });
5757
} catch (error) {
5858
dispatchToastMessage({ type: 'error', message: error });
@@ -72,7 +72,7 @@ function OAuthGroupPage({ _id, onClickBack, ...group }: OAuthGroupPageProps): Re
7272
(): void => {
7373
const handleConfirm = async (): Promise<void> => {
7474
try {
75-
await removeOAuthService(id);
75+
await removeOAuthService({ name: id });
7676
dispatchToastMessage({ type: 'success', message: t('Custom_OAuth_has_been_removed') });
7777
setSettingSections(settingSections.filter((section) => section !== `Custom OAuth: ${capitalize(id)}`));
7878
} catch (error) {

packages/rest-typings/src/v1/settings.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,14 @@ export type SettingsEndpoints = {
131131
POST: (params: { name: string }) => void;
132132
};
133133

134+
'/v1/settings.removeCustomOAuth': {
135+
POST: (params: { name: string }) => void;
136+
};
137+
138+
'/v1/settings.refreshOAuthServices': {
139+
POST: () => void;
140+
};
141+
134142
'/v1/settings': {
135143
GET: (params: SettingsGetParams) => {
136144
settings: ISetting[];

0 commit comments

Comments
 (0)