Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/ddp-migrate-batch7-oauth-caller.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@rocket.chat/meteor': patch
---

Migrated the Admin → OAuth services group page from `useMethod` (DDP) to `useEndpoint` (REST):

- `addOAuthService` → existing `POST /v1/settings.addCustomOAuth`
- `removeOAuthService` → new `POST /v1/settings.removeCustomOAuth`
- `refreshOAuthService` → new `POST /v1/settings.refreshOAuthServices`

DDP methods stay registered with deprecation logs pointing at the new routes until 9.0.0.
11 changes: 11 additions & 0 deletions .changeset/rest-settings-oauth-admin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@rocket.chat/rest-typings': minor
'@rocket.chat/meteor': minor
---

Added two new REST endpoints completing the Custom OAuth admin surface:

- `POST /v1/settings.removeCustomOAuth` body `{ name }` → removes all `Accounts_OAuth_Custom-<Name>-*` setting documents (replaces the deprecated `removeOAuthService` DDP method).
- `POST /v1/settings.refreshOAuthServices` (no body) → re-reads ServiceConfiguration entries from settings (replaces the deprecated `refreshOAuthService` DDP method).

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.
56 changes: 56 additions & 0 deletions apps/meteor/app/api/server/v1/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
isSettingsUpdatePropsColor,
isSettingsPublicWithPaginationProps,
isSettingsGetParams,
validateBadRequestErrorResponse,
validateForbiddenErrorResponse,
validateUnauthorizedErrorResponse,
} from '@rocket.chat/rest-typings';
Expand All @@ -28,6 +29,8 @@ import { disableCustomScripts } from '../../../lib/server/functions/disableCusto
import { checkSettingValueBounds } from '../../../lib/server/lib/checkSettingValueBonds';
import { notifyOnSettingChanged, notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener';
import { addOAuthServiceMethod } from '../../../lib/server/methods/addOAuthService';
import { refreshOAuthServiceMethod } from '../../../lib/server/methods/refreshOAuthService';
import { removeOAuthServiceMethod } from '../../../lib/server/methods/removeOAuthService';
import { SettingsEvents, settings } from '../../../settings/server';
import { setValue } from '../../../settings/server/raw';
import { API } from '../api';
Expand Down Expand Up @@ -240,6 +243,59 @@ API.v1.post(
},
);

API.v1.post(
'settings.removeCustomOAuth',
{
authRequired: true,
twoFactorRequired: true,
body: addCustomOAuthBodySchema,
response: {
200: ajv.compile<void>({
type: 'object',
properties: { success: { type: 'boolean', enum: [true] } },
required: ['success'],
additionalProperties: false,
}),
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
},
},
async function action() {
const { name } = this.bodyParams;
if (!name?.trim()) {
throw new Meteor.Error('error-name-param-not-provided', 'The parameter "name" is required');
}

await removeOAuthServiceMethod(this.userId, name);

return API.v1.success();
},
);

API.v1.post(
'settings.refreshOAuthServices',
{
authRequired: true,
twoFactorRequired: true,
response: {
200: ajv.compile<void>({
type: 'object',
properties: { success: { type: 'boolean', enum: [true] } },
required: ['success'],
additionalProperties: false,
}),
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
},
},
async function action() {
await refreshOAuthServiceMethod(this.userId);
return API.v1.success();
},
);

API.v1.get(
'settings',
{
Expand Down
2 changes: 2 additions & 0 deletions apps/meteor/app/lib/server/methods/addOAuthService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Meteor } from 'meteor/meteor';

import { addOAuthService } from '../../../../server/lib/oauth/addOAuthService';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { methodDeprecationLogger } from '../lib/deprecationWarningLogger';

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

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

const userId = Meteor.userId();
Expand Down
23 changes: 15 additions & 8 deletions apps/meteor/app/lib/server/methods/refreshOAuthService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Meteor } from 'meteor/meteor';

import { refreshLoginServices } from '../../../../server/lib/refreshLoginServices';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { methodDeprecationLogger } from '../lib/deprecationWarningLogger';

declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
Expand All @@ -11,8 +12,21 @@ declare module '@rocket.chat/ddp-client' {
}
}

export const refreshOAuthServiceMethod = async (userId: string): Promise<void> => {
if ((await hasPermissionAsync(userId, 'add-oauth-service')) !== true) {
throw new Meteor.Error('error-action-not-allowed', 'Refresh OAuth Services is not allowed', {
method: 'refreshOAuthService',
action: 'Refreshing_OAuth_Services',
});
}

await refreshLoginServices();
};

