"use strict";
/**
* @module Account
* @overview This module defines the `Account` class that models a Magic Batua account.
*
* @author Animesh Mishra <hello@animesh.ltd>
* @copyright © 2018 Animesh Ltd. All Rights Reserved.
*/
Object.defineProperty(exports, "__esModule", { value: true });
const Crypto = require("crypto");
const bson_1 = require("bson");
const Source_1 = require("./Source");
const Points = require("@magic.batua/points");
/**
* The `Account` class models the data and functions supported by Magic Batua
* accounts. Besides the standard profile information — name, email, phone, etc. —
* it also takes care of things such as referrals and loyalty points.
*
* Business logic used for password resets, salt generation, hashing and other
* such operations are also defined by the `Account` class.
*/
class Account {
/**
* In a server-side application, there are two instantiation scenarios: one where the input
* is provided by the client, and the other where the input is provided by the database.
*
* Client-side input is always lighter than database-side input, since auto-generated
* properties such as `_id` are missing in the client-side variant. Moreover, client-side
* input may contain less information than database-side input as some sensitive properties
* such as password, salt, etc. are never revealed to the client side.
*
* As such, we need a way to track the source of input and proceed with instantiation
* accordingly. This `source` parameter serves exactly this purpose.
*
* @param {any} json Input JSON
* @param {Source} source Input source. See `Source/Source.ts` for definition of `Source`.
*
* @class
*/
constructor(json, source = Source_1.Source.Database) {
/**
* Flag used to soft-delete an account. Accounts are never deleted permanently. This
* is for administrative and data analytics reasons. When a user submits a account
* deletion request, this flag is set to true and the `recoverBy` date is set to 14
* days ahead.
*
* If a user logs into their account within those 14 days, the deletion hold is lifted
* and this flag is set to `false` again.
*/
this.isDeleted = false;
this.name = json.name;
this.phone = json.phone;
this.email = json.email;
// Source is client only for signup requests. So we will need to
// hash the password and generate a referral code for the new
// account.
if (source == Source_1.Source.Client) {
this._id = new bson_1.ObjectId();
this.password = this.hash(json.password);
this.referralCode = this.generateReferralCode();
// Issue points for signup and initialise the ledger
let signup = {
points: Points.Points.ToSelfFor("Signup"),
type: "Issue",
notes: "Signup"
};
this.pointsLedger = new Points.Ledger([signup]);
}
else {
this._id = json._id;
this.isEmailVerified = json.isEmailVerified;
this.isPhoneVerified = json.isPhoneVerified;
this.referralCode = json.referralCode;
this.referredBy = json.referredBy;
this.referrals = json.referrals;
this.pointsLedger = new Points.Ledger(json.pointsLedger.transactions);
this.password = json.password;
this.salt = json.salt;
this.isDeleted = json.isDeleted;
this.recoverBy = json.recoverBy;
this.otp = json.otp;
}
}
/**
* Salts the given `password` using the `salt` used at account creation,
* and then compares the hash with the hashed password stored in the
* database.
*
* @param {string} password Client-provided password
*
* @returns {boolean} `true` if the password is correct, otherwise `false`
*
* @function CanAuthenticateUsing
* @memberof Account
* @instance
*/
CanAuthenticateUsing(password) {
return this.password == this.hash(password, this.salt);
}
/**
* Sets `referredBy` to the `_id` of the user who referred this user to
* Magic Batua.
*
* @param {string} id User `_id` of the referrer
*
* @function SetReferrer
* @memberof Account
* @instance
*/
SetReferrer(id) {
this.referredBy = id;
}
/**
* Adds a referral to the account's referrals list
*
* @param {string} id User `_id` of the referral
*
* @function AddReferral
* @memberof Account
* @instance
*/
AddReferral(id) {
if (!this.referrals) {
this.referrals = new Array();
}
this.referrals.push(id);
}
/**
* Adds loyalty points to the account.
*
* @param {number} points Number of points to be awarded
* @param {string} reason Reason for the generosity
*
* @function AddPoints
* @memberof Account
* @instance
*/
AddPoints(points, reason) {
this.pointsLedger.Issue(points, reason);
}
/**
* Redeems the given number of points. Throws an error if available
* points are fewer than the `points` requested.
*
* @param {number} points Numbe of points to be redeemed
* @param {string} reason Purpose of redemption
*
* @function RedeemPoints
* @memberof Account
* @instance
*/
RedeemPoints(points, reason) {
this.pointsLedger.Redeem(points, reason);
}
/**
* Removes a referral from the account's referral list. This is used
* only when a referral decides to permanently delete their account.
*
* @param {string} id User `_id` of the referral to be removed
*
* @function RemoveReferral
* @memberof Account
* @instance
*/
RemoveReferral(id) {
if (this.referrals) {
this.referrals = this.referrals.filter(element => element != id);
}
}
/**
* Changes the existing account password to `newPass`. This also changes the
* `salt` used before hashing.
*
* @param {string} newPass New password
* @returns A tuple of the form (salt, newPassword)
*
* @function ResetPassword
* @memberof Account
* @instance
*/
ResetPassword(newPass) {
this.password = this.hash(newPass);
}
/**
* Generates a random 6-digit number and stores it in database as a verification code.
*
* @function SetOTP
* @memberof Account
* @instance
*/
SetOTP() {
this.otp = Math.floor(Math.random() * 900000) + 100000;
}
/**
* Sets the verification code `otp` to `undefined` after a successful verification
*
* @function UnsetOTP
* @memberof Account
* @instance
*/
UnsetOTP() {
this.otp = undefined;
}
/**
* Puts the account into a 14-day deletion hold by setting `isDeleted` to `true`
* and setting the account `recoverBy` date to 14-days from now.
*
* @function Delete
* @memberof Account
* @instance
*/
Delete() {
this.isDeleted = true;
const millisecondsInAFortnight = 1209600000;
this.recoverBy = Date.now() + millisecondsInAFortnight;
}
/**
* Removes the deletion hold on the account by setting `isDeleted` to `false` and
* setting the account `recoverBy` date to `undefined`.
*
* @function Undelete
* @memberof Account
* @instance
*/
Undelete() {
this.isDeleted = false;
this.recoverBy = undefined;
}
/**
* Exports the `Account` object to a shareable JSON string. Used to compose
* HTTP response bodies. Prevents sensitive information such as password, salt etc.
* from leaking onto client-side.
*
* @returns {string} A stringified, sanitised version of the `Account` instance
*
* @function Export
* @memberof Account
* @instance
*/
Export() {
// Copy the object so changes made here don't affect `this` object
let exportable = JSON.parse(JSON.stringify(this));
// Convert ObjectId to string
exportable._id = exportable._id.toString();
// Remove sensitive information
delete exportable.otp;
delete exportable.password;
delete exportable.salt;
delete exportable.isDeleted;
delete exportable.recoverBy;
return JSON.stringify(exportable);
}
//
// Private methods
//
/**
* Storing plain-text password is poor security practice. When client sends a plain-text password,
* it must be salted, i.e. a random bytes must be attached to it, and then hashed to make it harder
* for hackers to hack into someone's account. This method takes in a plaintext UTF-8 password and
* returns a salted and hashed Base64 string which is written to the database.
*
* @param {string} password Plaintext to be hashed
* @param {string} salt A Base64 string denoting the random data to be attached to the password.
* If a value is not provided, one is generated randomly at runtime using
* Node's `Crypto.randomBytes()`.
*
* @returns {string} A salted and hashed Base64 string
*
* @function hash
* @memberof Account
* @private
* @instance
*/
hash(password, salt = Crypto.randomBytes(128).toString("base64")) {
this.salt = salt;
return Crypto.pbkdf2Sync(password, salt, 5000, 512, "sha512").toString("base64");
}
/**
* Generates a unique referral code using Node's `crypto` module.
*
* @returns {string} The referral code
*
* @function generateReferralCode
* @memberof Account
* @private
* @instance
*
*/
generateReferralCode() {
return Crypto.randomBytes(5).toString("hex").toLowerCase();
}
//
// Static methods
//
/**
* Changes user's password. The newPassword uses a unique salt and the salted
* string is hashed for better security.
*
* @param {string} password The new password
*
* @returns {any} A tuple of the form (salt, newPassword)
*
* @function ResetPassword
* @memberof Account
* @private
* @static
*/
static ResetPassword(newPassword) {
let salt = Crypto.randomBytes(128).toString("base64");
let hashed = Crypto.pbkdf2Sync(newPassword, salt, 5000, 512, "sha512").toString("base64");
return [salt, hashed];
}
}
exports.Account = Account;
//# sourceMappingURL=Account.js.map