chore: Relocate client-side custom OAuth modules (#36673)

sherif^2
Tasso Evangelista 5 months ago committed by GitHub
parent 61bca869b7
commit d9a685c710
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      apps/meteor/app/apple/client/index.ts
  2. 4
      apps/meteor/app/apple/lib/config.ts
  3. 1
      apps/meteor/app/nextcloud/client/index.ts
  4. 2
      apps/meteor/client/importPackages.ts
  5. 65
      apps/meteor/client/lib/customOAuth/CustomOAuth.ts
  6. 11
      apps/meteor/client/lib/customOAuth/CustomOAuthError.ts
  7. 30
      apps/meteor/client/views/root/AppLayout.tsx
  8. 9
      apps/meteor/client/views/root/hooks/customOAuth/useAppleOAuth.ts
  9. 4
      apps/meteor/client/views/root/hooks/customOAuth/useCustomOAuth.ts
  10. 13
      apps/meteor/client/views/root/hooks/customOAuth/useDolphinOAuth.ts
  11. 14
      apps/meteor/client/views/root/hooks/customOAuth/useDrupalOAuth.ts
  12. 14
      apps/meteor/client/views/root/hooks/customOAuth/useGitHubEnterpriseOAuth.ts
  13. 27
      apps/meteor/client/views/root/hooks/customOAuth/useGitLabOAuth.ts
  14. 14
      apps/meteor/client/views/root/hooks/customOAuth/useNextcloudOAuth.ts
  15. 19
      apps/meteor/client/views/root/hooks/customOAuth/useTokenpassOAuth.ts
  16. 8
      apps/meteor/client/views/root/hooks/customOAuth/useWordPressOAuth.ts
  17. 1
      packages/core-typings/src/ICustomOAuthConfig.ts

@ -1,4 +0,0 @@
import { CustomOAuth } from '../../custom-oauth/client/CustomOAuth';
import { config } from '../lib/config';
CustomOAuth.configureOAuthService('apple', config);

@ -1,3 +1,5 @@
import type { OauthConfig } from '@rocket.chat/core-typings';
export const config = {
serverURL: 'https://appleid.apple.com',
authorizePath: '/auth/authorize?response_mode=form_post',
@ -7,4 +9,4 @@ export const config = {
mergeUsers: true,
accessTokenParam: 'access_token',
loginStyle: 'popup',
};
} as const satisfies OauthConfig;

@ -1 +0,0 @@
import './useNextcloud';

@ -1,4 +1,3 @@
import '../app/apple/client';
import '../app/authorization/client';
import '../app/autotranslate/client';
import '../app/emoji/client';
@ -7,7 +6,6 @@ import '../app/gitlab/client';
import '../app/license/client';
import '../app/lib/client';
import '../app/livechat-enterprise/client';
import '../app/nextcloud/client';
import '../app/notifications/client';
import '../app/otr/client';
import '../app/slackbridge/client';

@ -2,25 +2,19 @@ import type { OAuthConfiguration, OauthConfig } from '@rocket.chat/core-typings'
import { Random } from '@rocket.chat/random';
import { capitalize } from '@rocket.chat/string-helpers';
import { Accounts } from 'meteor/accounts-base';
import { Match } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import { OAuth } from 'meteor/oauth';
import type { IOAuthProvider } from '../../../client/definitions/IOAuthProvider';
import { overrideLoginMethod, type LoginCallback } from '../../../client/lib/2fa/overrideLoginMethod';
import { loginServices } from '../../../client/lib/loginServices';
import { createOAuthTotpLoginMethod } from '../../../client/meteorOverrides/login/oauth';
import { isURL } from '../../../lib/utils/isURL';
// Request custom OAuth credentials for the user
// @param options {optional}
// @param credentialRequestCompleteCallback {Function} Callback function to call on
// completion. Takes one argument, credentialToken on success, or Error on
// error.
import type { IOAuthProvider } from '../../definitions/IOAuthProvider';
import { createOAuthTotpLoginMethod } from '../../meteorOverrides/login/oauth';
import { overrideLoginMethod, type LoginCallback } from '../2fa/overrideLoginMethod';
import { loginServices } from '../loginServices';
import { CustomOAuthError } from './CustomOAuthError';
const configuredOAuthServices = new Map<string, CustomOAuth>();
export class CustomOAuth implements IOAuthProvider {
export class CustomOAuth<TServiceName extends string = string> implements IOAuthProvider {
public serverURL: string;
public authorizePath: string;
@ -30,14 +24,9 @@ export class CustomOAuth implements IOAuthProvider {
public responseType: string;
constructor(
public readonly name: string,
options: OauthConfig,
public readonly name: TServiceName,
options: Readonly<OauthConfig>,
) {
this.name = name;
if (!Match.test(this.name, String)) {
throw new Meteor.Error('CustomOAuth: Name is required and must be String');
}
this.configure(options);
Accounts.oauth.registerService(this.name);
@ -45,26 +34,18 @@ export class CustomOAuth implements IOAuthProvider {
this.configureLogin();
}
configure(options: OauthConfig) {
if (!Match.test(options, Object)) {
throw new Meteor.Error('CustomOAuth: Options is required and must be Object');
}
if (!Match.test(options.serverURL, String)) {
throw new Meteor.Error('CustomOAuth: Options.serverURL is required and must be String');
}
if (!Match.test(options.authorizePath, String)) {
options.authorizePath = '/oauth/authorize';
configure(options: Readonly<OauthConfig>) {
if (typeof options !== 'object' || !options) {
throw new CustomOAuthError('options is required and must be object');
}
if (!Match.test(options.scope, String)) {
options.scope = 'openid';
if (typeof options.serverURL !== 'string') {
throw new CustomOAuthError('options.serverURL is required and must be string');
}
this.serverURL = options.serverURL;
this.authorizePath = options.authorizePath;
this.scope = options.scope;
this.authorizePath = options.authorizePath ?? '/oauth/authorize';
this.scope = options.scope ?? 'openid';
this.responseType = options.responseType || 'code';
if (!isURL(this.authorizePath)) {
@ -73,7 +54,7 @@ export class CustomOAuth implements IOAuthProvider {
}
configureLogin() {
const loginWithService = `loginWith${capitalize(String(this.name || ''))}` as const;
const loginWithService = `loginWith${capitalize(this.name) as Capitalize<TServiceName>}` as const;
const loginWithOAuthTokenAndTOTP = createOAuthTotpLoginMethod(this);
@ -125,17 +106,20 @@ export class CustomOAuth implements IOAuthProvider {
});
}
static configureOAuthService(serviceName: string, options: OauthConfig): CustomOAuth {
static configureOAuthService<TServiceName extends string = string>(
serviceName: TServiceName,
options: Readonly<OauthConfig>,
): CustomOAuth<TServiceName> {
const existingInstance = configuredOAuthServices.get(serviceName);
if (existingInstance) {
existingInstance.configure(options);
return existingInstance;
return existingInstance as CustomOAuth<TServiceName>;
}
// If we don't have a reference to the instance for this service and it was already registered on meteor,
// then there's nothing we can do to update it
if (Accounts.oauth.serviceNames().includes(serviceName)) {
throw new Error(`CustomOAuth service [${serviceName}] already registered, skipping new configuration.`);
throw new CustomOAuthError('service already registered, skipping new configuration', { service: serviceName });
}
const instance = new CustomOAuth(serviceName, options);
@ -143,7 +127,10 @@ export class CustomOAuth implements IOAuthProvider {
return instance;
}
static configureCustomOAuthService(serviceName: string, options: OauthConfig): CustomOAuth | undefined {
static configureCustomOAuthService<TServiceName extends string = string>(
serviceName: TServiceName,
options: Readonly<OauthConfig>,
): CustomOAuth<TServiceName> | undefined {
// Custom OAuth services are configured based on the login service list, so if this ends up being called multiple times, simply ignore it
// Non-Custom OAuth services are configured based on code, so if configureOAuthService is called multiple times for them, it's a bug and it should throw.
try {

@ -0,0 +1,11 @@
import { RocketChatError } from '../errors/RocketChatError';
type CustomOAuthErrorDetails = {
service?: string;
};
export class CustomOAuthError extends RocketChatError<'custom-oauth-error', CustomOAuthErrorDetails> {
constructor(reason?: string, details?: CustomOAuthErrorDetails) {
super('custom-oauth-error', details?.service ? `${details.service}: ${reason}` : reason, details);
}
}

@ -2,6 +2,15 @@ import { useEffect, Suspense, useSyncExternalStore } from 'react';
import DocumentTitleWrapper from './DocumentTitleWrapper';
import PageLoading from './PageLoading';
import { useAppleOAuth } from './hooks/customOAuth/useAppleOAuth';
import { useCustomOAuth } from './hooks/customOAuth/useCustomOAuth';
import { useDolphinOAuth } from './hooks/customOAuth/useDolphinOAuth';
import { useDrupalOAuth } from './hooks/customOAuth/useDrupalOAuth';
import { useGitHubEnterpriseOAuth } from './hooks/customOAuth/useGitHubEnterpriseOAuth';
import { useGitLabOAuth } from './hooks/customOAuth/useGitLabOAuth';
import { useNextcloudOAuth } from './hooks/customOAuth/useNextcloudOAuth';
import { useTokenpassOAuth } from './hooks/customOAuth/useTokenpassOAuth';
import { useWordPressOAuth } from './hooks/customOAuth/useWordPressOAuth';
import { useCodeHighlight } from './hooks/useCodeHighlight';
import { useEscapeKeyStroke } from './hooks/useEscapeKeyStroke';
import { useGoogleTagManager } from './hooks/useGoogleTagManager';
@ -9,16 +18,9 @@ import { useLoadMissedMessages } from './hooks/useLoadMissedMessages';
import { useLoginViaQuery } from './hooks/useLoginViaQuery';
import { useMessageLinkClicks } from './hooks/useMessageLinkClicks';
import { useSettingsOnLoadSiteUrl } from './hooks/useSettingsOnLoadSiteUrl';
import { useWordPressOAuth } from './hooks/useWordPressOAuth';
import { useCorsSSLConfig } from '../../../app/cors/client/useCorsSSLConfig';
import { useDolphin } from '../../../app/dolphin/client/hooks/useDolphin';
import { useDrupal } from '../../../app/drupal/client/hooks/useDrupal';
import { useEmojiOne } from '../../../app/emoji-emojione/client/hooks/useEmojiOne';
import { useGitHubEnterpriseAuth } from '../../../app/github-enterprise/client/hooks/useGitHubEnterpriseAuth';
import { useGitLabAuth } from '../../../app/gitlab/client/hooks/useGitLabAuth';
import { useLivechatEnterprise } from '../../../app/livechat-enterprise/hooks/useLivechatEnterprise';
import { useNextcloud } from '../../../app/nextcloud/client/useNextcloud';
import { useTokenPassAuth } from '../../../app/tokenpass/client/hooks/useTokenPassAuth';
import { useIframeLoginListener } from '../../hooks/iframe/useIframeLoginListener';
import { useNotificationPermission } from '../../hooks/notification/useNotificationPermission';
import { useAnalytics } from '../../hooks/useAnalytics';
@ -26,7 +28,6 @@ import { useAnalyticsEventTracking } from '../../hooks/useAnalyticsEventTracking
import { useAutoupdate } from '../../hooks/useAutoupdate';
import { useLoadRoomForAllowedAnonymousRead } from '../../hooks/useLoadRoomForAllowedAnonymousRead';
import { appLayout } from '../../lib/appLayout';
import { useCustomOAuth } from '../../sidebar/hooks/useCustomOAuth';
import { useRedirectToSetupWizard } from '../../startup/useRedirectToSetupWizard';
const AppLayout = () => {
@ -50,12 +51,13 @@ const AppLayout = () => {
useRedirectToSetupWizard();
useSettingsOnLoadSiteUrl();
useLivechatEnterprise();
useNextcloud();
useGitLabAuth();
useGitHubEnterpriseAuth();
useDrupal();
useDolphin();
useTokenPassAuth();
useNextcloudOAuth();
useGitLabOAuth();
useGitHubEnterpriseOAuth();
useDrupalOAuth();
useDolphinOAuth();
useTokenpassOAuth();
useAppleOAuth();
useWordPressOAuth();
useCustomOAuth();
useCorsSSLConfig();

@ -0,0 +1,9 @@
import { config } from '../../../../../app/apple/lib/config';
import { CustomOAuth } from '../../../../lib/customOAuth/CustomOAuth';
/* const Apple =*/ CustomOAuth.configureOAuthService('apple', config);
export const useAppleOAuth = () => {
// Here we would expect to handle changes in settings, updating the configuration
// accordingly, but it was not implemented yet.
};

@ -1,7 +1,7 @@
import { useEffect } from 'react';
import { CustomOAuth } from '../../../app/custom-oauth/client/CustomOAuth';
import { loginServices } from '../../lib/loginServices';
import { CustomOAuth } from '../../../../lib/customOAuth/CustomOAuth';
import { loginServices } from '../../../../lib/loginServices';
export const useCustomOAuth = () => {
useEffect(

@ -1,7 +1,8 @@
import type { OauthConfig } from '@rocket.chat/core-typings';
import { useSetting } from '@rocket.chat/ui-contexts';
import { useEffect } from 'react';
import { CustomOAuth } from '../../../custom-oauth/client/CustomOAuth';
import { CustomOAuth } from '../../../../lib/customOAuth/CustomOAuth';
const config = {
serverURL: '',
@ -14,18 +15,20 @@ const config = {
forOtherUsers: ['services.dolphin.name'],
},
accessTokenParam: 'access_token',
};
} as const satisfies OauthConfig;
const Dolphin = CustomOAuth.configureOAuthService('dolphin', config);
export const useDolphin = () => {
export const useDolphinOAuth = () => {
const enabled = useSetting('Accounts_OAuth_Dolphin');
const url = useSetting('Accounts_OAuth_Dolphin_URL') as string;
useEffect(() => {
if (enabled) {
config.serverURL = url;
Dolphin.configure(config);
Dolphin.configure({
...config,
serverURL: url,
});
}
}, [enabled, url]);
};

@ -2,12 +2,12 @@ import type { OauthConfig } from '@rocket.chat/core-typings';
import { useSetting } from '@rocket.chat/ui-contexts';
import { useEffect } from 'react';
import { CustomOAuth } from '../../../custom-oauth/client/CustomOAuth';
import { CustomOAuth } from '../../../../lib/customOAuth/CustomOAuth';
// Drupal Server CallBack URL needs to be http(s)://{rocketchat.server}[:port]/_oauth/drupal
// In RocketChat -> Administration the URL needs to be http(s)://{drupal.server}/
const config: OauthConfig = {
const config = {
serverURL: '',
identityPath: '/oauth2/UserInfo',
authorizePath: '/oauth2/authorize',
@ -21,17 +21,19 @@ const config: OauthConfig = {
forOtherUsers: ['services.drupal.name'],
},
accessTokenParam: 'access_token',
};
} as const satisfies OauthConfig;
const Drupal = CustomOAuth.configureOAuthService('drupal', config);
export const useDrupal = () => {
export const useDrupalOAuth = () => {
const drupalUrl = useSetting('API_Drupal_URL') as string;
useEffect(() => {
if (drupalUrl) {
config.serverURL = drupalUrl;
Drupal.configure(config);
Drupal.configure({
...config,
serverURL: drupalUrl,
});
}
}, [drupalUrl]);
};

@ -2,12 +2,12 @@ import type { OauthConfig } from '@rocket.chat/core-typings';
import { useSetting } from '@rocket.chat/ui-contexts';
import { useEffect } from 'react';
import { CustomOAuth } from '../../../custom-oauth/client/CustomOAuth';
import { CustomOAuth } from '../../../../lib/customOAuth/CustomOAuth';
// GitHub Enterprise Server CallBack URL needs to be http(s)://{rocketchat.server}[:port]/_oauth/github_enterprise
// In RocketChat -> Administration the URL needs to be http(s)://{github.enterprise.server}/
const config: OauthConfig = {
const config = {
serverURL: '',
identityPath: '/api/v3/user',
authorizePath: '/login/oauth/authorize',
@ -16,17 +16,19 @@ const config: OauthConfig = {
forLoggedInUser: ['services.github-enterprise'],
forOtherUsers: ['services.github-enterprise.username'],
},
};
} as const satisfies OauthConfig;
const GitHubEnterprise = CustomOAuth.configureOAuthService('github_enterprise', config);
export const useGitHubEnterpriseAuth = () => {
export const useGitHubEnterpriseOAuth = () => {
const githubApiUrl = useSetting('API_GitHub_Enterprise_URL') as string;
useEffect(() => {
if (githubApiUrl) {
config.serverURL = githubApiUrl;
GitHubEnterprise.configure(config);
GitHubEnterprise.configure({
...config,
serverURL: githubApiUrl,
});
}
}, [githubApiUrl]);
};

@ -2,9 +2,9 @@ import type { OauthConfig } from '@rocket.chat/core-typings';
import { useSetting } from '@rocket.chat/ui-contexts';
import { useEffect } from 'react';
import { CustomOAuth } from '../../../custom-oauth/client/CustomOAuth';
import { CustomOAuth } from '../../../../lib/customOAuth/CustomOAuth';
const config: OauthConfig = {
const config = {
serverURL: 'https://gitlab.com',
identityPath: '/api/v4/user',
scope: 'read_user',
@ -14,28 +14,21 @@ const config: OauthConfig = {
forOtherUsers: ['services.gitlab.username'],
},
accessTokenParam: 'access_token',
};
} as const satisfies OauthConfig;
const Gitlab = CustomOAuth.configureOAuthService('gitlab', config);
export const useGitLabAuth = () => {
export const useGitLabOAuth = () => {
const gitlabApiUrl = useSetting('API_Gitlab_URL') as string;
const gitlabIdentiry = useSetting('Accounts_OAuth_Gitlab_identity_path') as string;
const gitlabMergeUsers = useSetting('Accounts_OAuth_Gitlab_merge_users', false);
useEffect(() => {
if (gitlabApiUrl) {
config.serverURL = gitlabApiUrl.trim().replace(/\/*$/, '');
}
if (gitlabIdentiry) {
config.identityPath = gitlabIdentiry.trim() || config.identityPath;
}
if (gitlabMergeUsers) {
config.mergeUsers = true;
}
Gitlab.configure(config);
Gitlab.configure({
...config,
...(gitlabApiUrl && { serverURL: gitlabApiUrl.trim().replace(/\/*$/, '') }),
...(gitlabIdentiry && { identityPath: gitlabIdentiry.trim() || config.identityPath }),
...(gitlabMergeUsers && { mergeUsers: true }),
});
}, [gitlabApiUrl, gitlabIdentiry, gitlabMergeUsers]);
};

@ -2,9 +2,9 @@ import type { OauthConfig } from '@rocket.chat/core-typings';
import { useSetting } from '@rocket.chat/ui-contexts';
import { useEffect } from 'react';
import { CustomOAuth } from '../../custom-oauth/client/CustomOAuth';
import { CustomOAuth } from '../../../../lib/customOAuth/CustomOAuth';
const config: OauthConfig = {
const config = {
serverURL: '',
tokenPath: '/index.php/apps/oauth2/api/v1/token',
tokenSentVia: 'header',
@ -15,17 +15,19 @@ const config: OauthConfig = {
forLoggedInUser: ['services.nextcloud'],
forOtherUsers: ['services.nextcloud.name'],
},
};
} as const satisfies OauthConfig;
const Nextcloud = CustomOAuth.configureOAuthService('nextcloud', config);
export const useNextcloud = (): void => {
export const useNextcloudOAuth = (): void => {
const nextcloudURL = useSetting('Accounts_OAuth_Nextcloud_URL') as string;
useEffect(() => {
if (nextcloudURL) {
config.serverURL = nextcloudURL.trim().replace(/\/*$/, '');
Nextcloud.configure(config);
Nextcloud.configure({
...config,
serverURL: nextcloudURL.trim().replace(/\/*$/, ''),
});
}
}, [nextcloudURL]);
};

@ -2,9 +2,9 @@ import type { OauthConfig } from '@rocket.chat/core-typings';
import { useSetting } from '@rocket.chat/ui-contexts';
import { useEffect } from 'react';
import { CustomOAuth } from '../../../custom-oauth/client/CustomOAuth';
import { CustomOAuth } from '../../../../lib/customOAuth/CustomOAuth';
const config: OauthConfig = {
const config = {
serverURL: '',
identityPath: '/oauth/user',
authorizePath: '/oauth/authorize',
@ -18,18 +18,19 @@ const config: OauthConfig = {
forOtherUsers: ['services.tokenpass.name'],
},
accessTokenParam: 'access_token',
};
} as const satisfies OauthConfig;
const Tokenpass = CustomOAuth.configureOAuthService('tokenpass', config);
export const useTokenPassAuth = () => {
export const useTokenpassOAuth = () => {
const setting = useSetting('API_Tokenpass_URL') as string | undefined;
useEffect(() => {
if (!setting) {
return;
}
config.serverURL = setting;
Tokenpass.configure(config);
if (!setting) return;
Tokenpass.configure({
...config,
serverURL: setting,
});
}, [setting]);
};

@ -2,16 +2,16 @@ import type { OauthConfig } from '@rocket.chat/core-typings';
import { useSetting } from '@rocket.chat/ui-contexts';
import { useEffect } from 'react';
import { CustomOAuth } from '../../../../app/custom-oauth/client/CustomOAuth';
import { CustomOAuth } from '../../../../lib/customOAuth/CustomOAuth';
const configDefault: OauthConfig = {
const configDefault = {
serverURL: '',
addAutopublishFields: {
forLoggedInUser: ['services.wordpress'],
forOtherUsers: ['services.wordpress.user_login'],
},
accessTokenParam: 'access_token',
};
} as const satisfies OauthConfig;
const WordPress = CustomOAuth.configureOAuthService('wordpress', configDefault);
@ -22,7 +22,7 @@ const configureServerType = (
tokenPath?: string,
authorizePath?: string,
scope?: string,
) => {
): OauthConfig => {
switch (serverType) {
case 'custom': {
return {

@ -14,4 +14,5 @@ export type OauthConfig = {
usernameField?: string;
mergeUsers?: boolean;
responseType?: string;
loginStyle?: 'popup' | 'redirect';
};

Loading…
Cancel
Save