Meteor.methods<ServerMethods>({
async refreshOAuthService() {
methodDeprecationLogger.method('refreshOAuthService', '9.0.0', '/v1/settings.refreshOAuthServices');

const userId = Meteor.userId();

if (!userId) {
Expand All @@ -21,13 +35,6 @@ Meteor.methods<ServerMethods>({
});
}

if ((await hasPermissionAsync(userId, 'add-oauth-service')) !== true) {
throw new Meteor.Error('error-action-not-allowed', 'Refresh OAuth Services is not allowed', {
method: 'refreshOAuthService',
action: 'Refreshing_OAuth_Services',
});
}

await refreshLoginServices();
await refreshOAuthServiceMethod(userId);
},
});
99 changes: 52 additions & 47 deletions apps/meteor/app/lib/server/methods/removeOAuthService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';

import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { methodDeprecationLogger } from '../lib/deprecationWarningLogger';
import { notifyOnSettingChangedById } from '../lib/notifyListener';

declare module '@rocket.chat/ddp-client' {
Expand All @@ -14,8 +15,58 @@ declare module '@rocket.chat/ddp-client' {
}
}

export const removeOAuthServiceMethod = async (userId: string, name: string): Promise<void> => {
if ((await hasPermissionAsync(userId, 'add-oauth-service')) !== true) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'removeOAuthService' });
}

const normalized = capitalize(name.toLowerCase().replace(/[^a-z0-9_]/g, ''));

const settingsIds = [
`Accounts_OAuth_Custom-${normalized}`,
`Accounts_OAuth_Custom-${normalized}-url`,
`Accounts_OAuth_Custom-${normalized}-token_path`,
`Accounts_OAuth_Custom-${normalized}-identity_path`,
`Accounts_OAuth_Custom-${normalized}-authorize_path`,
`Accounts_OAuth_Custom-${normalized}-scope`,
`Accounts_OAuth_Custom-${normalized}-access_token_param`,
`Accounts_OAuth_Custom-${normalized}-token_sent_via`,
`Accounts_OAuth_Custom-${normalized}-identity_token_sent_via`,
`Accounts_OAuth_Custom-${normalized}-id`,
`Accounts_OAuth_Custom-${normalized}-secret`,
`Accounts_OAuth_Custom-${normalized}-button_label_text`,
`Accounts_OAuth_Custom-${normalized}-button_label_color`,
`Accounts_OAuth_Custom-${normalized}-button_color`,
`Accounts_OAuth_Custom-${normalized}-login_style`,
`Accounts_OAuth_Custom-${normalized}-key_field`,
`Accounts_OAuth_Custom-${normalized}-username_field`,
`Accounts_OAuth_Custom-${normalized}-email_field`,
`Accounts_OAuth_Custom-${normalized}-name_field`,
`Accounts_OAuth_Custom-${normalized}-avatar_field`,
`Accounts_OAuth_Custom-${normalized}-roles_claim`,
`Accounts_OAuth_Custom-${normalized}-merge_roles`,
`Accounts_OAuth_Custom-${normalized}-roles_to_sync`,
`Accounts_OAuth_Custom-${normalized}-merge_users`,
`Accounts_OAuth_Custom-${normalized}-show_button`,
`Accounts_OAuth_Custom-${normalized}-groups_claim`,
`Accounts_OAuth_Custom-${normalized}-channels_admin`,
`Accounts_OAuth_Custom-${normalized}-map_channels`,
`Accounts_OAuth_Custom-${normalized}-groups_channel_map`,
`Accounts_OAuth_Custom-${normalized}-merge_users_distinct_services`,
];

const promises = settingsIds.map((id) => Settings.removeById(id));

(await Promise.all(promises)).forEach((value, index) => {
if (value?.deletedCount) {
void notifyOnSettingChangedById(settingsIds[index], 'removed');
}
});
};

Meteor.methods<ServerMethods>({
async removeOAuthService(name) {
methodDeprecationLogger.method('removeOAuthService', '9.0.0', '/v1/settings.removeCustomOAuth');
check(name, String);

const userId = Meteor.userId();
Expand All @@ -26,52 +77,6 @@ Meteor.methods<ServerMethods>({
});
}

if ((await hasPermissionAsync(userId, 'add-oauth-service')) !== true) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'removeOAuthService' });
}

name = name.toLowerCase().replace(/[^a-z0-9_]/g, '');
name = capitalize(name);

