UNPKG

@magic.batua/account

Version:

The Account modules powers the user account management features of the Magic Batua platform.

315 lines 11 kB
"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