[NEW] Two Factor authentication via email (#15949)
parent
93d1014759
commit
3813aefc8a
@ -0,0 +1,91 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { SHA256 } from 'meteor/sha'; |
||||
import toastr from 'toastr'; |
||||
|
||||
import { modal } from '../../ui-utils/client'; |
||||
import { t, APIClient } from '../../utils/client'; |
||||
|
||||
const methods = { |
||||
totp: { |
||||
text: 'Open_your_authentication_app_and_enter_the_code', |
||||
}, |
||||
email: { |
||||
text: 'Verify_your_email_for_the_code_we_sent', |
||||
html: true, |
||||
}, |
||||
password: { |
||||
title: 'Please_enter_your_password', |
||||
text: 'For_your_security_you_must_enter_your_current_password_to_continue', |
||||
inputType: 'password', |
||||
}, |
||||
}; |
||||
|
||||
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; |
||||
} |
||||
|
||||
modal.open({ |
||||
title: t(methods[method].title || 'Two Factor Authentication'), |
||||
text: t(methods[method].text), |
||||
html: methods[method].html, |
||||
type: 'input', |
||||
inputActionText: method === 'email' && t('Send_me_the_code_again'), |
||||
async inputAction(e) { |
||||
const { value } = e.currentTarget; |
||||
e.currentTarget.value = t('Sending'); |
||||
|
||||
await APIClient.v1.post('users.2fa.sendEmailCode', { emailOrUsername }); |
||||
|
||||
e.currentTarget.value = value; |
||||
}, |
||||
inputType: methods[method].inputType || 'text', |
||||
showCancelButton: true, |
||||
closeOnConfirm: true, |
||||
confirmButtonText: t('Verify'), |
||||
cancelButtonText: t('Cancel'), |
||||
}, (code) => { |
||||
if (code === false) { |
||||
return originalCallback(new Meteor.Error('totp-canceled')); |
||||
} |
||||
|
||||
if (method === 'password') { |
||||
code = SHA256(code); |
||||
} |
||||
onCode(code, method); |
||||
}); |
||||
} |
||||
|
||||
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,3 +1,4 @@ |
||||
import './accountSecurity.html'; |
||||
import './accountSecurity'; |
||||
import './callWithTwoFactorRequired'; |
||||
import './TOTPPassword'; |
||||
|
||||
@ -0,0 +1,17 @@ |
||||
import { DDPCommon } from 'meteor/ddp-common'; |
||||
import { DDP } from 'meteor/ddp'; |
||||
|
||||
class MethodInvocation extends DDPCommon.MethodInvocation { |
||||
constructor(options) { |
||||
const result = super(options); |
||||
const currentInvocation = DDP._CurrentInvocation.get(); |
||||
|
||||
if (currentInvocation) { |
||||
this.twoFactorChecked = currentInvocation.twoFactorChecked; |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
} |
||||
|
||||
DDPCommon.MethodInvocation = MethodInvocation; |
||||
@ -0,0 +1,138 @@ |
||||
import { Random } from 'meteor/random'; |
||||
import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; |
||||
import { Accounts } from 'meteor/accounts-base'; |
||||
import bcrypt from 'bcrypt'; |
||||
|
||||
import { settings } from '../../../settings/server'; |
||||
import * as Mailer from '../../../mailer'; |
||||
import { Users } from '../../../models/server'; |
||||
import { ICodeCheck, IProcessInvalidCodeResult } from './ICodeCheck'; |
||||
import { IUser } from '../../../../definition/IUser'; |
||||
|
||||
export class EmailCheck implements ICodeCheck { |
||||
public readonly name = 'email'; |
||||
|
||||
private getUserVerifiedEmails(user: IUser): string[] { |
||||
if (!Array.isArray(user.emails)) { |
||||
return []; |
||||
} |
||||
return user.emails.filter(({ verified }) => verified).map((e) => e.address); |
||||
} |
||||
|
||||
public isEnabled(user: IUser): boolean { |
||||
if (!settings.get('Accounts_TwoFactorAuthentication_By_Email_Enabled')) { |
||||
return false; |
||||
} |
||||
|
||||
if (!user.services?.email2fa?.enabled) { |
||||
return false; |
||||
} |
||||
|
||||
return this.getUserVerifiedEmails(user).length > 0; |
||||
} |
||||
|
||||
private send2FAEmail(address: string, random: string, user: IUser): void { |
||||
const language = user.language || settings.get('Language') || 'en'; |
||||
|
||||
const t = (s: string): string => TAPi18n.__(s, { lng: language }); |
||||
|
||||
Mailer.send({ |
||||
to: address, |
||||
from: settings.get('From_Email'), |
||||
subject: 'Authentication code', |
||||
replyTo: undefined, |
||||
data: { |
||||
code: random.replace(/^(\d{3})/, '$1-'), |
||||
}, |
||||
headers: undefined, |
||||
text: ` |
||||
${ t('Here_is_your_authentication_code') } |
||||
|
||||
__code__ |
||||
|
||||
${ t('Do_not_provide_this_code_to_anyone') } |
||||
${ t('If_you_didnt_try_to_login_in_your_account_please_ignore_this_email') } |
||||
`,
|
||||
html: ` |
||||
<p>${ t('Here_is_your_authentication_code') }</p> |
||||
<p style="font-size: 30px;"> |
||||
<b>__code__</b> |
||||
</p> |
||||
<p>${ t('Do_not_provide_this_code_to_anyone') }</p> |
||||
<p>${ t('If_you_didnt_try_to_login_in_your_account_please_ignore_this_email') }</p> |
||||
`,
|
||||
}); |
||||
} |
||||
|
||||
public verify(user: IUser, codeFromEmail: string): boolean { |
||||
if (!this.isEnabled(user)) { |
||||
return false; |
||||
} |
||||
|
||||
if (!user.services || !Array.isArray(user.services?.emailCode)) { |
||||
return false; |
||||
} |
||||
|
||||
// Remove non digits
|
||||
codeFromEmail = codeFromEmail.replace(/([^\d])/g, ''); |
||||
|
||||
Users.removeExpiredEmailCodesOfUserId(user._id); |
||||
|
||||
const valid = user.services.emailCode.find(({ code, expire }) => { |
||||
if (expire < new Date()) { |
||||
return false; |
||||
} |
||||
|
||||
if (bcrypt.compareSync(codeFromEmail, code)) { |
||||
Users.removeEmailCodeByUserIdAndCode(user._id, code); |
||||
return true; |
||||
} |
||||
|
||||
return false; |
||||
}); |
||||
|
||||
return !!valid; |
||||
} |
||||
|
||||
public sendEmailCode(user: IUser): string[] { |
||||
const emails = this.getUserVerifiedEmails(user); |
||||
const random = Random._randomString(6, '0123456789'); |
||||
const encryptedRandom = bcrypt.hashSync(random, Accounts._bcryptRounds()); |
||||
const expire = new Date(); |
||||
const expirationInSeconds = parseInt(settings.get('Accounts_TwoFactorAuthentication_By_Email_Code_Expiration')); |
||||
|
||||
expire.setSeconds(expire.getSeconds() + expirationInSeconds); |
||||
|
||||
Users.addEmailCodeByUserId(user._id, encryptedRandom, expire); |
||||
|
||||
for (const address of emails) { |
||||
this.send2FAEmail(address, random, user); |
||||
} |
||||
|
||||
return emails; |
||||
} |
||||
|
||||
public processInvalidCode(user: IUser): IProcessInvalidCodeResult { |
||||
Users.removeExpiredEmailCodesOfUserId(user._id); |
||||
|
||||
// Generate new code if the there isn't any code with more than 5 minutes to expire
|
||||
const expireWithDelta = new Date(); |
||||
expireWithDelta.setMinutes(expireWithDelta.getMinutes() - 5); |
||||
|
||||
const hasValidCode = user.services?.emailCode?.filter(({ expire }) => expire > expireWithDelta); |
||||
|
||||
if (hasValidCode?.length) { |
||||
return { |
||||
codeGenerated: false, |
||||
codeCount: hasValidCode.length, |
||||
codeExpires: hasValidCode.map((i) => i.expire), |
||||
}; |
||||
} |
||||
|
||||
this.sendEmailCode(user); |
||||
|
||||
return { |
||||
codeGenerated: true, |
||||
}; |
||||
} |
||||
} |
||||
@ -0,0 +1,17 @@ |
||||
import { IUser } from '../../../../definition/IUser'; |
||||
|
||||
export interface IProcessInvalidCodeResult { |
||||
codeGenerated: boolean; |
||||
codeCount?: number; |
||||
codeExpires?: Date[]; |
||||
} |
||||
|
||||
export interface ICodeCheck { |
||||
readonly name: string; |
||||
|
||||
isEnabled(user: IUser): boolean; |
||||
|
||||
verify(user: IUser, code: string): boolean; |
||||
|
||||
processInvalidCode(user: IUser): IProcessInvalidCodeResult; |
||||
} |
||||
@ -0,0 +1,38 @@ |
||||
import { Accounts } from 'meteor/accounts-base'; |
||||
|
||||
import { ICodeCheck, IProcessInvalidCodeResult } from './ICodeCheck'; |
||||
import { IUser } from '../../../../definition/IUser'; |
||||
|
||||
export class PasswordCheckFallback implements ICodeCheck { |
||||
public readonly name = 'password'; |
||||
|
||||
public isEnabled(user: IUser): boolean { |
||||
// TODO: Uncomment for version 4.0 forcing the
|
||||
// password fallback for who has password set.
|
||||
// return user.services?.password?.bcrypt != null;
|
||||
return !user; |
||||
} |
||||
|
||||
public verify(user: IUser, code: string): boolean { |
||||
if (!this.isEnabled(user)) { |
||||
return false; |
||||
} |
||||
|
||||
const passCheck = Accounts._checkPassword(user, { |
||||
digest: code.toLowerCase(), |
||||
algorithm: 'sha-256', |
||||
}); |
||||
|
||||
if (passCheck.error) { |
||||
return false; |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
public processInvalidCode(): IProcessInvalidCodeResult { |
||||
return { |
||||
codeGenerated: false, |
||||
}; |
||||
} |
||||
} |
||||
@ -0,0 +1,36 @@ |
||||
import { TOTP } from '../lib/totp'; |
||||
import { IUser } from '../../../../definition/IUser'; |
||||
import { settings } from '../../../settings/server'; |
||||
import { ICodeCheck, IProcessInvalidCodeResult } from './ICodeCheck'; |
||||
|
||||
export class TOTPCheck implements ICodeCheck { |
||||
public readonly name = 'totp'; |
||||
|
||||
public isEnabled(user: IUser): boolean { |
||||
if (!settings.get('Accounts_TwoFactorAuthentication_Enabled')) { |
||||
return false; |
||||
} |
||||
|
||||
return user.services?.totp?.enabled === true; |
||||
} |
||||
|
||||
public verify(user: IUser, code: string): boolean { |
||||
if (!this.isEnabled(user)) { |
||||
return false; |
||||
} |
||||
|
||||
return TOTP.verify({ |
||||
secret: user.services?.totp?.secret, |
||||
token: code, |
||||
userId: user._id, |
||||
backupTokens: user.services?.totp?.hashedBackup, |
||||
}); |
||||
} |
||||
|
||||
public processInvalidCode(): IProcessInvalidCodeResult { |
||||
// Nothing to do
|
||||
return { |
||||
codeGenerated: false, |
||||
}; |
||||
} |
||||
} |
||||
@ -0,0 +1,156 @@ |
||||
import crypto from 'crypto'; |
||||
|
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Accounts } from 'meteor/accounts-base'; |
||||
|
||||
import { settings } from '../../../settings/server'; |
||||
import { TOTPCheck } from './TOTPCheck'; |
||||
import { EmailCheck } from './EmailCheck'; |
||||
import { PasswordCheckFallback } from './PasswordCheckFallback'; |
||||
import { IUser } from '../../../../definition/IUser'; |
||||
import { ICodeCheck } from './ICodeCheck'; |
||||
import { Users } from '../../../models/server'; |
||||
import { IMethodConnection } from '../../../../definition/IMethodThisType'; |
||||
|
||||
export interface ITwoFactorOptions { |
||||
disablePasswordFallback?: boolean; |
||||
disableRememberMe?: boolean; |
||||
} |
||||
|
||||
export const totpCheck = new TOTPCheck(); |
||||
export const emailCheck = new EmailCheck(); |
||||
export const passwordCheckFallback = new PasswordCheckFallback(); |
||||
|
||||
export const checkMethods = new Map<string, ICodeCheck>(); |
||||
|
||||
checkMethods.set(totpCheck.name, totpCheck); |
||||
checkMethods.set(emailCheck.name, emailCheck); |
||||
|
||||
export function getMethodByNameOrFirstActiveForUser(user: IUser, name?: string): ICodeCheck | undefined { |
||||
if (name && checkMethods.has(name)) { |
||||
return checkMethods.get(name); |
||||
} |
||||
|
||||
return Array.from(checkMethods.values()).find((method) => method.isEnabled(user)); |
||||
} |
||||
|
||||
export function getAvailableMethodNames(user: IUser): string[] | [] { |
||||
return Array.from(checkMethods).filter(([, method]) => method.isEnabled(user)).map(([name]) => name) || []; |
||||
} |
||||
|
||||
export function getUserForCheck(userId: string): IUser { |
||||
return Users.findOneById(userId, { |
||||
fields: { |
||||
emails: 1, |
||||
language: 1, |
||||
'services.totp': 1, |
||||
'services.email2fa': 1, |
||||
'services.emailCode': 1, |
||||
'services.password': 1, |
||||
'services.resume.loginTokens': 1, |
||||
}, |
||||
}); |
||||
} |
||||
|
||||
export function getFingerprintFromConnection(connection: IMethodConnection): string { |
||||
const data = JSON.stringify({ |
||||
userAgent: connection.httpHeaders['user-agent'], |
||||
clientAddress: connection.clientAddress, |
||||
}); |
||||
|
||||
return crypto.createHash('md5').update(data).digest('hex'); |
||||
} |
||||
|
||||
export function isAuthorizedForToken(connection: IMethodConnection, user: IUser, options: ITwoFactorOptions): boolean { |
||||
const currentToken = Accounts._getLoginToken(connection.id); |
||||
const tokenObject = user.services?.resume?.loginTokens?.find((i) => i.hashedToken === currentToken); |
||||
|
||||
if (!tokenObject) { |
||||
return false; |
||||
} |
||||
|
||||
if (tokenObject.bypassTwoFactor === true) { |
||||
return true; |
||||
} |
||||
|
||||
if (options.disableRememberMe === true) { |
||||
return false; |
||||
} |
||||
|
||||
if (!tokenObject.twoFactorAuthorizedUntil || !tokenObject.twoFactorAuthorizedHash) { |
||||
return false; |
||||
} |
||||
|
||||
if (tokenObject.twoFactorAuthorizedUntil < new Date()) { |
||||
return false; |
||||
} |
||||
|
||||
if (tokenObject.twoFactorAuthorizedHash !== getFingerprintFromConnection(connection)) { |
||||
return false; |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
export function rememberAuthorization(connection: IMethodConnection, user: IUser): void { |
||||
const currentToken = Accounts._getLoginToken(connection.id); |
||||
|
||||
const rememberFor = parseInt(settings.get('Accounts_TwoFactorAuthentication_RememberFor')); |
||||
|
||||
if (rememberFor <= 0) { |
||||
return; |
||||
} |
||||
|
||||
const expires = new Date(); |
||||
expires.setSeconds(expires.getSeconds() + rememberFor); |
||||
|
||||
Users.setTwoFactorAuthorizationHashAndUntilForUserIdAndToken(user._id, currentToken, getFingerprintFromConnection(connection), expires); |
||||
} |
||||
|
||||
interface ICheckCodeForUser { |
||||
user: IUser | string; |
||||
code?: string; |
||||
method?: string; |
||||
options?: ITwoFactorOptions; |
||||
connection?: IMethodConnection; |
||||
} |
||||
|
||||
function _checkCodeForUser({ user, code, method, options = {}, connection }: ICheckCodeForUser): boolean { |
||||
if (typeof user === 'string') { |
||||
user = getUserForCheck(user); |
||||
} |
||||
|
||||
if (connection && isAuthorizedForToken(connection, user, options)) { |
||||
return true; |
||||
} |
||||
|
||||
let selectedMethod = getMethodByNameOrFirstActiveForUser(user, method); |
||||
|
||||
if (!selectedMethod) { |
||||
if (options.disablePasswordFallback || !passwordCheckFallback.isEnabled(user)) { |
||||
return true; |
||||
} |
||||
selectedMethod = passwordCheckFallback; |
||||
} |
||||
|
||||
if (!code) { |
||||
const data = selectedMethod.processInvalidCode(user); |
||||
const availableMethods = getAvailableMethodNames(user); |
||||
|
||||
throw new Meteor.Error('totp-required', 'TOTP Required', { method: selectedMethod.name, ...data, availableMethods }); |
||||
} |
||||
|
||||
const valid = selectedMethod.verify(user, code); |
||||
|
||||
if (!valid) { |
||||
throw new Meteor.Error('totp-invalid', 'TOTP Invalid', { method: selectedMethod.name }); |
||||
} |
||||
|
||||
if (options.disableRememberMe !== true && connection) { |
||||
rememberAuthorization(connection, user); |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
export const checkCodeForUser = process.env.TEST_MODE ? (): boolean => true : _checkCodeForUser; |
||||
@ -0,0 +1,36 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
import { checkCodeForUser, ITwoFactorOptions } from './code/index'; |
||||
import { IMethodThisType } from '../../../definition/IMethodThisType'; |
||||
|
||||
export function twoFactorRequired(fn: Function, options: ITwoFactorOptions): Function { |
||||
return function(this: IMethodThisType, ...args: any[]): any { |
||||
if (!this.userId) { |
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'twoFactorRequired' }); |
||||
} |
||||
|
||||
// get two factor options from last item of args and remove it
|
||||
const twoFactor = args.pop(); |
||||
if (twoFactor) { |
||||
if (twoFactor.twoFactorCode && twoFactor.twoFactorMethod) { |
||||
checkCodeForUser({ |
||||
user: this.userId, |
||||
connection: this.connection || undefined, |
||||
code: twoFactor.twoFactorCode, |
||||
method: twoFactor.twoFactorMethod, |
||||
options, |
||||
}); |
||||
this.twoFactorChecked = true; |
||||
} else { |
||||
// if it was not two factor options, put it back
|
||||
args.push(twoFactor); |
||||
} |
||||
} |
||||
|
||||
if (!this.twoFactorChecked) { |
||||
checkCodeForUser({ user: this.userId, connection: this.connection || undefined, options }); |
||||
} |
||||
|
||||
return fn.apply(this, args); |
||||
}; |
||||
} |
||||
@ -0,0 +1,3 @@ |
||||
export namespace settings { |
||||
export function get(name: string): string; |
||||
} |
||||
@ -0,0 +1,16 @@ |
||||
export interface IMethodConnection { |
||||
id: string; |
||||
close: Function; |
||||
onClose: Function; |
||||
clientAddress: string; |
||||
httpHeaders: Record<string, any>; |
||||
} |
||||
|
||||
export interface IMethodThisType { |
||||
isSimulation: boolean; |
||||
userId: string | null; |
||||
connection: IMethodConnection | null; |
||||
setUserId(userId: string): void; |
||||
unblock(): void; |
||||
twoFactorChecked: boolean | undefined; |
||||
} |
||||
@ -0,0 +1,5 @@ |
||||
import { IMethodThisType } from './IMethodThisType'; |
||||
|
||||
export interface IMethodType { |
||||
[key: string]: (this: IMethodThisType, ...args: any[]) => any; |
||||
} |
||||
@ -0,0 +1,100 @@ |
||||
export interface ILoginToken { |
||||
hashedToken: string; |
||||
twoFactorAuthorizedUntil?: Date; |
||||
twoFactorAuthorizedHash?: string; |
||||
} |
||||
|
||||
export interface IMeteorLoginToken extends ILoginToken { |
||||
when: Date; |
||||
} |
||||
|
||||
export interface IPersonalAccessToken extends ILoginToken { |
||||
type: 'personalAccessToken'; |
||||
createdAt: Date; |
||||
lastTokenPart: string; |
||||
name?: string; |
||||
bypassTwoFactor?: boolean; |
||||
} |
||||
|
||||
export interface IUserEmailVerificationToken { |
||||
token: string; |
||||
address: string; |
||||
when: Date; |
||||
} |
||||
|
||||
export interface IUserEmailCode { |
||||
code: string; |
||||
expire: Date; |
||||
} |
||||
|
||||
type LoginToken = ILoginToken & IPersonalAccessToken; |
||||
|
||||
export interface IUserServices { |
||||
password?: { |
||||
bcrypt: string; |
||||
}; |
||||
email?: { |
||||
verificationTokens?: IUserEmailVerificationToken[]; |
||||
}; |
||||
resume?: { |
||||
loginTokens?: LoginToken[]; |
||||
}; |
||||
google?: any; |
||||
facebook?: any; |
||||
github?: any; |
||||
totp?: { |
||||
enabled: boolean; |
||||
hashedBackup: string[]; |
||||
secret: string; |
||||
}; |
||||
email2fa?: { |
||||
enabled: boolean; |
||||
changedAt: Date; |
||||
}; |
||||
emailCode: IUserEmailCode[]; |
||||
} |
||||
|
||||
export interface IUserEmail { |
||||
address: string; |
||||
verified: boolean; |
||||
} |
||||
|
||||
export interface IUserSettings { |
||||
profile: any; |
||||
preferences: { |
||||
[key: string]: any; |
||||
}; |
||||
} |
||||
|
||||
export interface IUser { |
||||
_id: string; |
||||
createdAt: Date; |
||||
roles: string[]; |
||||
type: string; |
||||
active: boolean; |
||||
username?: string; |
||||
name?: string; |
||||
services?: IUserServices; |
||||
emails?: IUserEmail[]; |
||||
status?: string; |
||||
statusConnection?: string; |
||||
lastLogin?: Date; |
||||
avatarOrigin?: string; |
||||
utcOffset?: number; |
||||
language?: string; |
||||
statusDefault?: string; |
||||
oauth?: { |
||||
authorizedClients: string[]; |
||||
}; |
||||
_updatedAt?: Date; |
||||
statusLivechat?: string; |
||||
e2e?: { |
||||
private_key: string; |
||||
public_key: string; |
||||
}; |
||||
requirePasswordChange?: boolean; |
||||
customFields?: { |
||||
[key: string]: any; |
||||
}; |
||||
settings?: IUserSettings; |
||||
} |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,40 @@ |
||||
/* eslint-disable @typescript-eslint/interface-name-prefix */ |
||||
declare module 'meteor/random' { |
||||
namespace Random { |
||||
function _randomString(numberOfChars: number, map: string): string; |
||||
} |
||||
} |
||||
|
||||
declare module 'meteor/accounts-base' { |
||||
namespace Accounts { |
||||
function _bcryptRounds(): number; |
||||
|
||||
function _getLoginToken(connectionId: string): string | undefined; |
||||
} |
||||
} |
||||
|
||||
declare module 'meteor/meteor' { |
||||
type globalError = Error; |
||||
namespace Meteor { |
||||
interface ErrorStatic { |
||||
new (error: string | number, reason?: string, details?: any): Error; |
||||
} |
||||
interface Error extends globalError { |
||||
error: string | number; |
||||
reason?: string; |
||||
details?: any; |
||||
} |
||||
|
||||
const server: any; |
||||
|
||||
interface MethodThisType { |
||||
twoFactorChecked: boolean | undefined; |
||||
} |
||||
} |
||||
} |
||||
|
||||
declare module 'meteor/rocketchat:tap-i18n' { |
||||
namespace TAPi18n { |
||||
function __(s: string, options: { lng: string }): string; |
||||
} |
||||
} |
||||
@ -0,0 +1,16 @@ |
||||
import { Migrations } from '../../../app/migrations'; |
||||
import { Settings } from '../../../app/models/server'; |
||||
|
||||
Migrations.add({ |
||||
version: 177, |
||||
up() { |
||||
// Disable auto opt in for existent installations
|
||||
Settings.upsert({ |
||||
_id: 'Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In', |
||||
}, { |
||||
$set: { |
||||
value: false, |
||||
}, |
||||
}); |
||||
}, |
||||
}); |
||||
Loading…
Reference in new issue