Source/Account.js

"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