UNPKG

@magic.batua/account

Version:

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

394 lines (373 loc) 18.1 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>index.js - Postman Documentation</title> <script src="scripts/prettify/prettify.js"></script> <script src="scripts/prettify/lang-css.js"></script> <!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <link type="text/css" rel="stylesheet" href="styles/ionicons.min.css"> <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css"> <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css"> </head> <body> <input type="checkbox" id="nav-trigger" class="nav-trigger" /> <label for="nav-trigger" class="navicon-button x"> <div class="navicon"></div> </label> <label for="nav-trigger" class="overlay"></label> <nav> <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Account.html">Account</a><ul class='methods'><li data-type='method'><a href="Account.html#AddPoints">AddPoints</a></li><li data-type='method'><a href="Account.html#AddReferral">AddReferral</a></li><li data-type='method'><a href="Account.html#CanAuthenticateUsing">CanAuthenticateUsing</a></li><li data-type='method'><a href="Account.html#Delete">Delete</a></li><li data-type='method'><a href="Account.html#Export">Export</a></li><li data-type='method'><a href="Account.html#RedeemPoints">RedeemPoints</a></li><li data-type='method'><a href="Account.html#RemoveReferral">RemoveReferral</a></li><li data-type='method'><a href="Account.html#ResetPassword">ResetPassword</a></li><li data-type='method'><a href="Account.html#SetOTP">SetOTP</a></li><li data-type='method'><a href="Account.html#SetReferrer">SetReferrer</a></li><li data-type='method'><a href="Account.html#Undelete">Undelete</a></li><li data-type='method'><a href="Account.html#UnsetOTP">UnsetOTP</a></li></ul></li><li><a href="Registry.html">Registry</a><ul class='methods'><li data-type='method'><a href="Registry.html#Create">Create</a></li><li data-type='method'><a href="Registry.html#DidResetPassword">DidResetPassword</a></li><li data-type='method'><a href="Registry.html#DidSendOTP">DidSendOTP</a></li><li data-type='method'><a href="Registry.html#HasVerified">HasVerified</a></li><li data-type='method'><a href="Registry.html#IsDuplicate">IsDuplicate</a></li><li data-type='method'><a href="Registry.html#Modify">Modify</a></li><li data-type='method'><a href="Registry.html#Remove">Remove</a></li><li data-type='method'><a href="Registry.html#Retrieve">Retrieve</a></li></ul></li></ul><h3>Modules</h3><ul><li><a href="module-Database.html">Database</a><ul class='methods'><li data-type='method'><a href="module-Database.html#~Find">Find</a></li><li data-type='method'><a href="module-Database.html#~FindAndReplace">FindAndReplace</a></li><li data-type='method'><a href="module-Database.html#~GetAccountByID">GetAccountByID</a></li><li data-type='method'><a href="module-Database.html#~GetReferrer">GetReferrer</a></li><li data-type='method'><a href="module-Database.html#~Insert">Insert</a></li><li data-type='method'><a href="module-Database.html#~IsDuplicate">IsDuplicate</a></li><li data-type='method'><a href="module-Database.html#~UpdateInPlace">UpdateInPlace</a></li></ul></li><li><a href="module-Source.html">Source</a></li></ul> </nav> <div id="main"> <h1 class="page-title">index.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>"use strict"; /** * @module @magic.batua/account * @overview Defines the {@link Registry} class that provides the interface to consume * account management features provided by the {@link Account} class. * * @author Animesh Mishra &lt;hello@animesh.ltd> * @copyright © 2018 Animesh Ltd. All Rights Reserved. */ Object.defineProperty(exports, "__esModule", { value: true }); const Database = require("./Source/Database"); const Account_1 = require("./Source/Account"); const error_1 = require("@magic.batua/error"); const error_2 = require("@magic.batua/error"); const Source_1 = require("./Source/Source"); const points_1 = require("@magic.batua/points"); /** * The `Registry` class provides the interface between the API server and the `Account` * module. This class defines the methods and interfaces responsible for new account * registration, login authentication, profile updates and account deletion/undeletion * requests. * * - See [Create()]{@link Registry#Create} to understand signup logic. * - See [Retrieve()]{@link Registry#Retrieve} to understand login logic. * - See [Modify()]{@link Registry#Modify} to understand update profile logic. * - See [Remove()]{@link Registry#Remove} to understand account deletion logic. * * * In addition to these functionalities, the `Registry` class also defines several utility * methods that carry out key responsibilities. [DidSendOTP()]{@link Registry#DidSendOTP} * and [HasVerified()]{@link Registry#HasVerified} ensure smooth OTP-based verification. * While [DidResetPassword()]{@link Registry#DidResetPassword} carries out the seemingly * straightforward but actually quite complex task of resetting a user's password. */ class Registry { /** * A `Registry` instance needs access to the messaging API and database for proper * functioning. This constructor initialises an instance with fully-configured * `Messaging` and `Mongo.Db` instances. * * @param {Mongo.Db} db A MongoDB database instance * @param {Messaging} messaging A `Messaging` instance as defined in `@magic.batua/messaging` package. * * @class */ constructor(db, messaging) { this.db = db; this.messaging = messaging; } /** * Checks whether the given account parameters already exists in our records. Used * to prevent duplicate registrations. * * @param {SignupQuery} input See `index.ts` for definition of `SignupQuery`. * * @function IsDuplicate * @memberof Registry * @instance */ async IsDuplicate(input) { let account = new Account_1.Account(input, Source_1.Source.Client); let isDuplicate = await Database.IsDuplicate(account, this.db); return isDuplicate; } /** * Registers a new Magic Batua account and returns a stringified version of the * new `Account` object. The registration process is as follows: * * 1. Initialise a new `Account` object using the given `input`. * 2. Check for duplicate account * 3. Send a verification SMS * 4. If an `inviteCode` is provided in the `input` query, find the referrer. * - Add a new referral to the `referrer` account and award them Magic Points for a referral. * 5. Issue Magic Points to the new account. * 6. Write the account to the database. * * @param {SignupQuery} input See `index.ts` for definition of `SignupQuery`. * * @returns A stringified version of the `Account` object * * @function Create * @memberof Registry * @instance * * @example * let registry = new Registry(...) * registry.Create({ * name: "Godzilla" * phone: 1234567890, * email: "god@zilla.com", * password: "Password", * inviteCode: "BigInJapan" // Optional * }) */ async Create(input) { // Initialise a new Account let newAccount = new Account_1.Account(input, Source_1.Source.Client); // Check for duplicate let isDuplicate = await Database.IsDuplicate(newAccount, this.db); if (isDuplicate) { throw new error_1.ClientError(error_2.Code.Conflict, "Phone number already exists.", ""); } // Send SMS newAccount.SetOTP(); await this.messaging.SendSMS({ recipient: newAccount.phone, text: "Your Magic Batua verification pin is " + newAccount.otp + "." }); // Find and update referrer (if any) if (input.inviteCode) { let referrer = await Database.GetReferrer(input.inviteCode, this.db); if (referrer == null) { throw new error_1.ClientError(error_2.Code.BadRequest, "Invalid invite code."); } referrer.AddReferral(newAccount._id.toHexString()); // Award points to referrer referrer.AddPoints(points_1.Points.ToReferrerFor("Signup"), "Referral"); await Database.FindAndReplace(referrer, this.db); newAccount.SetReferrer(referrer._id.toHexString()); } // Write to database newAccount = await Database.Insert(newAccount, this.db); return newAccount.Export(); } /** * Returns a stringified version of the `Account` object that matches the given `query`. * If the account requested had been marked for deletion earlier, and account `recoverBy` * date is in the future, the deletion hold on the account is lifted and the account is * restored to its former glory. * * There is no separate function to lift the deletion hold on an account. After requesting * a deletion, a user has 14 days to cancel it by logging back into their account. After * the 14th day, the account is soft-deleted and can't be recovered. * * @param {LoginQuery} query See `index.ts` for definition of `LoginQuery` * * @returns A stringified `Account` object * * @function Retrieve * @memberof Registry * @instance * * @example * let registry = new Registry(...) * registry.Retrieve({ * phone: "1234567890", * password: "Godzilla" * }) */ async Retrieve(query) { let account = await Database.Find(query.phone, this.db); if (account == null) { throw new error_1.ClientError(error_2.Code.NotFound, "Given mobile number isn't registered with us."); } if (account.CanAuthenticateUsing(query.password)) { if (!account.isDeleted) { return account.Export(); } else if (account.recoverBy &lt;= Date.now()) { throw new error_1.ClientError(error_2.Code.NotFound, "Given mobile number isn't registered with us."); } account.Undelete(); await Database.FindAndReplace(account, this.db); let updated = await Database.GetAccountByID(account._id.toHexString(), this.db); return updated.Export(); } else { throw new error_1.ClientError(error_2.Code.Unauthorised, "Incorrect phone number or password."); } } /** * Modifies profile information for the given account `_id` as instructed by the `query` * parameter. At the time of writing, only email, phone and name could be updated. For * changing/resetting password, use [DidResetPassword()]{@link Registry#DidResetPassword} * instead. * * **This method doesn't perform validation on input data. So you could very well set the * phone as "0000" and it wouldn't bat an eye. This should be improved in the next version.**. * * @param {string} id Magic Batua user `_id` * @param {any} query Key-value pairs to be updated * * @function Modify * @memberof Registry * @instance * * @example * let registry = new Registry(...) * registry.Modify("abcdefgh", { * phone: "1234567890", * name: "Godzilla" * }) */ async Modify(id, query) { // Check if the user account exists and is active let exists = await Database.GetAccountByID(id, this.db); if (exists == null) { throw new error_1.ClientError(error_2.Code.NotFound, "Given ID is not registered with us."); } else if (exists.isDeleted &amp;&amp; exists.recoverBy &lt;= Date.now()) { throw new error_1.ClientError(error_2.Code.NotFound, "Given ID is not registered with us."); } // User exists, but has been deleted and is within the 14-day account recovery period if (exists.isDeleted &amp;&amp; exists.recoverBy > Date.now()) { throw new error_1.ClientError(error_2.Code.Conflict, "This account is marked for deletion on " + new Date(exists.recoverBy) + "."); } // All checks passed, now we can update the database record await Database.UpdateInPlace(id, query, this.db); } /** * Puts the account with ID `_id` under a 14-day deletion hold. If the account owner * doesn't logs into their account within this 14-day period, the account is permanently * *soft-deleted* and can't be recovered. * * If a user does log in within the 14-day window, the deletion hold is lifted and the * account is restored back to normal. See [Retrieve()]{@link Registry#Retrieve} for * the logic that removes the deletion hold. * * @param {string} id `_id` of the user to be deleted * * @function Remove * @memberof Registry * @instance */ async Remove(id) { let account = await Database.GetAccountByID(id, this.db); if (account == null) { throw new error_1.ClientError(error_2.Code.NotFound, "No such records exist in our database."); } if (account.isDeleted) { throw new error_1.ClientError(error_2.Code.NotFound, "No such records exists in our database."); } if (account.referredBy) { let referrer = await Database.GetAccountByID(account.referredBy, this.db); if (referrer) { referrer.RemoveReferral(account._id.toHexString()); await Database.FindAndReplace(referrer, this.db); } } account.Delete(); await Database.FindAndReplace(account, this.db); } /** * Generates a random one-time verification pin and sends it to the given `phone` * number. The method is designed such that if the `phone` number is not registered * with us, the method will throw an error and refuse to send the SMS. * * This could be problematic in some cases, so if a solid reason can be found to remove * this caveat, you should edit out the part of code in the beginning of the method. * * @param {string} phone A mobile number registered with us. * * @returns `true` if the SMS was sent successfully, otherwise throws an error. * * @function DidSendOTP * @memberof Registry * @instance */ async DidSendOTP(phone) { // See if the phone number exists in the records let account = await Database.Find(phone, this.db); if (account == null) { throw new error_1.ClientError(error_2.Code.NotFound, "The given phone number doesn't exist in our records."); } // Send verification SMS account.SetOTP(); await this.messaging.SendSMS({ recipient: account.phone, text: `Your Magic Batua verification pin is ${account.otp}.` }); // Update OTP in the database await Database.FindAndReplace(account, this.db); return true; } /** * Marks an account as verified if the given `pin` matches the one sent to the * account's registered mobile number. * * @param {string} phone Registered mobile number * @param {number} pin OTP sent for verification * * @returns `true` if verification is successful, otherwise throws an error. * * @function HasVerified * @memberof Registry * @instance */ async HasVerified(phone, pin) { // See if the phone number exists in the records let account = await Database.Find(phone, this.db); if (account == null) { throw new error_1.ClientError(error_2.Code.NotFound, "The given phone number doesn't exist in our records."); } if (account.otp != pin) { throw new error_1.ClientError(error_2.Code.Unauthorised, "Incorrect verification pin."); } // Pins match, set the phone as verified and unset the OTP account.UnsetOTP(); account.isPhoneVerified = true; await Database.FindAndReplace(account, this.db); return true; } /** * Before a user can submit a reset password request, they need to verify their * identity via a one-time pin sent to their registered mobile number. This method * expects that `pin` as well as the `newPassword` as the input. * * If OTP-verification succeeds, the `newPassword` is salted using a new randomly * generated salt and then hashed before being stored in the database. So in * effect, this method resets both the `salt` and the `password`. * * If OTP-verification fails, password is not reset and an error is thrown instead. * * @param {string} phone Registered mobile number * @param {string} newPass New password * @param {pin} pin OTP sent during verification * * @returns `true` if password reset is successful, otherwise throws an error. * * @function DidResetPassword * @memberof Registry * @instance */ async DidResetPassword(phone, newPass, pin) { if (await this.HasVerified(phone, pin)) { let account = await Database.Find(phone, this.db); account.ResetPassword(newPass); await Database.FindAndReplace(account, this.db); return true; } return false; } } exports.Registry = Registry; //# sourceMappingURL=index.js.map</code></pre> </article> </section> </div> <br class="clear"> <footer> Documentation generated at Mon Mar 19 2018 22:39:23 GMT+0530 (IST) </footer> <script>prettyPrint();</script> <script src="scripts/linenumber.js"></script> </body> </html>