Chore: Partially migrate 2FA client code to TypeScript (#23419)
parent
a1f5943160
commit
655353257c
@ -1,64 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { SHA256 } from 'meteor/sha'; |
||||
import toastr from 'toastr'; |
||||
|
||||
import { t } from '../../utils/client'; |
||||
import { imperativeModal } from '../../../client/lib/imperativeModal'; |
||||
import TwoFactorModal from '../../../client/components/TwoFactorModal'; |
||||
|
||||
|
||||
export function process2faReturn({ error, result, originalCallback, onCode, emailOrUsername }) { |
||||
if (!error || (error.error !== 'totp-required' && error.errorType !== 'totp-required')) { |
||||
return originalCallback(error, result); |
||||
} |
||||
|
||||
const method = error.details && error.details.method; |
||||
|
||||
if (!emailOrUsername && Meteor.user()) { |
||||
emailOrUsername = Meteor.user().username; |
||||
} |
||||
|
||||
imperativeModal.open({ |
||||
component: TwoFactorModal, |
||||
props: { |
||||
method, |
||||
onConfirm: (code, method) => { |
||||
onCode(method === 'password' ? SHA256(code) : code, method); |
||||
imperativeModal.close(); |
||||
}, |
||||
onClose: () => { |
||||
imperativeModal.close(); |
||||
originalCallback(new Meteor.Error('totp-canceled')); |
||||
}, |
||||
emailOrUsername, |
||||
}, |
||||
}); |
||||
} |
||||
|
||||
const { call } = Meteor; |
||||
Meteor.call = function(ddpMethod, ...args) { |
||||
let callback = args.pop(); |
||||
if (typeof callback !== 'function') { |
||||
args.push(callback); |
||||
callback = () => {}; |
||||
} |
||||
|
||||
return call(ddpMethod, ...args, function(error, result) { |
||||
process2faReturn({ |
||||
error, |
||||
result, |
||||
originalCallback: callback, |
||||
onCode: (code, method) => { |
||||
call(ddpMethod, ...args, { twoFactorCode: code, twoFactorMethod: method }, (error, result) => { |
||||
if (error && error.error === 'totp-invalid') { |
||||
error.toastrShowed = true; |
||||
toastr.error(t('Invalid_two_factor_code')); |
||||
return callback(error); |
||||
} |
||||
|
||||
callback(error, result); |
||||
}); |
||||
}, |
||||
}); |
||||
}); |
||||
}; |
@ -1,7 +1,7 @@ |
||||
import './callWithTwoFactorRequired'; |
||||
import './TOTPPassword'; |
||||
import './TOTPOAuth'; |
||||
import './TOTPGoogle'; |
||||
import './TOTPSaml'; |
||||
import './TOTPLDAP'; |
||||
import './TOTPCrowd'; |
||||
import './overrideMeteorCall'; |
@ -1,51 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import toastr from 'toastr'; |
||||
import { Accounts } from 'meteor/accounts-base'; |
||||
|
||||
import { t } from '../../../utils/client'; |
||||
import { process2faReturn } from '../callWithTwoFactorRequired'; |
||||
|
||||
export class Utils2fa { |
||||
static reportError(error, callback) { |
||||
if (callback) { |
||||
callback(error); |
||||
} else { |
||||
throw error; |
||||
} |
||||
} |
||||
|
||||
static convertError(err) { |
||||
if (err && err instanceof Meteor.Error && err.error === Accounts.LoginCancelledError.numericError) { |
||||
return new Accounts.LoginCancelledError(err.reason); |
||||
} |
||||
|
||||
return err; |
||||
} |
||||
|
||||
static overrideLoginMethod(loginMethod, loginArgs, cb, loginMethodTOTP, emailOrUsername) { |
||||
loginMethod.apply(this, loginArgs.concat([(error) => { |
||||
if (!error || error.error !== 'totp-required') { |
||||
return cb(error); |
||||
} |
||||
|
||||
process2faReturn({ |
||||
error, |
||||
emailOrUsername, |
||||
originalCallback: cb, |
||||
onCode: (code) => { |
||||
loginMethodTOTP && loginMethodTOTP.apply(this, loginArgs.concat([code, (error) => { |
||||
if (error) { |
||||
console.log(error); |
||||
} |
||||
if (error && error.error === 'totp-invalid') { |
||||
toastr.error(t('Invalid_two_factor_code')); |
||||
cb(); |
||||
} else { |
||||
cb(error); |
||||
} |
||||
}])); |
||||
}, |
||||
}); |
||||
}])); |
||||
} |
||||
} |
@ -0,0 +1,49 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
import { t } from '../../utils/client'; |
||||
import { process2faReturn } from '../../../client/lib/2fa/process2faReturn'; |
||||
import { isTotpInvalidError } from '../../../client/lib/2fa/utils'; |
||||
import { dispatchToastMessage } from '../../../client/lib/toast'; |
||||
|
||||
const { call } = Meteor; |
||||
|
||||
type Callback = { |
||||
(error: unknown): void; |
||||
(error: unknown, result: unknown): void; |
||||
}; |
||||
|
||||
const callWithTotp = (methodName: string, args: unknown[], callback: Callback) => |
||||
(twoFactorCode: string, twoFactorMethod: string): unknown => |
||||
call(methodName, ...args, { twoFactorCode, twoFactorMethod }, (error: unknown, result: unknown): void => { |
||||
if (isTotpInvalidError(error)) { |
||||
(error as { toastrShowed?: true }).toastrShowed = true; |
||||
dispatchToastMessage({ |
||||
type: 'error', |
||||
message: t('Invalid_two_factor_code'), |
||||
}); |
||||
callback(error); |
||||
return; |
||||
} |
||||
|
||||
callback(error, result); |
||||
}); |
||||
|
||||
const callWithoutTotp = (methodName: string, args: unknown[], callback: Callback) => |
||||
(): unknown => |
||||
call(methodName, ...args, (error: unknown, result: unknown): void => { |
||||
process2faReturn({ |
||||
error, |
||||
result, |
||||
onCode: callWithTotp(methodName, args, callback), |
||||
originalCallback: callback, |
||||
emailOrUsername: undefined, |
||||
}); |
||||
}); |
||||
|
||||
Meteor.call = function(methodName: string, ...args: unknown[]): unknown { |
||||
const callback = args.length > 0 && typeof args[args.length - 1] === 'function' |
||||
? args.pop() as Callback |
||||
: (): void => undefined; |
||||
|
||||
return callWithoutTotp(methodName, args, callback)(); |
||||
}; |
@ -0,0 +1,48 @@ |
||||
import { t } from '../../../app/utils/client'; |
||||
import { dispatchToastMessage } from '../toast'; |
||||
import { process2faReturn } from './process2faReturn'; |
||||
import { isTotpInvalidError, isTotpRequiredError } from './utils'; |
||||
|
||||
type LoginCallback = { |
||||
(error: unknown): void; |
||||
(error: unknown, result: unknown): void; |
||||
}; |
||||
|
||||
type LoginMethod<A extends unknown[]> = (...args: [...args: A, cb: LoginCallback]) => void; |
||||
|
||||
type LoginMethodWithTotp<A extends unknown[]> = ( |
||||
...args: [...args: A, code: string, cb: LoginCallback] |
||||
) => void; |
||||
|
||||
export const overrideLoginMethod = <A extends unknown[]>( |
||||
loginMethod: LoginMethod<A>, |
||||
loginArgs: A, |
||||
callback: LoginCallback, |
||||
loginMethodTOTP: LoginMethodWithTotp<A>, |
||||
emailOrUsername: string, |
||||
): void => { |
||||
loginMethod.call(null, ...loginArgs, (error: unknown, result?: unknown) => { |
||||
if (!isTotpRequiredError(error)) { |
||||
callback(error); |
||||
return; |
||||
} |
||||
|
||||
process2faReturn({ |
||||
error, |
||||
result, |
||||
emailOrUsername, |
||||
originalCallback: callback, |
||||
onCode: (code: string) => { |
||||
loginMethodTOTP?.call(null, ...loginArgs, code, (error: unknown) => { |
||||
if (isTotpInvalidError(error)) { |
||||
dispatchToastMessage({ type: 'error', message: t('Invalid_two_factor_code') }); |
||||
callback(null); |
||||
return; |
||||
} |
||||
|
||||
callback(error); |
||||
}); |
||||
}, |
||||
}); |
||||
}); |
||||
}; |
@ -0,0 +1,82 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { SHA256 } from 'meteor/sha'; |
||||
|
||||
import TwoFactorModal from '../../components/TwoFactorModal'; |
||||
import { imperativeModal } from '../imperativeModal'; |
||||
import { isTotpRequiredError } from './utils'; |
||||
|
||||
const twoFactorMethods = ['totp', 'email', 'password'] as const; |
||||
|
||||
type TwoFactorMethod = typeof twoFactorMethods[number]; |
||||
|
||||
const isTwoFactorMethod = (method: string): method is TwoFactorMethod => |
||||
twoFactorMethods.includes(method as TwoFactorMethod); |
||||
|
||||
const hasRequiredTwoFactorMethod = ( |
||||
error: Meteor.Error, |
||||
): error is Meteor.Error & { details: { method: TwoFactorMethod } } => { |
||||
const details = error.details as unknown; |
||||
|
||||
return ( |
||||
typeof details === 'object' && |
||||
details !== null && |
||||
typeof (details as { method: unknown }).method === 'string' && |
||||
isTwoFactorMethod((details as { method: string }).method) |
||||
); |
||||
}; |
||||
|
||||
function assertModalProps(props: { |
||||
method: TwoFactorMethod; |
||||
emailOrUsername?: string; |
||||
}): asserts props is |
||||
| { method: 'totp' } |
||||
| { method: 'password' } |
||||
| { method: 'email'; emailOrUsername: string } { |
||||
if (props.method === 'email' && typeof props.emailOrUsername !== 'string') { |
||||
throw new Error('Invalid Two Factor method'); |
||||
} |
||||
} |
||||
|
||||
export function process2faReturn({ |
||||
error, |
||||
result, |
||||
originalCallback, |
||||
onCode, |
||||
emailOrUsername, |
||||
}: { |
||||
error: unknown; |
||||
result: unknown; |
||||
originalCallback: { |
||||
(error: unknown): void; |
||||
(error: unknown, result: unknown): void; |
||||
}; |
||||
onCode: (code: string, method: string) => void; |
||||
emailOrUsername: string | null | undefined; |
||||
}): void { |
||||
if (!isTotpRequiredError(error) || !hasRequiredTwoFactorMethod(error)) { |
||||
originalCallback(error, result); |
||||
return; |
||||
} |
||||
|
||||
const props = { |
||||
method: error.details.method, |
||||
emailOrUsername: emailOrUsername ?? Meteor.user()?.username, |
||||
}; |
||||
|
||||
assertModalProps(props); |
||||
|
||||
imperativeModal.open({ |
||||
component: TwoFactorModal, |
||||
props: { |
||||
...props, |
||||
onConfirm: (code: string, method: string): void => { |
||||
imperativeModal.close(); |
||||
onCode(method === 'password' ? SHA256(code) : code, method); |
||||
}, |
||||
onClose: (): void => { |
||||
imperativeModal.close(); |
||||
originalCallback(new Meteor.Error('totp-canceled')); |
||||
}, |
||||
}, |
||||
}); |
||||
} |
@ -0,0 +1,32 @@ |
||||
import { Accounts } from 'meteor/accounts-base'; |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
export const isTotpRequiredError = ( |
||||
error: unknown, |
||||
): error is Meteor.Error & { error: 'totp-required' } => |
||||
(error as { error?: unknown } | undefined)?.error === 'totp-required'; |
||||
|
||||
export const isTotpInvalidError = ( |
||||
error: unknown, |
||||
): error is Meteor.Error & { error: 'totp-invalid' } => |
||||
(error as { error?: unknown } | undefined)?.error === 'totp-invalid'; |
||||
|
||||
export const isLoginCancelledError = (error: unknown): error is Meteor.Error => |
||||
error instanceof Meteor.Error && error.error === Accounts.LoginCancelledError.numericError; |
||||
|
||||
export const reportError = <T>(error: T, callback?: (error?: T) => void): void => { |
||||
if (callback) { |
||||
callback(error); |
||||
return; |
||||
} |
||||
|
||||
throw error; |
||||
}; |
||||
|
||||
export const convertError = <T>(error: T): Accounts.LoginCancelledError | T => { |
||||
if (isLoginCancelledError(error)) { |
||||
return new Accounts.LoginCancelledError(error.reason); |
||||
} |
||||
|
||||
return error; |
||||
}; |
@ -0,0 +1,21 @@ |
||||
import { Emitter } from '@rocket.chat/emitter'; |
||||
|
||||
export type ToastMessagePayload = { |
||||
type: 'success' | 'info' | 'warning' | 'error'; |
||||
message: string | Error; |
||||
title?: string; |
||||
options?: object; |
||||
}; |
||||
|
||||
const emitter = new Emitter<{ |
||||
notify: ToastMessagePayload; |
||||
}>(); |
||||
|
||||
export const dispatchToastMessage = (payload: ToastMessagePayload): void => { |
||||
// TODO: buffer it if there is no subscriber
|
||||
emitter.emit('notify', payload); |
||||
}; |
||||
|
||||
export const subscribeToToastMessages = ( |
||||
callback: (payload: ToastMessagePayload) => void, |
||||
): (() => void) => emitter.on('notify', callback); |
@ -1,28 +1,33 @@ |
||||
import React, { FC } from 'react'; |
||||
import React, { FC, useEffect } from 'react'; |
||||
import toastr from 'toastr'; |
||||
|
||||
import { ToastMessagesContext, ToastMessagePayload } from '../contexts/ToastMessagesContext'; |
||||
import { ToastMessagesContext } from '../contexts/ToastMessagesContext'; |
||||
import { dispatchToastMessage, subscribeToToastMessages } from '../lib/toast'; |
||||
import { handleError } from '../lib/utils/handleError'; |
||||
|
||||
const dispatch = ({ type, message, title, options }: ToastMessagePayload): void => { |
||||
if (type === 'error' && typeof message === 'object') { |
||||
handleError(message); |
||||
return; |
||||
} |
||||
const contextValue = { |
||||
dispatch: dispatchToastMessage, |
||||
}; |
||||
|
||||
if (typeof message !== 'string') { |
||||
message = `[${message.name}] ${message.message}`; |
||||
} |
||||
const ToastMessagesProvider: FC = ({ children }) => { |
||||
useEffect( |
||||
() => |
||||
subscribeToToastMessages(({ type, message, title, options }) => { |
||||
if (type === 'error' && typeof message === 'object') { |
||||
handleError(message); |
||||
return; |
||||
} |
||||
|
||||
toastr[type](message, title, options); |
||||
}; |
||||
if (typeof message !== 'string') { |
||||
message = `[${message.name}] ${message.message}`; |
||||
} |
||||
|
||||
const contextValue = { |
||||
dispatch, |
||||
}; |
||||
toastr[type](message, title, options); |
||||
}), |
||||
[], |
||||
); |
||||
|
||||
const ToastMessagesProvider: FC = ({ children }) => ( |
||||
<ToastMessagesContext.Provider children={children} value={contextValue} /> |
||||
); |
||||
return <ToastMessagesContext.Provider children={children} value={contextValue} />; |
||||
}; |
||||
|
||||
export default ToastMessagesProvider; |
||||
|
Loading…
Reference in new issue