const settingsIds = [
`Accounts_OAuth_Custom-${name}`,
`Accounts_OAuth_Custom-${name}-url`,
`Accounts_OAuth_Custom-${name}-token_path`,
`Accounts_OAuth_Custom-${name}-identity_path`,
`Accounts_OAuth_Custom-${name}-authorize_path`,
`Accounts_OAuth_Custom-${name}-scope`,
`Accounts_OAuth_Custom-${name}-access_token_param`,
`Accounts_OAuth_Custom-${name}-token_sent_via`,
`Accounts_OAuth_Custom-${name}-identity_token_sent_via`,
`Accounts_OAuth_Custom-${name}-id`,
`Accounts_OAuth_Custom-${name}-secret`,
`Accounts_OAuth_Custom-${name}-button_label_text`,
`Accounts_OAuth_Custom-${name}-button_label_color`,
`Accounts_OAuth_Custom-${name}-button_color`,
`Accounts_OAuth_Custom-${name}-login_style`,
`Accounts_OAuth_Custom-${name}-key_field`,
`Accounts_OAuth_Custom-${name}-username_field`,
`Accounts_OAuth_Custom-${name}-email_field`,
`Accounts_OAuth_Custom-${name}-name_field`,
`Accounts_OAuth_Custom-${name}-avatar_field`,
`Accounts_OAuth_Custom-${name}-roles_claim`,
`Accounts_OAuth_Custom-${name}-merge_roles`,
`Accounts_OAuth_Custom-${name}-roles_to_sync`,
`Accounts_OAuth_Custom-${name}-merge_users`,
`Accounts_OAuth_Custom-${name}-show_button`,
`Accounts_OAuth_Custom-${name}-groups_claim`,
`Accounts_OAuth_Custom-${name}-channels_admin`,
`Accounts_OAuth_Custom-${name}-map_channels`,
`Accounts_OAuth_Custom-${name}-groups_channel_map`,
`Accounts_OAuth_Custom-${name}-merge_users_distinct_services`,
];

const promises = settingsIds.map((id) => Settings.removeById(id));

(await Promise.all(promises)).forEach((value, index) => {
if (value?.deletedCount) {
void notifyOnSettingChangedById(settingsIds[index], 'removed');
}
});
await removeOAuthServiceMethod(userId, name);
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ISetting } from '@rocket.chat/core-typings';
import { Button } from '@rocket.chat/fuselage';
import { capitalize } from '@rocket.chat/string-helpers';
import { GenericModal } from '@rocket.chat/ui-client';
import { useToastMessageDispatch, useAbsoluteUrl, useMethod, useTranslation, useSetModal } from '@rocket.chat/ui-contexts';
import { useToastMessageDispatch, useAbsoluteUrl, useEndpoint, useTranslation, useSetModal } from '@rocket.chat/ui-contexts';
import DOMPurify from 'dompurify';
import type { ReactElement } from 'react';
import { memo, useEffect, useState } from 'react';
Expand Down Expand Up @@ -34,15 +34,15 @@ function OAuthGroupPage({ _id, onClickBack, ...group }: OAuthGroupPageProps): Re
};

const dispatchToastMessage = useToastMessageDispatch();
const refreshOAuthService = useMethod('refreshOAuthService');
const addOAuthService = useMethod('addOAuthService');
const removeOAuthService = useMethod('removeOAuthService');
const refreshOAuthService = useEndpoint('POST', '/v1/settings.refreshOAuthServices');
const addOAuthService = useEndpoint('POST', '/v1/settings.addCustomOAuth');
const removeOAuthService = useEndpoint('POST', '/v1/settings.removeCustomOAuth');
const setModal = useSetModal();

const handleRefreshOAuthServicesButtonClick = async (): Promise<void> => {
dispatchToastMessage({ type: 'info', message: t('Refreshing') });
try {
await refreshOAuthService();
await refreshOAuthService(undefined);
dispatchToastMessage({ type: 'success', message: t('Done') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
Expand All @@ -52,7 +52,7 @@ function OAuthGroupPage({ _id, onClickBack, ...group }: OAuthGroupPageProps): Re
const handleAddCustomOAuthButtonClick = (): void => {
const onConfirm = async (text: string): Promise<void> => {
try {
await addOAuthService(text);
await addOAuthService({ name: text });
dispatchToastMessage({ type: 'success', message: t('Custom_OAuth_has_been_added') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
Expand All @@ -72,7 +72,7 @@ function OAuthGroupPage({ _id, onClickBack, ...group }: OAuthGroupPageProps): Re
(): void => {
const handleConfirm = async (): Promise<void> => {
try {
await removeOAuthService(id);
await removeOAuthService({ name: id });
dispatchToastMessage({ type: 'success', message: t('Custom_OAuth_has_been_removed') });
setSettingSections(settingSections.filter((section) => section !== `Custom OAuth: ${capitalize(id)}`));
} catch (error) {
Expand Down
8 changes: 8 additions & 0 deletions packages/rest-typings/src/v1/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ export type SettingsEndpoints = {
POST: (params: { name: string }) => void;
};

'/v1/settings.removeCustomOAuth': {
POST: (params: { name: string }) => void;
};

'/v1/settings.refreshOAuthServices': {
POST: () => void;
};

'/v1/settings': {
GET: (params: SettingsGetParams) => {
settings: ISetting[];
Expand Down
Loading