|
|
|
|
@ -3,7 +3,7 @@ import { Meteor } from 'meteor/meteor'; |
|
|
|
|
|
|
|
|
|
import TwoFactorModal from '../../components/TwoFactorModal'; |
|
|
|
|
import { imperativeModal } from '../imperativeModal'; |
|
|
|
|
import { isTotpRequiredError } from './utils'; |
|
|
|
|
import { isTotpInvalidError, isTotpRequiredError } from './utils'; |
|
|
|
|
|
|
|
|
|
const twoFactorMethods = ['totp', 'email', 'password'] as const; |
|
|
|
|
|
|
|
|
|
@ -49,7 +49,7 @@ export async function process2faReturn({ |
|
|
|
|
onCode: (code: string, method: string) => void; |
|
|
|
|
emailOrUsername: string | null | undefined; |
|
|
|
|
}): Promise<void> { |
|
|
|
|
if (!isTotpRequiredError(error) || !hasRequiredTwoFactorMethod(error)) { |
|
|
|
|
if (!(isTotpRequiredError(error) || isTotpInvalidError(error)) || !hasRequiredTwoFactorMethod(error)) { |
|
|
|
|
originalCallback(error, result); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
@ -57,66 +57,83 @@ export async function process2faReturn({ |
|
|
|
|
const props = { |
|
|
|
|
method: error.details.method, |
|
|
|
|
emailOrUsername: emailOrUsername || error.details.emailOrUsername || Meteor.user()?.username, |
|
|
|
|
// eslint-disable-next-line no-nested-ternary
|
|
|
|
|
invalidAttempt: isTotpInvalidError(error), |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
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')); |
|
|
|
|
}, |
|
|
|
|
}, |
|
|
|
|
}); |
|
|
|
|
try { |
|
|
|
|
const code = await invokeTwoFactorModal(props); |
|
|
|
|
|
|
|
|
|
onCode(code, props.method); |
|
|
|
|
} catch (error) { |
|
|
|
|
process2faReturn({ |
|
|
|
|
error, |
|
|
|
|
result, |
|
|
|
|
originalCallback, |
|
|
|
|
onCode, |
|
|
|
|
emailOrUsername, |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export async function process2faAsyncReturn({ |
|
|
|
|
promise, |
|
|
|
|
error, |
|
|
|
|
onCode, |
|
|
|
|
emailOrUsername, |
|
|
|
|
}: { |
|
|
|
|
promise: Promise<unknown>; |
|
|
|
|
error: unknown; |
|
|
|
|
onCode: (code: string, method: string) => unknown | Promise<unknown>; |
|
|
|
|
emailOrUsername: string | null | undefined; |
|
|
|
|
}): Promise<unknown> { |
|
|
|
|
// if the promise is rejected, we need to check if it's a 2fa error
|
|
|
|
|
return promise.catch(async (error) => { |
|
|
|
|
// if it's not a 2fa error, we reject the promise
|
|
|
|
|
if (!isTotpRequiredError(error) || !hasRequiredTwoFactorMethod(error)) { |
|
|
|
|
throw error; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const props = { |
|
|
|
|
method: error.details.method, |
|
|
|
|
emailOrUsername: emailOrUsername || error.details.emailOrUsername || Meteor.user()?.username, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
assertModalProps(props); |
|
|
|
|
|
|
|
|
|
return new Promise<unknown>((resolve, reject) => { |
|
|
|
|
imperativeModal.open({ |
|
|
|
|
component: TwoFactorModal, |
|
|
|
|
props: { |
|
|
|
|
...props, |
|
|
|
|
onConfirm: (code: string, method: string): void => { |
|
|
|
|
imperativeModal.close(); |
|
|
|
|
|
|
|
|
|
// once we have the code, we resolve the promise with the result of the `onCode` callback
|
|
|
|
|
resolve(onCode(method === 'password' ? SHA256(code) : code, method)); |
|
|
|
|
}, |
|
|
|
|
onClose: (): void => { |
|
|
|
|
imperativeModal.close(); |
|
|
|
|
reject(new Meteor.Error('totp-canceled')); |
|
|
|
|
}, |
|
|
|
|
// if it's not a 2fa error, we reject the promise
|
|
|
|
|
if (!(isTotpRequiredError(error) || isTotpInvalidError(error)) || !hasRequiredTwoFactorMethod(error)) { |
|
|
|
|
throw error; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const props = { |
|
|
|
|
method: error.details.method, |
|
|
|
|
emailOrUsername: emailOrUsername || error.details.emailOrUsername || Meteor.user()?.username, |
|
|
|
|
// eslint-disable-next-line no-nested-ternary
|
|
|
|
|
invalidAttempt: isTotpInvalidError(error), |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
assertModalProps(props); |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
const code = await invokeTwoFactorModal(props); |
|
|
|
|
|
|
|
|
|
return onCode(code, props.method); |
|
|
|
|
} catch (error) { |
|
|
|
|
return process2faAsyncReturn({ |
|
|
|
|
error, |
|
|
|
|
onCode, |
|
|
|
|
emailOrUsername, |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export const invokeTwoFactorModal = async (props: { |
|
|
|
|
method: 'totp' | 'email' | 'password'; |
|
|
|
|
emailOrUsername?: string | undefined; |
|
|
|
|
invalidAttempt?: boolean; |
|
|
|
|
}) => { |
|
|
|
|
assertModalProps(props); |
|
|
|
|
|
|
|
|
|
return new Promise<string>((resolve, reject) => { |
|
|
|
|
imperativeModal.open({ |
|
|
|
|
component: TwoFactorModal, |
|
|
|
|
props: { |
|
|
|
|
...props, |
|
|
|
|
onConfirm: (code: string, method: string): void => { |
|
|
|
|
imperativeModal.close(); |
|
|
|
|
resolve(method === 'password' ? SHA256(code) : code); |
|
|
|
|
}, |
|
|
|
|
onClose: (): void => { |
|
|
|
|
imperativeModal.close(); |
|
|
|
|
reject(new Meteor.Error('totp-canceled')); |
|
|
|
|
}, |
|
|
|
|
}); |
|
|
|
|
}, |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|