/*
* @autor v.orel
*/
const UB = require('@unitybase/ub')
const Session = UB.Session
const totp = require('./modules/totp')
/* global uba_otp createGuid */
// eslint-disable-next-line camelcase
const me = uba_otp
/**
* Generate one-time-password (OTP) and store it into uba_otp
*
* @param {string} otpKind Must be one of 'EMail', 'SMS', 'TOTP'
* @param {number} [userID=Session.userID]
* @param {object} [uData='']
* @param {number} [lifeTime] life time of otp in seconds; Default 30 day for email, 30 min for SMS and 10 years for TOTP
* @returns {string}
* @memberOf uba_otp_ns.prototype
* @memberOfModule @unitybase/uba
* @public
*/
me.generateOtp = function (otpKind, userID, uData, lifeTime) {
let otp
userID = userID || Session.userID
if (otpKind === 'EMail') {
otp = createGuid()
if (!lifeTime) lifeTime = 30 * 24 * 60 * 60 // 30 days
} else if (otpKind === 'SMS') {
otp = (Math.random() * 1000000 >>> 0).toString(10).padStart(6, '0') // 6 digits random number
if (!lifeTime) lifeTime = 20 * 60 // 30 minutes
} else if (otpKind === 'TOTP') {
return doGenerateTOTPSecret(userID)
} else {
throw new Error('invalid otpKind')
}
const expiredDate = new Date()
expiredDate.setTime(expiredDate.getTime() + lifeTime * 1000)
const uDataStr = uData ? JSON.stringify(uData) : ''
const store = UB.DataStore('uba_otp')
const res = store.run('insert', {
execParams: {
otp,
userID,
otpKind,
expiredDate,
uData: uDataStr
}
})
if (!res) {
throw store.lastError
}
return otp
}
/**
* Switch session to user from OTP (SMS or EMail) or execute callback in session of user from OTP.
* For TOTP use verifyTotp function.
*
* @param {string} otp
* @param {string} otpKind
* @param {Function} [fCheckUData] function for check OTP from uData
* @param {object} [checkData] value for check OTP from uData
* @param {Function} [call] If defined then this function will be called in user's session and restore original user session after call
* @returns {boolean}
* @function auth
* @deprecated use authAndExecute instead
* @memberOf uba_otp_ns.prototype
* @memberOfModule @unitybase/uba
* @public
*/
me.auth = function (otp, otpKind, fCheckUData, checkData, call) {
const repo = UB.Repository('uba_otp').attrs(['userID', 'ID', 'uData'])
.where('[otp]', '=', otp).where('[expiredDate]', '>=', new Date())
.whereIf(otpKind, '[otpKind]', '=', otpKind)
const inst = repo.select()
if (inst.eof) return false
if (otpKind !== 'TOTP') {
const res = inst.run('delete', {
execParams: { ID: inst.get('ID') }
})
if (!res) throw inst.lastError
}
if ((!fCheckUData) || (fCheckUData(inst.get('uData'), checkData))) {
if (call) {
Session.runAsUser(inst.get('userID'), call.bind(null, inst.get('uData')))
} else {
Session.setUser(inst.get('userID'))
}
return true
} else {
return false
}
}
/**
* Verify TOTP for currently logged-in user
*
* @param {string} totpValue TOTP value entered by user (6 digits string)
* @param {number} [userID] optional user ID to verify TOTP for. By default - Session.userID
* @function verifyTotp
* @memberOf uba_otp_ns.prototype
* @memberOfModule @unitybase/uba
* @returns {boolean}
*/
me.verifyTotp = function (totpValue, userID) {
const secret = UB.Repository('uba_otp').attrs('otp')
.where('userID', '=', userID || Session.userID)
.where('[expiredDate]', '>=', new Date())
.where('[otpKind]', '=', 'TOTP')
.selectScalar()
if (!secret) return false
return totp.verifyTotp(secret, totpValue)
}
/**
* Check given otp, and in case it is correct run callback
*
* @example
// generation otp
var userID = 100000000122,
uData = {size: {width: 100, height: 50}};
var otp = uba_otp.generateOtp('EMail', userID, uData);
// send this otp via EMail
//............................
// after receiving this otp
var isOtpCorrect = uba_otp.authAndExecute('EMail', otp, function(uData){
var params = JSON.parse(uData);
console.log('user ID is', Session.userID);//'user ID is 100000000122';
console.log('width is', params.width);//'width is 100';
}))
* @param {string} otp
* @param {string} otpKind
* @param {Function} callBack This function will be called in user's session with uData parameter from otp if otp is correct.
* @returns {boolean} Is otp correct
* @function authAndExecute
* @memberOf uba_otp_ns.prototype
* @memberOfModule @unitybase/uba
* @public
*/
me.authAndExecute = function (otp, otpKind, callBack) {
const store = UB.Repository('uba_otp').attrs(['userID', 'ID', 'uData'])
.where('[otp]', '=', otp).where('[otpKind]', '=', otpKind)
.where('[userID.disabled]', '=', 0)
.where('[expiredDate]', '>=', new Date())
.select()
if (store.eof) return false
store.run('delete', { execParams: { ID: store.get('ID') } })
Session.runAsUser(store.get('userID'), callBack.bind(null, store.get('uData')))
return true
}
/**
* Generate TOTP secret for user and store it into uba_otp
* In case secret already generated return existed secret
*
* @param {number} userID
* @returns {string}
* @private
*/
function doGenerateTOTPSecret (userID) {
let secret = UB.Repository('uba_otp').attrs('otp')
.where('userID', '=', userID)
.where('otpKind', '=', 'TOTP')
.selectScalar()
if (secret) return secret
const store = UB.DataStore('uba_otp')
secret = totp.generateTotpSecret()
const lifeTime = 365 * 10 * 24 * 60 * 60 // 10 years
const expiredDate = new Date()
expiredDate.setTime(expiredDate.getTime() + lifeTime * 1000)
store.run('insert', {
execParams: {
otp: secret,
userID,
otpKind: 'TOTP',
expiredDate,
uData: ''
}
})
return secret
}