From 5e5ff1a25e1ef8072e7d0bc0db3b801f586d4407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padr=C3=B3n?= Date: Tue, 18 Dec 2018 21:44:00 -0400 Subject: [PATCH] eslint config --- .eslintignore | 17 + .eslintrc.json | 18 + backend/.eslintrc | 93 -- backend/Gruntfile.js | 2 +- backend/api/controllers/TodosController.js | 2 - backend/api/controllers/account/logout.js | 30 +- .../account/update-billing-card.js | 98 +- .../controllers/account/update-password.js | 22 +- .../api/controllers/account/update-profile.js | 142 +- .../account/view-account-overview.js | 21 +- .../controllers/account/view-edit-password.js | 17 +- .../controllers/account/view-edit-profile.js | 17 +- .../api/controllers/dashboard/view-welcome.js | 21 +- .../deliver-contact-form-message.js | 44 +- .../api/controllers/entrance/confirm-email.js | 76 +- backend/api/controllers/entrance/login.js | 44 +- .../entrance/send-password-recovery-email.js | 45 +- backend/api/controllers/entrance/signup.js | 113 +- .../entrance/update-password-and-login.js | 49 +- .../entrance/view-forgot-password.js | 26 +- .../api/controllers/entrance/view-login.js | 23 +- .../controllers/entrance/view-new-password.js | 39 +- .../api/controllers/entrance/view-signup.js | 23 +- .../controllers/view-homepage-or-redirect.js | 24 +- backend/api/helpers/send-template-email.js | 290 ++-- backend/api/hooks/custom/index.js | 210 ++- backend/api/models/Todos.js | 11 +- backend/api/models/User.js | 141 +- backend/api/policies/is-logged-in.js | 4 +- backend/api/policies/is-super-admin.js | 6 +- backend/api/responses/expired.js | 13 +- backend/api/responses/unauthorized.js | 14 +- backend/app.js | 2 + backend/scripts/rebuild-cloud-sdk.js | 81 +- backend/tasks/pipeline.js | 9 +- frontend/components/todo-editor.js | 122 +- frontend/components/todo-list.js | 196 ++- frontend/index.html | 160 +- frontend/scripts.js | 39 +- package-lock.json | 1386 +++++++++++++++++ package.json | 27 + 41 files changed, 2507 insertions(+), 1210 deletions(-) create mode 100644 .eslintignore create mode 100644 .eslintrc.json delete mode 100644 backend/.eslintrc create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..993c846 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,17 @@ +/frontend/vue.js +/frontend/app.js +*~ +*# +.DS_STORE +.netbeans +nbproject +.idea +.node_history +dump.rdb + +npm-debug.log +lib-cov +*.seed +*.log +*.out +*.pid \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..7b730f0 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": "standard", + "globals": { + "localStorage": true, + "alert": true, + "sails": true, + "node": true, + "User": true, + "_": true, + "Vue": true + }, + "rules": { + "quotes": [2, "double"], + "semi": [2, "always"], + "no-inner-declarations": 0, + "no-throw-literal": 0 + } +} diff --git a/backend/.eslintrc b/backend/.eslintrc deleted file mode 100644 index 60ac3f0..0000000 --- a/backend/.eslintrc +++ /dev/null @@ -1,93 +0,0 @@ -{ - // ╔═╗╔═╗╦ ╦╔╗╔╔╦╗┬─┐┌─┐ - // ║╣ ╚═╗║ ║║║║ ║ ├┬┘│ - // o╚═╝╚═╝╩═╝╩╝╚╝ ╩ ┴└─└─┘ - // A set of basic code conventions designed to encourage quality and consistency - // across your Sails app's code base. These rules are checked against - // automatically any time you run `npm test`. - // - // > An additional eslintrc override file is included in the `assets/` folder - // > right out of the box. This is specifically to allow for variations in acceptable - // > global variables between front-end JavaScript code designed to run in the browser - // > vs. backend code designed to run in a Node.js/Sails process. - // - // > Note: If you're using mocha, you'll want to add an extra override file to your - // > `test/` folder so that eslint will tolerate mocha-specific globals like `before` - // > and `describe`. - // Designed for ESLint v4. - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // For more information about any of the rules below, check out the relevant - // reference page on eslint.org. For example, to get details on "no-sequences", - // you would visit `http://eslint.org/docs/rules/no-sequences`. If you're unsure - // or could use some advice, come by https://sailsjs.com/support. - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "env": { - "node": true - }, - - "parserOptions": { - "ecmaVersion": 8, - "ecmaFeatures": { - "experimentalObjectRestSpread": true - } - }, - - "globals": { - // If "no-undef" is enabled below, be sure to list all global variables that - // are used in this app's backend code (including the globalIds of models): - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "Promise": true, - "sails": true, - "_": true, - - // Models: - "User": true - - // …and any others. - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - }, - - "rules": { - "block-scoped-var": ["error"], - "callback-return": ["error", ["done", "proceed", "next", "onwards", "callback", "cb"]], - "camelcase": ["warn", {"properties":"always"}], - "comma-style": ["warn", "last"], - "curly": ["warn"], - "eqeqeq": ["error", "always"], - "eol-last": ["warn"], - "handle-callback-err": ["error"], - "indent": ["warn", 2, { - "SwitchCase": 1, - "MemberExpression": "off", - "FunctionDeclaration": {"body":1, "parameters":"off"}, - "FunctionExpression": {"body":1, "parameters":"off"}, - "CallExpression": {"arguments":"off"}, - "ArrayExpression": 1, - "ObjectExpression": 1, - "ignoredNodes": ["ConditionalExpression"] - }], - "linebreak-style": ["error", "unix"], - "no-dupe-keys": ["error"], - "no-duplicate-case": ["error"], - "no-extra-semi": ["warn"], - "no-labels": ["error"], - "no-mixed-spaces-and-tabs": [2, "smart-tabs"], - "no-redeclare": ["warn"], - "no-return-assign": ["error", "always"], - "no-sequences": ["error"], - "no-trailing-spaces": ["warn"], - "no-undef": ["error"], - "no-unexpected-multiline": ["warn"], - "no-unreachable": ["warn"], - "no-unused-vars": ["warn", {"caughtErrors":"all", "caughtErrorsIgnorePattern": "^unused($|[A-Z].*$)", "argsIgnorePattern": "^unused($|[A-Z].*$)", "varsIgnorePattern": "^unused($|[A-Z].*$)" }], - "no-use-before-define": ["error", {"functions":false}], - "one-var": ["warn", "never"], - "prefer-arrow-callback": ["warn", {"allowNamedFunctions":true}], - "quotes": ["warn", "single", {"avoidEscape":false, "allowTemplateLiterals":true}], - "semi": ["warn", "always"], - "semi-spacing": ["warn", {"before":false, "after":true}], - "semi-style": ["warn", "last"] - } - -} diff --git a/backend/Gruntfile.js b/backend/Gruntfile.js index e3b2847..37d51ff 100644 --- a/backend/Gruntfile.js +++ b/backend/Gruntfile.js @@ -12,7 +12,7 @@ * For more information see: * https://sailsjs.com/anatomy/Gruntfile.js */ -module.exports = function(grunt) { +module.exports = function (grunt) { var loadGruntTasks = require('sails-hook-grunt/accessible/load-grunt-tasks'); diff --git a/backend/api/controllers/TodosController.js b/backend/api/controllers/TodosController.js index 41d8a78..1478049 100644 --- a/backend/api/controllers/TodosController.js +++ b/backend/api/controllers/TodosController.js @@ -6,7 +6,5 @@ */ module.exports = { - }; - diff --git a/backend/api/controllers/account/logout.js b/backend/api/controllers/account/logout.js index 5bc8269..5e607a1 100644 --- a/backend/api/controllers/account/logout.js +++ b/backend/api/controllers/account/logout.js @@ -1,14 +1,9 @@ module.exports = { + friendlyName: "Logout", + description: "Log out of this app.", - friendlyName: 'Logout', - - - description: 'Log out of this app.', - - - extendedDescription: -`This action deletes the \`req.session.userId\` key from the session of the requesting user agent. + extendedDescription: `This action deletes the \`req.session.userId\` key from the session of the requesting user agent. Actual garbage collection of session data depends on this app's session store, and potentially also on the [TTL configuration](https://sailsjs.com/docs/reference/configuration/sails-config-session) you provided for it. @@ -16,24 +11,20 @@ you provided for it. Note that this action does not check to see whether or not the requesting user was actually logged in. (If they weren't, then this action is just a no-op.)`, - exits: { - success: { - description: 'The requesting user agent has been successfully logged out.' + description: "The requesting user agent has been successfully logged out." }, redirect: { - description: 'The requesting user agent looks to be a web browser.', - extendedDescription: 'After logging out from a web browser, the user is redirected away.', - responseType: 'redirect' + description: "The requesting user agent looks to be a web browser.", + extendedDescription: + "After logging out from a web browser, the user is redirected away.", + responseType: "redirect" } - }, - fn: async function () { - // Clear the `userId` property from this session. delete this.req.session.userId; @@ -41,10 +32,7 @@ actually logged in. (If they weren't, then this action is just a no-op.)`, // > Under the covers, this persists the now-logged-out session back // > to the underlying session store. if (!this.req.wantsJSON) { - throw {redirect: '/login'}; + throw { redirect: "/login" }; } - } - - }; diff --git a/backend/api/controllers/account/update-billing-card.js b/backend/api/controllers/account/update-billing-card.js index 03053c0..bc07c17 100644 --- a/backend/api/controllers/account/update-billing-card.js +++ b/backend/api/controllers/account/update-billing-card.js @@ -1,79 +1,85 @@ module.exports = { + friendlyName: "Update billing card", - - friendlyName: 'Update billing card', - - - description: 'Update the credit card for the logged-in user.', - + description: "Update the credit card for the logged-in user.", inputs: { - stripeToken: { - type: 'string', - example: 'tok_199k3qEXw14QdSnRwmsK99MH', - description: 'The single-use Stripe Checkout token identifier representing the user\'s payment source (i.e. credit card.)', - extendedDescription: 'Omit this (or use "") to remove this user\'s payment source.', + type: "string", + example: "tok_199k3qEXw14QdSnRwmsK99MH", + description: + "The single-use Stripe Checkout token identifier representing the user's payment source (i.e. credit card.)", + extendedDescription: + "Omit this (or use \"\") to remove this user's payment source.", whereToGet: { - description: 'This Stripe.js token is provided to the front-end (client-side) code after completing a Stripe Checkout or Stripe Elements flow.' + description: + "This Stripe.js token is provided to the front-end (client-side) code after completing a Stripe Checkout or Stripe Elements flow." } }, billingCardLast4: { - type: 'string', - example: '4242', - description: 'Omit if removing card info.', - whereToGet: { description: 'Credit card info is provided by Stripe after completing the checkout flow.' } + type: "string", + example: "4242", + description: "Omit if removing card info.", + whereToGet: { + description: + "Credit card info is provided by Stripe after completing the checkout flow." + } }, billingCardBrand: { - type: 'string', - example: 'visa', - description: 'Omit if removing card info.', - whereToGet: { description: 'Credit card info is provided by Stripe after completing the checkout flow.' } + type: "string", + example: "visa", + description: "Omit if removing card info.", + whereToGet: { + description: + "Credit card info is provided by Stripe after completing the checkout flow." + } }, billingCardExpMonth: { - type: 'string', - example: '08', - description: 'Omit if removing card info.', - whereToGet: { description: 'Credit card info is provided by Stripe after completing the checkout flow.' } + type: "string", + example: "08", + description: "Omit if removing card info.", + whereToGet: { + description: + "Credit card info is provided by Stripe after completing the checkout flow." + } }, billingCardExpYear: { - type: 'string', - example: '2023', - description: 'Omit if removing card info.', - whereToGet: { description: 'Credit card info is provided by Stripe after completing the checkout flow.' } - }, - + type: "string", + example: "2023", + description: "Omit if removing card info.", + whereToGet: { + description: + "Credit card info is provided by Stripe after completing the checkout flow." + } + } }, - fn: async function (inputs) { - // Add, update, or remove the default payment source for the logged-in user's // customer entry in Stripe. - var stripeCustomerId = await sails.helpers.stripe.saveBillingInfo.with({ - stripeCustomerId: this.req.me.stripeCustomerId, - token: inputs.stripeToken || '', - }).timeout(5000).retry(); + var stripeCustomerId = await sails.helpers.stripe.saveBillingInfo + .with({ + stripeCustomerId: this.req.me.stripeCustomerId, + token: inputs.stripeToken || "" + }) + .timeout(5000) + .retry(); // Update (or clear) the card info we have stored for this user in our database. // > Remember, never store complete card numbers-- only the last 4 digits + expiration! // > Storing (or even receiving) complete, unencrypted card numbers would require PCI // > compliance in the U.S. - await User.updateOne({ id: this.req.me.id }) - .set({ + await User.updateOne({ id: this.req.me.id }).set({ stripeCustomerId, - hasBillingCard: inputs.stripeToken ? true : false, - billingCardBrand: inputs.stripeToken ? inputs.billingCardBrand : '', - billingCardLast4: inputs.stripeToken ? inputs.billingCardLast4 : '', - billingCardExpMonth: inputs.stripeToken ? inputs.billingCardExpMonth : '', - billingCardExpYear: inputs.stripeToken ? inputs.billingCardExpYear : '' + hasBillingCard: !!inputs.stripeToken, + billingCardBrand: inputs.stripeToken ? inputs.billingCardBrand : "", + billingCardLast4: inputs.stripeToken ? inputs.billingCardLast4 : "", + billingCardExpMonth: inputs.stripeToken ? inputs.billingCardExpMonth : "", + billingCardExpYear: inputs.stripeToken ? inputs.billingCardExpYear : "" }); - } - - }; diff --git a/backend/api/controllers/account/update-password.js b/backend/api/controllers/account/update-password.js index f6c6af9..59abee7 100644 --- a/backend/api/controllers/account/update-password.js +++ b/backend/api/controllers/account/update-password.js @@ -1,35 +1,23 @@ module.exports = { + friendlyName: "Update password", - - friendlyName: 'Update password', - - - description: 'Update the password for the logged-in user.', - + description: "Update the password for the logged-in user.", inputs: { - password: { - description: 'The new, unencrypted password.', - example: 'abc123v2', + description: "The new, unencrypted password.", + example: "abc123v2", required: true } - }, - fn: async function (inputs) { - // Hash the new password. var hashed = await sails.helpers.passwords.hashPassword(inputs.password); // Update the record for the logged-in user. - await User.updateOne({ id: this.req.me.id }) - .set({ + await User.updateOne({ id: this.req.me.id }).set({ password: hashed }); - } - - }; diff --git a/backend/api/controllers/account/update-profile.js b/backend/api/controllers/account/update-profile.js index f6adea7..6cfcc6c 100644 --- a/backend/api/controllers/account/update-profile.js +++ b/backend/api/controllers/account/update-profile.js @@ -1,37 +1,26 @@ module.exports = { + friendlyName: "Update profile", - - friendlyName: 'Update profile', - - - description: 'Update the profile for the logged-in user.', - + description: "Update the profile for the logged-in user.", inputs: { - fullName: { - type: 'string' + type: "string" }, emailAddress: { - type: 'string' - }, - + type: "string" + } }, - exits: { - emailAlreadyInUse: { statusCode: 409, - description: 'The provided email address is already in use.', - }, - + description: "The provided email address is already in use." + } }, - fn: async function (inputs) { - var newEmailAddress = inputs.emailAddress; if (newEmailAddress !== undefined) { newEmailAddress = newEmailAddress.toLowerCase(); @@ -40,26 +29,41 @@ module.exports = { // Determine if this request wants to change the current user's email address, // revert her pending email address change, modify her pending email address // change, or if the email address won't be affected at all. - var desiredEmailEffect;// ('change-immediately', 'begin-change', 'cancel-pending-change', 'modify-pending-change', or '') + var desiredEmailEffect; // ('change-immediately', 'begin-change', 'cancel-pending-change', 'modify-pending-change', or '') if ( newEmailAddress === undefined || - (this.req.me.emailStatus !== 'change-requested' && newEmailAddress === this.req.me.emailAddress) || - (this.req.me.emailStatus === 'change-requested' && newEmailAddress === this.req.me.emailChangeCandidate) + (this.req.me.emailStatus !== "change-requested" && + newEmailAddress === this.req.me.emailAddress) || + (this.req.me.emailStatus === "change-requested" && + newEmailAddress === this.req.me.emailChangeCandidate) ) { - desiredEmailEffect = ''; - } else if (this.req.me.emailStatus === 'change-requested' && newEmailAddress === this.req.me.emailAddress) { - desiredEmailEffect = 'cancel-pending-change'; - } else if (this.req.me.emailStatus === 'change-requested' && newEmailAddress !== this.req.me.emailAddress) { - desiredEmailEffect = 'modify-pending-change'; - } else if (!sails.config.custom.verifyEmailAddresses || this.req.me.emailStatus === 'unconfirmed') { - desiredEmailEffect = 'change-immediately'; + desiredEmailEffect = ""; + } else if ( + this.req.me.emailStatus === "change-requested" && + newEmailAddress === this.req.me.emailAddress + ) { + desiredEmailEffect = "cancel-pending-change"; + } else if ( + this.req.me.emailStatus === "change-requested" && + newEmailAddress !== this.req.me.emailAddress + ) { + desiredEmailEffect = "modify-pending-change"; + } else if ( + !sails.config.custom.verifyEmailAddresses || + this.req.me.emailStatus === "unconfirmed" + ) { + desiredEmailEffect = "change-immediately"; } else { - desiredEmailEffect = 'begin-change'; + desiredEmailEffect = "begin-change"; } - // If the email address is changing, make sure it is not already being used. - if (_.contains(['begin-change', 'change-immediately', 'modify-pending-change'], desiredEmailEffect)) { + if ( + _.contains( + ["begin-change", "change-immediately", "modify-pending-change"], + desiredEmailEffect + ) + ) { let conflictingUser = await User.findOne({ or: [ { emailAddress: newEmailAddress }, @@ -67,48 +71,50 @@ module.exports = { ] }); if (conflictingUser) { - throw 'emailAlreadyInUse'; + throw "emailAlreadyInUse"; } } - // Start building the values to set in the db. // (We always set the fullName if provided.) var valuesToSet = { - fullName: inputs.fullName, + fullName: inputs.fullName }; switch (desiredEmailEffect) { - // Change now - case 'change-immediately': + case "change-immediately": _.extend(valuesToSet, { emailAddress: newEmailAddress, - emailChangeCandidate: '', - emailProofToken: '', + emailChangeCandidate: "", + emailProofToken: "", emailProofTokenExpiresAt: 0, - emailStatus: this.req.me.emailStatus === 'unconfirmed' ? 'unconfirmed' : 'confirmed' + emailStatus: + this.req.me.emailStatus === "unconfirmed" + ? "unconfirmed" + : "confirmed" }); break; // Begin new email change, or modify a pending email change - case 'begin-change': - case 'modify-pending-change': + case "begin-change": + case "modify-pending-change": _.extend(valuesToSet, { emailChangeCandidate: newEmailAddress, - emailProofToken: await sails.helpers.strings.random('url-friendly'), - emailProofTokenExpiresAt: Date.now() + sails.config.custom.emailProofTokenTTL, - emailStatus: 'change-requested' + emailProofToken: await sails.helpers.strings.random("url-friendly"), + emailProofTokenExpiresAt: + Date.now() + sails.config.custom.emailProofTokenTTL, + emailStatus: "change-requested" }); break; // Cancel pending email change - case 'cancel-pending-change': + case "cancel-pending-change": _.extend(valuesToSet, { - emailChangeCandidate: '', - emailProofToken: '', + emailChangeCandidate: "", + emailProofToken: "", emailProofTokenExpiresAt: 0, - emailStatus: 'confirmed' + emailStatus: "confirmed" }); break; @@ -116,8 +122,7 @@ module.exports = { } // Save to the db - await User.updateOne({id: this.req.me.id }) - .set(valuesToSet); + await User.updateOne({ id: this.req.me.id }).set(valuesToSet); // If this is an immediate change, and billing features are enabled, // then also update the billing email for this user's linked customer entry @@ -126,15 +131,20 @@ module.exports = { // > then one will be set up implicitly, so we'll need to persist it to our // > database. (This could happen if Stripe credentials were not configured // > at the time this user was originally created.) - if(desiredEmailEffect === 'change-immediately' && sails.config.custom.enableBillingFeatures) { - let didNotAlreadyHaveCustomerId = (! this.req.me.stripeCustomerId); - let stripeCustomerId = await sails.helpers.stripe.saveBillingInfo.with({ - stripeCustomerId: this.req.me.stripeCustomerId, - emailAddress: newEmailAddress - }).timeout(5000).retry(); - if (didNotAlreadyHaveCustomerId){ - await User.updateOne({ id: this.req.me.id }) - .set({ + if ( + desiredEmailEffect === "change-immediately" && + sails.config.custom.enableBillingFeatures + ) { + let didNotAlreadyHaveCustomerId = !this.req.me.stripeCustomerId; + let stripeCustomerId = await sails.helpers.stripe.saveBillingInfo + .with({ + stripeCustomerId: this.req.me.stripeCustomerId, + emailAddress: newEmailAddress + }) + .timeout(5000) + .retry(); + if (didNotAlreadyHaveCustomerId) { + await User.updateOne({ id: this.req.me.id }).set({ stripeCustomerId }); } @@ -142,19 +152,19 @@ module.exports = { // If an email address change was requested, and re-confirmation is required, // send the "confirm account" email. - if (desiredEmailEffect === 'begin-change' || desiredEmailEffect === 'modify-pending-change') { + if ( + desiredEmailEffect === "begin-change" || + desiredEmailEffect === "modify-pending-change" + ) { await sails.helpers.sendTemplateEmail.with({ to: newEmailAddress, - subject: 'Your account has been updated', - template: 'email-verify-new-email', + subject: "Your account has been updated", + template: "email-verify-new-email", templateData: { - fullName: inputs.fullName||this.req.me.fullName, + fullName: inputs.fullName || this.req.me.fullName, token: valuesToSet.emailProofToken } }); } - } - - }; diff --git a/backend/api/controllers/account/view-account-overview.js b/backend/api/controllers/account/view-account-overview.js index f5841f9..6429b1a 100644 --- a/backend/api/controllers/account/view-account-overview.js +++ b/backend/api/controllers/account/view-account-overview.js @@ -1,30 +1,21 @@ module.exports = { + friendlyName: "View account overview", - - friendlyName: 'View account overview', - - - description: 'Display "Account Overview" page.', - + description: "Display \"Account Overview\" page.", exits: { - success: { - viewTemplatePath: 'pages/account/account-overview', + viewTemplatePath: "pages/account/account-overview" } - }, - fn: async function () { - // If billing features are enabled, include our configured Stripe.js // public key in the view locals. Otherwise, leave it as undefined. return { - stripePublishableKey: sails.config.custom.enableBillingFeatures? sails.config.custom.stripePublishableKey : undefined, + stripePublishableKey: sails.config.custom.enableBillingFeatures + ? sails.config.custom.stripePublishableKey + : undefined }; - } - - }; diff --git a/backend/api/controllers/account/view-edit-password.js b/backend/api/controllers/account/view-edit-password.js index 208a1d6..e61ef34 100644 --- a/backend/api/controllers/account/view-edit-password.js +++ b/backend/api/controllers/account/view-edit-password.js @@ -1,26 +1,15 @@ module.exports = { + friendlyName: "View edit password", - - friendlyName: 'View edit password', - - - description: 'Display "Edit password" page.', - + description: "Display \"Edit password\" page.", exits: { - success: { - viewTemplatePath: 'pages/account/edit-password' + viewTemplatePath: "pages/account/edit-password" } - }, - fn: async function () { - return {}; - } - - }; diff --git a/backend/api/controllers/account/view-edit-profile.js b/backend/api/controllers/account/view-edit-profile.js index baea0f7..2261f0f 100644 --- a/backend/api/controllers/account/view-edit-profile.js +++ b/backend/api/controllers/account/view-edit-profile.js @@ -1,26 +1,15 @@ module.exports = { + friendlyName: "View edit profile", - - friendlyName: 'View edit profile', - - - description: 'Display "Edit profile" page.', - + description: "Display \"Edit profile\" page.", exits: { - success: { - viewTemplatePath: 'pages/account/edit-profile', + viewTemplatePath: "pages/account/edit-profile" } - }, - fn: async function () { - return {}; - } - - }; diff --git a/backend/api/controllers/dashboard/view-welcome.js b/backend/api/controllers/dashboard/view-welcome.js index 989a7f1..ec2da90 100644 --- a/backend/api/controllers/dashboard/view-welcome.js +++ b/backend/api/controllers/dashboard/view-welcome.js @@ -1,27 +1,16 @@ module.exports = { + friendlyName: "View welcome page", - - friendlyName: 'View welcome page', - - - description: 'Display the dashboard "Welcome" page.', - + description: "Display the dashboard \"Welcome\" page.", exits: { - success: { - viewTemplatePath: 'pages/dashboard/welcome', - description: 'Display the welcome page for authenticated users.' - }, - + viewTemplatePath: "pages/dashboard/welcome", + description: "Display the welcome page for authenticated users." + } }, - fn: async function () { - return {}; - } - - }; diff --git a/backend/api/controllers/deliver-contact-form-message.js b/backend/api/controllers/deliver-contact-form-message.js index d31a7f2..9acacdd 100644 --- a/backend/api/controllers/deliver-contact-form-message.js +++ b/backend/api/controllers/deliver-contact-form-message.js @@ -1,58 +1,52 @@ module.exports = { + friendlyName: "Deliver contact form message", - friendlyName: 'Deliver contact form message', - - - description: 'Deliver a contact form message to the appropriate internal channel(s).', - + description: "Deliver a contact form message to the appropriate internal channel(s).", inputs: { emailAddress: { required: true, - type: 'string', - description: 'A return email address where we can respond.', - example: 'hermione@hogwarts.edu' + type: "string", + description: "A return email address where we can respond.", + example: "hermione@hogwarts.edu" }, topic: { required: true, - type: 'string', - description: 'The topic from the contact form.', - example: 'I want to buy stuff.' + type: "string", + description: "The topic from the contact form.", + example: "I want to buy stuff." }, fullName: { required: true, - type: 'string', - description: 'The full name of the human sending this message.', - example: 'Hermione Granger' + type: "string", + description: "The full name of the human sending this message.", + example: "Hermione Granger" }, message: { required: true, - type: 'string', - description: 'The custom message, in plain text.' + type: "string", + description: "The custom message, in plain text." } }, - exits: { success: { - description: 'The message was sent successfully.' + description: "The message was sent successfully." } }, - - fn: async function(inputs) { - + fn: async function (inputs) { if (!sails.config.custom.internalEmailAddress) { throw new Error( -`Cannot deliver incoming message from contact form because there is no internal + `Cannot deliver incoming message from contact form because there is no internal email address (\`sails.config.custom.internalEmailAddress\`) configured for this app. To enable contact form emails, you'll need to add this missing setting to your custom config -- usually in \`config/custom.js\`, \`config/staging.js\`, @@ -62,8 +56,8 @@ your custom config -- usually in \`config/custom.js\`, \`config/staging.js\`, await sails.helpers.sendTemplateEmail.with({ to: sails.config.custom.internalEmailAddress, - subject: 'New contact form message', - template: 'internal/email-contact-form', + subject: "New contact form message", + template: "internal/email-contact-form", layout: false, templateData: { contactName: inputs.fullName, @@ -72,8 +66,6 @@ your custom config -- usually in \`config/custom.js\`, \`config/staging.js\`, message: inputs.message } }); - } - }; diff --git a/backend/api/controllers/entrance/confirm-email.js b/backend/api/controllers/entrance/confirm-email.js index 37ec949..d40cfc0 100644 --- a/backend/api/controllers/entrance/confirm-email.js +++ b/backend/api/controllers/entrance/confirm-email.js @@ -1,56 +1,50 @@ module.exports = { - - friendlyName: 'Confirm email', - + friendlyName: "Confirm email", description: `Confirm a new user's email address, or an existing user's request for an email address change, then redirect to either a special landing page (for newly-signed up users), or the account page (for existing users who just changed their email address).`, - inputs: { token: { - description: 'The confirmation token from the email.', - example: '4-32fad81jdaf$329' + description: "The confirmation token from the email.", + example: "4-32fad81jdaf$329" } }, - exits: { success: { - description: 'Email address confirmed and requesting user logged in.' + description: "Email address confirmed and requesting user logged in." }, redirect: { - description: 'Email address confirmed and requesting user logged in. Since this looks like a browser, redirecting...', - responseType: 'redirect' + description: "Email address confirmed and requesting user logged in. Since this looks like a browser, redirecting...", + responseType: "redirect" }, invalidOrExpiredToken: { - responseType: 'expired', - description: 'The provided token is expired, invalid, or already used up.', + responseType: "expired", + description: "The provided token is expired, invalid, or already used up." }, emailAddressNoLongerAvailable: { statusCode: 409, - viewTemplatePath: '500', - description: 'The email address is no longer available.', - extendedDescription: 'This is an edge case that is not always anticipated by websites and APIs. Since it is pretty rare, the 500 server error page is used as a simple catch-all. If this becomes important in the future, this could easily be expanded into a custom error page or resolution flow. But for context: this behavior of showing the 500 server error page mimics how popular apps like Slack behave under the same circumstances.', + viewTemplatePath: "500", + description: "The email address is no longer available.", + extendedDescription: "This is an edge case that is not always anticipated by websites and APIs. Since it is pretty rare, the 500 server error page is used as a simple catch-all. If this becomes important in the future, this could easily be expanded into a custom error page or resolution flow. But for context: this behavior of showing the 500 server error page mimics how popular apps like Slack behave under the same circumstances." } }, - fn: async function (inputs) { - // If no token was provided, this is automatically invalid. if (!inputs.token) { - throw 'invalidOrExpiredToken'; + throw "invalidOrExpiredToken"; } // Get the user with the matching email token. @@ -58,10 +52,10 @@ then redirect to either a special landing page (for newly-signed up users), or t // If no such user exists, or their token is expired, bail. if (!user || user.emailProofTokenExpiresAt <= Date.now()) { - throw 'invalidOrExpiredToken'; + throw "invalidOrExpiredToken"; } - if (user.emailStatus === 'unconfirmed') { + if (user.emailStatus === "unconfirmed") { // ┌─┐┌─┐┌┐┌┌─┐┬┬─┐┌┬┐┬┌┐┌┌─┐ ╔═╗╦╦═╗╔═╗╔╦╗ ╔╦╗╦╔╦╗╔═╗ ╦ ╦╔═╗╔═╗╦═╗ ┌─┐┌┬┐┌─┐┬┬ // │ │ ││││├┤ │├┬┘││││││││ ┬ ╠╣ ║╠╦╝╚═╗ ║───║ ║║║║║╣ ║ ║╚═╗║╣ ╠╦╝ ├┤ │││├─┤││ // └─┘└─┘┘└┘└ ┴┴└─┴ ┴┴┘└┘└─┘ ╚ ╩╩╚═╚═╝ ╩ ╩ ╩╩ ╩╚═╝ ╚═╝╚═╝╚═╝╩╚═ └─┘┴ ┴┴ ┴┴┴─┘ @@ -70,23 +64,22 @@ then redirect to either a special landing page (for newly-signed up users), or t // store their user id in the session (just in case they aren't logged // in already), and then redirect them to the "email confirmed" page. await User.updateOne({ id: user.id }).set({ - emailStatus: 'confirmed', - emailProofToken: '', + emailStatus: "confirmed", + emailProofToken: "", emailProofTokenExpiresAt: 0 }); this.req.session.userId = user.id; if (this.req.wantsJSON) { - return; + return false; } else { - throw { redirect: '/email/confirmed' }; + throw { redirect: "/email/confirmed" }; } - - } else if (user.emailStatus === 'change-requested') { + } else if (user.emailStatus === "change-requested") { // ┌─┐┌─┐┌┐┌┌─┐┬┬─┐┌┬┐┬┌┐┌┌─┐ ╔═╗╦ ╦╔═╗╔╗╔╔═╗╔═╗╔╦╗ ┌─┐┌┬┐┌─┐┬┬ // │ │ ││││├┤ │├┬┘││││││││ ┬ ║ ╠═╣╠═╣║║║║ ╦║╣ ║║ ├┤ │││├─┤││ // └─┘└─┘┘└┘└ ┴┴└─┴ ┴┴┘└┘└─┘ ╚═╝╩ ╩╩ ╩╝╚╝╚═╝╚═╝═╩╝ └─┘┴ ┴┴ ┴┴┴─┘ - if (!user.emailChangeCandidate){ + if (!user.emailChangeCandidate) { throw new Error(`Consistency violation: Could not update Stripe customer because this user record's emailChangeCandidate ("${user.emailChangeCandidate}") is missing. (This should never happen.)`); } @@ -96,7 +89,7 @@ then redirect to either a special landing page (for newly-signed up users), or t // last checked its availability. (This is a relatively rare edge case-- // see exit description.) if (await User.count({ emailAddress: user.emailChangeCandidate }) > 0) { - throw 'emailAddressNoLongerAvailable'; + throw "emailAddressNoLongerAvailable"; } // If billing features are enabled, also update the billing email for this @@ -106,13 +99,13 @@ then redirect to either a special landing page (for newly-signed up users), or t // > then one will be set up implicitly, so we'll need to persist it to our // > database. (This could happen if Stripe credentials were not configured // > at the time this user was originally created.) - if(sails.config.custom.enableBillingFeatures) { - let didNotAlreadyHaveCustomerId = (! user.stripeCustomerId); + if (sails.config.custom.enableBillingFeatures) { + let didNotAlreadyHaveCustomerId = (!user.stripeCustomerId); let stripeCustomerId = await sails.helpers.stripe.saveBillingInfo.with({ stripeCustomerId: user.stripeCustomerId, emailAddress: user.emailChangeCandidate }).timeout(5000).retry(); - if (didNotAlreadyHaveCustomerId){ + if (didNotAlreadyHaveCustomerId) { await User.updateOne({ id: user.id }).set({ stripeCustomerId }); @@ -123,25 +116,22 @@ then redirect to either a special landing page (for newly-signed up users), or t // (just in case they aren't logged in already), then redirect them to // their "my account" page so they can see their updated email address. await User.updateOne({ id: user.id }) - .set({ - emailStatus: 'confirmed', - emailProofToken: '', - emailProofTokenExpiresAt: 0, - emailAddress: user.emailChangeCandidate, - emailChangeCandidate: '', - }); + .set({ + emailStatus: "confirmed", + emailProofToken: "", + emailProofTokenExpiresAt: 0, + emailAddress: user.emailChangeCandidate, + emailChangeCandidate: "" + }); this.req.session.userId = user.id; if (this.req.wantsJSON) { - return; + } else { - throw { redirect: '/account' }; + throw { redirect: "/account" }; } - } else { throw new Error(`Consistency violation: User ${user.id} has an email proof token, but somehow also has an emailStatus of "${user.emailStatus}"! (This should never happen.)`); } - } - }; diff --git a/backend/api/controllers/entrance/login.js b/backend/api/controllers/entrance/login.js index 59a32cd..4bd1d55 100644 --- a/backend/api/controllers/entrance/login.js +++ b/backend/api/controllers/entrance/login.js @@ -1,11 +1,8 @@ module.exports = { + friendlyName: "Login", - friendlyName: 'Login', - - - description: 'Log in using the provided email and password combination.', - + description: "Log in using the provided email and password combination.", extendedDescription: `This action attempts to look up the user record in the database with the @@ -13,36 +10,34 @@ specified email address. Then, if such a user exists, it uses bcrypt to compare the hashed password from the database with the provided password attempt.`, - inputs: { emailAddress: { - description: 'The email to try in this attempt, e.g. "irl@example.com".', - type: 'string', + description: "The email to try in this attempt, e.g. \"irl@example.com\".", + type: "string", required: true }, password: { - description: 'The unencrypted password to try in this attempt, e.g. "passwordlol".', - type: 'string', + description: "The unencrypted password to try in this attempt, e.g. \"passwordlol\".", + type: "string", required: true }, rememberMe: { - description: 'Whether to extend the lifetime of the user\'s session.', + description: "Whether to extend the lifetime of the user's session.", extendedDescription: `Note that this is NOT SUPPORTED when using virtual requests (e.g. sending requests over WebSockets instead of HTTP).`, - type: 'boolean' + type: "boolean" } }, - exits: { success: { - description: 'The requesting user agent has been successfully logged in.', + description: "The requesting user agent has been successfully logged in.", extendedDescription: `Under the covers, this stores the id of the logged-in user in the session as the \`userId\` key. The next time this user agent sends a request, assuming @@ -56,7 +51,7 @@ and exposed as \`req.me\`.)` badCombo: { description: `The provided email and password combination does not match any user in the database.`, - responseType: 'unauthorized' + responseType: "unauthorized" // ^This uses the custom `unauthorized` response located in `api/responses/unauthorized.js`. // To customize the generic "unauthorized" response across this entire app, change that file // (see api/responses/unauthorized). @@ -68,24 +63,22 @@ and exposed as \`req.me\`.)` }, - fn: async function (inputs) { - // Look up by the email address. // (note that we lowercase it to ensure the lookup is always case-insensitive, // regardless of which database we're using) var userRecord = await User.findOne({ - emailAddress: inputs.emailAddress.toLowerCase(), + emailAddress: inputs.emailAddress.toLowerCase() }); // If there was no matching user, respond thru the "badCombo" exit. - if(!userRecord) { - throw 'badCombo'; + if (!userRecord) { + throw "badCombo"; } // If the password doesn't match, then also exit thru "badCombo". await sails.helpers.passwords.checkPassword(inputs.password, userRecord.password) - .intercept('incorrect', 'badCombo'); + .intercept("incorrect", "badCombo"); // If "Remember Me" was enabled, then keep the session alive for // a longer amount of time. (This causes an updated "Set Cookie" @@ -95,19 +88,18 @@ and exposed as \`req.me\`.)` if (inputs.rememberMe) { if (this.req.isSocket) { sails.log.warn( - 'Received `rememberMe: true` from a virtual request, but it was ignored\n'+ - 'because a browser\'s session cookie cannot be reset over sockets.\n'+ - 'Please use a traditional HTTP request instead.' + "Received `rememberMe: true` from a virtual request, but it was ignored\n" + + "because a browser's session cookie cannot be reset over sockets.\n" + + "Please use a traditional HTTP request instead." ); } else { this.req.session.cookie.maxAge = sails.config.custom.rememberMeCookieMaxAge; } - }//fi + }// fi // Modify the active session instance. // (This will be persisted when the response is sent.) this.req.session.userId = userRecord.id; - } }; diff --git a/backend/api/controllers/entrance/send-password-recovery-email.js b/backend/api/controllers/entrance/send-password-recovery-email.js index 5ef1a2c..29de881 100644 --- a/backend/api/controllers/entrance/send-password-recovery-email.js +++ b/backend/api/controllers/entrance/send-password-recovery-email.js @@ -1,66 +1,55 @@ module.exports = { + friendlyName: "Send password recovery email", - - friendlyName: 'Send password recovery email', - - - description: 'Send a password recovery notification to the user with the specified email address.', - + description: + "Send a password recovery notification to the user with the specified email address.", inputs: { - emailAddress: { - description: 'The email address of the alleged user who wants to recover their password.', - example: 'rydahl@example.com', - type: 'string', + description: + "The email address of the alleged user who wants to recover their password.", + example: "rydahl@example.com", + type: "string", required: true } - }, - exits: { - success: { - description: 'The email address might have matched a user in the database. (If so, a recovery email was sent.)' - }, - + description: + "The email address might have matched a user in the database. (If so, a recovery email was sent.)" + } }, - fn: async function (inputs) { - // Find the record for this user. // (Even if no such user exists, pretend it worked to discourage sniffing.) var userRecord = await User.findOne({ emailAddress: inputs.emailAddress }); if (!userRecord) { return; - }//• + } // • // Come up with a pseudorandom, probabilistically-unique token for use // in our password recovery email. - var token = await sails.helpers.strings.random('url-friendly'); + var token = await sails.helpers.strings.random("url-friendly"); // Store the token on the user record // (This allows us to look up the user when the link from the email is clicked.) - await User.update({ id: userRecord.id }) - .set({ + await User.update({ id: userRecord.id }).set({ passwordResetToken: token, - passwordResetTokenExpiresAt: Date.now() + sails.config.custom.passwordResetTokenTTL, + passwordResetTokenExpiresAt: + Date.now() + sails.config.custom.passwordResetTokenTTL }); // Send recovery email await sails.helpers.sendTemplateEmail.with({ to: inputs.emailAddress, - subject: 'Password reset instructions', - template: 'email-reset-password', + subject: "Password reset instructions", + template: "email-reset-password", templateData: { fullName: userRecord.fullName, token: token } }); - } - - }; diff --git a/backend/api/controllers/entrance/signup.js b/backend/api/controllers/entrance/signup.js index 596b737..6f5f294 100644 --- a/backend/api/controllers/entrance/signup.js +++ b/backend/api/controllers/entrance/signup.js @@ -1,14 +1,9 @@ module.exports = { + friendlyName: "Signup", + description: "Sign up for a new user account.", - friendlyName: 'Signup', - - - description: 'Sign up for a new user account.', - - - extendedDescription: -`This creates a new user record in the database, signs in the requesting user agent + extendedDescription: `This creates a new user record in the database, signs in the requesting user agent by modifying its [session](https://sailsjs.com/documentation/concepts/sessions), and (if emailing with Mailgun is enabled) sends an account verification email. @@ -16,84 +11,90 @@ If a verification email is sent, the new user's account is put in an "unconfirme until they confirm they are using a legitimate email address (by clicking the link in the account verification message.)`, - inputs: { - emailAddress: { required: true, - type: 'string', + type: "string", isEmail: true, - description: 'The email address for the new account, e.g. m@example.com.', - extendedDescription: 'Must be a valid email address.', + description: "The email address for the new account, e.g. m@example.com.", + extendedDescription: "Must be a valid email address." }, password: { required: true, - type: 'string', + type: "string", maxLength: 200, - example: 'passwordlol', - description: 'The unencrypted password to use for the new account.' + example: "passwordlol", + description: "The unencrypted password to use for the new account." }, - fullName: { + fullName: { required: true, - type: 'string', - example: 'Frida Kahlo de Rivera', - description: 'The user\'s full name.', + type: "string", + example: "Frida Kahlo de Rivera", + description: "The user's full name." } - }, - exits: { - success: { - description: 'New user account was created successfully.' + description: "New user account was created successfully." }, invalid: { - responseType: 'badRequest', - description: 'The provided fullName, password and/or email address are invalid.', - extendedDescription: 'If this request was sent from a graphical user interface, the request '+ - 'parameters should have been validated/coerced _before_ they were sent.' + responseType: "badRequest", + description: + "The provided fullName, password and/or email address are invalid.", + extendedDescription: + "If this request was sent from a graphical user interface, the request " + + "parameters should have been validated/coerced _before_ they were sent." }, emailAlreadyInUse: { statusCode: 409, - description: 'The provided email address is already in use.', - }, - + description: "The provided email address is already in use." + } }, - fn: async function (inputs) { - var newEmailAddress = inputs.emailAddress.toLowerCase(); // Build up data for the new user record and save it to the database. // (Also use `fetch` to retrieve the new ID so that we can use it below.) - var newUserRecord = await User.create(_.extend({ - emailAddress: newEmailAddress, - password: await sails.helpers.passwords.hashPassword(inputs.password), - fullName: inputs.fullName, - tosAcceptedByIp: this.req.ip - }, sails.config.custom.verifyEmailAddresses? { - emailProofToken: await sails.helpers.strings.random('url-friendly'), - emailProofTokenExpiresAt: Date.now() + sails.config.custom.emailProofTokenTTL, - emailStatus: 'unconfirmed' - }:{})) - .intercept('E_UNIQUE', 'emailAlreadyInUse') - .intercept({name: 'UsageError'}, 'invalid') - .fetch(); + var newUserRecord = await User.create( + _.extend( + { + emailAddress: newEmailAddress, + password: await sails.helpers.passwords.hashPassword(inputs.password), + fullName: inputs.fullName, + tosAcceptedByIp: this.req.ip + }, + sails.config.custom.verifyEmailAddresses + ? { + emailProofToken: await sails.helpers.strings.random( + "url-friendly" + ), + emailProofTokenExpiresAt: + Date.now() + sails.config.custom.emailProofTokenTTL, + emailStatus: "unconfirmed" + } + : {} + ) + ) + .intercept("E_UNIQUE", "emailAlreadyInUse") + .intercept({ name: "UsageError" }, "invalid") + .fetch(); // If billing feaures are enabled, save a new customer entry in the Stripe API. // Then persist the Stripe customer id in the database. if (sails.config.custom.enableBillingFeatures) { - let stripeCustomerId = await sails.helpers.stripe.saveBillingInfo.with({ - emailAddress: newEmailAddress - }).timeout(5000).retry(); - await User.updateOne(newUserRecord.id) - .set({ + let stripeCustomerId = await sails.helpers.stripe.saveBillingInfo + .with({ + emailAddress: newEmailAddress + }) + .timeout(5000) + .retry(); + await User.updateOne(newUserRecord.id).set({ stripeCustomerId }); } @@ -105,17 +106,17 @@ the account verification message.)`, // Send "confirm account" email await sails.helpers.sendTemplateEmail.with({ to: newEmailAddress, - subject: 'Please confirm your account', - template: 'email-verify-account', + subject: "Please confirm your account", + template: "email-verify-account", templateData: { fullName: inputs.fullName, token: newUserRecord.emailProofToken } }); } else { - sails.log.info('Skipping new account email verification... (since `verifyEmailAddresses` is disabled)'); + sails.log.info( + "Skipping new account email verification... (since `verifyEmailAddresses` is disabled)" + ); } - } - }; diff --git a/backend/api/controllers/entrance/update-password-and-login.js b/backend/api/controllers/entrance/update-password-and-login.js index 3c60057..ddb92a8 100644 --- a/backend/api/controllers/entrance/update-password-and-login.js +++ b/backend/api/controllers/entrance/update-password-and-login.js @@ -1,48 +1,41 @@ module.exports = { + friendlyName: "Update password and login", - - friendlyName: 'Update password and login', - - - description: 'Finish the password recovery flow by setting the new password and '+ - 'logging in the requesting user, based on the authenticity of their token.', - + description: + "Finish the password recovery flow by setting the new password and " + + "logging in the requesting user, based on the authenticity of their token.", inputs: { - password: { - description: 'The new, unencrypted password.', - example: 'abc123v2', + description: "The new, unencrypted password.", + example: "abc123v2", required: true }, token: { - description: 'The password token that was generated by the `sendPasswordRecoveryEmail` endpoint.', - example: 'gwa8gs8hgw9h2g9hg29hgwh9asdgh9q34$$$$$asdgasdggds', + description: + "The password token that was generated by the `sendPasswordRecoveryEmail` endpoint.", + example: "gwa8gs8hgw9h2g9hg29hgwh9asdgh9q34$$$$$asdgasdggds", required: true } - }, - exits: { - success: { - description: 'Password successfully updated, and requesting user agent is now logged in.' + description: + "Password successfully updated, and requesting user agent is now logged in." }, invalidToken: { - description: 'The provided password token is invalid, expired, or has already been used.', - responseType: 'expired' + description: + "The provided password token is invalid, expired, or has already been used.", + responseType: "expired" } - }, - fn: async function (inputs) { - - if(!inputs.token) { - throw 'invalidToken'; + if (!inputs.token) { + throw "invalidToken"; } // Look up the user with this reset token. @@ -50,25 +43,21 @@ module.exports = { // If no such user exists, or their token is expired, bail. if (!userRecord || userRecord.passwordResetTokenExpiresAt <= Date.now()) { - throw 'invalidToken'; + throw "invalidToken"; } // Hash the new password. var hashed = await sails.helpers.passwords.hashPassword(inputs.password); // Store the user's new password and clear their reset token so it can't be used again. - await User.updateOne({ id: userRecord.id }) - .set({ + await User.updateOne({ id: userRecord.id }).set({ password: hashed, - passwordResetToken: '', + passwordResetToken: "", passwordResetTokenExpiresAt: 0 }); // Log the user in. // (This will be persisted when the response is sent.) this.req.session.userId = userRecord.id; - } - - }; diff --git a/backend/api/controllers/entrance/view-forgot-password.js b/backend/api/controllers/entrance/view-forgot-password.js index e6b5404..abe7d9e 100644 --- a/backend/api/controllers/entrance/view-forgot-password.js +++ b/backend/api/controllers/entrance/view-forgot-password.js @@ -1,36 +1,26 @@ module.exports = { + friendlyName: "View forgot password", - - friendlyName: 'View forgot password', - - - description: 'Display "Forgot password" page.', - + description: "Display \"Forgot password\" page.", exits: { - success: { - viewTemplatePath: 'pages/entrance/forgot-password', + viewTemplatePath: "pages/entrance/forgot-password" }, redirect: { - description: 'The requesting user is already logged in.', - extendedDescription: 'Logged-in users should change their password in "Account settings."', - responseType: 'redirect', + description: "The requesting user is already logged in.", + extendedDescription: + "Logged-in users should change their password in \"Account settings.\"", + responseType: "redirect" } - }, - fn: async function () { - if (this.req.me) { - throw {redirect: '/'}; + throw { redirect: "/" }; } return {}; - } - - }; diff --git a/backend/api/controllers/entrance/view-login.js b/backend/api/controllers/entrance/view-login.js index 1d1c590..8c22358 100644 --- a/backend/api/controllers/entrance/view-login.js +++ b/backend/api/controllers/entrance/view-login.js @@ -1,35 +1,24 @@ module.exports = { + friendlyName: "View login", - - friendlyName: 'View login', - - - description: 'Display "Login" page.', - + description: "Display \"Login\" page.", exits: { - success: { - viewTemplatePath: 'pages/entrance/login', + viewTemplatePath: "pages/entrance/login" }, redirect: { - description: 'The requesting user is already logged in.', - responseType: 'redirect' + description: "The requesting user is already logged in.", + responseType: "redirect" } - }, - fn: async function () { - if (this.req.me) { - throw {redirect: '/'}; + throw { redirect: "/" }; } return {}; - } - - }; diff --git a/backend/api/controllers/entrance/view-new-password.js b/backend/api/controllers/entrance/view-new-password.js index dab3f3d..e96276e 100644 --- a/backend/api/controllers/entrance/view-new-password.js +++ b/backend/api/controllers/entrance/view-new-password.js @@ -1,57 +1,46 @@ module.exports = { + friendlyName: "View new password", - - friendlyName: 'View new password', - - - description: 'Display "New password" page.', - + description: "Display \"New password\" page.", inputs: { - token: { - description: 'The password reset token from the email.', - example: '4-32fad81jdaf$329' + description: "The password reset token from the email.", + example: "4-32fad81jdaf$329" } - }, - exits: { - success: { - viewTemplatePath: 'pages/entrance/new-password' + viewTemplatePath: "pages/entrance/new-password" }, invalidOrExpiredToken: { - responseType: 'expired', - description: 'The provided token is expired, invalid, or has already been used.', + responseType: "expired", + description: + "The provided token is expired, invalid, or has already been used." } - }, - fn: async function (inputs) { - // If password reset token is missing, display an error page explaining that the link is bad. if (!inputs.token) { - sails.log.warn('Attempting to view new password (recovery) page, but no reset password token included in request! Displaying error page...'); - throw 'invalidOrExpiredToken'; - }//• + sails.log.warn( + "Attempting to view new password (recovery) page, but no reset password token included in request! Displaying error page..." + ); + throw "invalidOrExpiredToken"; + } // • // Look up the user with this reset token. var userRecord = await User.findOne({ passwordResetToken: inputs.token }); // If no such user exists, or their token is expired, display an error page explaining that the link is bad. if (!userRecord || userRecord.passwordResetTokenExpiresAt <= Date.now()) { - throw 'invalidOrExpiredToken'; + throw "invalidOrExpiredToken"; } // Grab token and include it in view locals return { token: inputs.token }; - } - - }; diff --git a/backend/api/controllers/entrance/view-signup.js b/backend/api/controllers/entrance/view-signup.js index be43753..4d3a290 100644 --- a/backend/api/controllers/entrance/view-signup.js +++ b/backend/api/controllers/entrance/view-signup.js @@ -1,35 +1,24 @@ module.exports = { + friendlyName: "View signup", - - friendlyName: 'View signup', - - - description: 'Display "Signup" page.', - + description: "Display \"Signup\" page.", exits: { - success: { - viewTemplatePath: 'pages/entrance/signup', + viewTemplatePath: "pages/entrance/signup" }, redirect: { - description: 'The requesting user is already logged in.', - responseType: 'redirect' + description: "The requesting user is already logged in.", + responseType: "redirect" } - }, - fn: async function () { - if (this.req.me) { - throw {redirect: '/'}; + throw { redirect: "/" }; } return {}; - } - - }; diff --git a/backend/api/controllers/view-homepage-or-redirect.js b/backend/api/controllers/view-homepage-or-redirect.js index b37f3be..b04555d 100644 --- a/backend/api/controllers/view-homepage-or-redirect.js +++ b/backend/api/controllers/view-homepage-or-redirect.js @@ -1,37 +1,31 @@ module.exports = { + friendlyName: "View homepage or redirect", - friendlyName: 'View homepage or redirect', - - - description: 'Display or redirect to the appropriate homepage, depending on login status.', - + description: "Display or redirect to the appropriate homepage, depending on login status.", exits: { success: { statusCode: 200, - description: 'Requesting user is a guest, so show the public landing page.', - viewTemplatePath: 'pages/homepage' + description: "Requesting user is a guest, so show the public landing page.", + viewTemplatePath: "pages/homepage" }, redirect: { - responseType: 'redirect', - description: 'Requesting user is logged in, so redirect to the internal welcome page.' - }, + responseType: "redirect", + description: "Requesting user is logged in, so redirect to the internal welcome page." + } }, - fn: async function () { - if (this.req.me) { - throw {redirect:'/welcome'}; + const error = { redirect: "/welcome" }; + throw error; } return {}; - } - }; diff --git a/backend/api/helpers/send-template-email.js b/backend/api/helpers/send-template-email.js index 8a9a64d..0019df5 100644 --- a/backend/api/helpers/send-template-email.js +++ b/backend/api/helpers/send-template-email.js @@ -1,124 +1,126 @@ module.exports = { + friendlyName: "Send template email", + description: "Send an email using a template.", - friendlyName: 'Send template email', - - - description: 'Send an email using a template.', - - - extendedDescription: 'To ease testing and development, if the provided "to" email address ends in "@example.com", '+ - 'then the email message will be written to the terminal instead of actually being sent.'+ - '(Thanks [@simonratner](https://github.com/simonratner)!)', - + extendedDescription: + "To ease testing and development, if the provided \"to\" email address ends in \"@example.com\", " + + "then the email message will be written to the terminal instead of actually being sent." + + "(Thanks [@simonratner](https://github.com/simonratner)!)", inputs: { - template: { - description: 'The relative path to an EJS template within our `views/emails/` folder -- WITHOUT the file extension.', - extendedDescription: 'Use strings like "foo" or "foo/bar", but NEVER "foo/bar.ejs". For example, '+ - '"marketing/welcome" would send an email using the "views/emails/marketing/welcome.ejs" template.', - example: 'email-reset-password', - type: 'string', + description: + "The relative path to an EJS template within our `views/emails/` folder -- WITHOUT the file extension.", + extendedDescription: + "Use strings like \"foo\" or \"foo/bar\", but NEVER \"foo/bar.ejs\". For example, " + + "\"marketing/welcome\" would send an email using the \"views/emails/marketing/welcome.ejs\" template.", + example: "email-reset-password", + type: "string", required: true }, templateData: { - description: 'A dictionary of data which will be accessible in the EJS template.', - extendedDescription: 'Each key will be a local variable accessible in the template. For instance, if you supply '+ - 'a dictionary with a \`friends\` key, and \`friends\` is an array like \`[{name:"Chandra"}, {name:"Mary"}]\`),'+ - 'then you will be able to access \`friends\` from the template:\n'+ - '\`\`\`\n'+ - '\n'+ - '\`\`\`'+ - '\n'+ - 'This is EJS, so use \`<%= %>\` to inject the HTML-escaped content of a variable, \`<%= %>\` to skip HTML-escaping '+ - 'and inject the data as-is, or \`<% %>\` to execute some JavaScript code such as an \`if\` statement or \`for\` loop.', + description: + "A dictionary of data which will be accessible in the EJS template.", + extendedDescription: + "Each key will be a local variable accessible in the template. For instance, if you supply " + + "a dictionary with a `friends` key, and `friends` is an array like `[{name:\"Chandra\"}, {name:\"Mary\"}]`)," + + "then you will be able to access `friends` from the template:\n" + + "```\n" + + "\n" + + "```" + + "\n" + + "This is EJS, so use `<%= %>` to inject the HTML-escaped content of a variable, `<%= %>` to skip HTML-escaping " + + "and inject the data as-is, or `<% %>` to execute some JavaScript code such as an `if` statement or `for` loop.", type: {}, defaultsTo: {} }, to: { - description: 'The email address of the primary recipient.', - extendedDescription: 'If this is any address ending in "@example.com", then don\'t actually deliver the message. '+ - 'Instead, just log it to the console.', - example: 'foo@bar.com', + description: "The email address of the primary recipient.", + extendedDescription: + "If this is any address ending in \"@example.com\", then don't actually deliver the message. " + + "Instead, just log it to the console.", + example: "foo@bar.com", required: true }, subject: { - description: 'The subject of the email.', - example: 'Hello there.', - defaultsTo: '' + description: "The subject of the email.", + example: "Hello there.", + defaultsTo: "" }, layout: { - description: 'Set to `false` to disable layouts altogether, or provide the path (relative '+ - 'from `views/layouts/`) to an override email layout.', - defaultsTo: 'layout-email', - custom: (layout)=>layout===false || _.isString(layout) + description: + "Set to `false` to disable layouts altogether, or provide the path (relative " + + "from `views/layouts/`) to an override email layout.", + defaultsTo: "layout-email", + custom: layout => layout === false || _.isString(layout) }, ensureAck: { - description: 'Whether to wait for acknowledgement (to hear back) that the email was successfully sent (or at least queued for sending) before returning.', - extendedDescription: 'Otherwise by default, this returns immediately and delivers the request to deliver this email in the background.', - type: 'boolean', + description: + "Whether to wait for acknowledgement (to hear back) that the email was successfully sent (or at least queued for sending) before returning.", + extendedDescription: + "Otherwise by default, this returns immediately and delivers the request to deliver this email in the background.", + type: "boolean", defaultsTo: false } - }, - exits: { - success: { - outputFriendlyName: 'Email delivery report', - outputDescription: 'A dictionary of information about what went down.', + outputFriendlyName: "Email delivery report", + outputDescription: "A dictionary of information about what went down.", outputType: { - loggedInsteadOfSending: 'boolean' + loggedInsteadOfSending: "boolean" } } - }, + fn: async function (inputs) { + var path = require("path"); + var url = require("url"); + var util = require("util"); - fn: async function(inputs) { - - var path = require('path'); - var url = require('url'); - var util = require('util'); - - - if (!_.startsWith(path.basename(inputs.template), 'email-')) { + if (!_.startsWith(path.basename(inputs.template), "email-")) { sails.log.warn( - 'The "template" that was passed in to `sendTemplateEmail()` does not begin with '+ - '"email-" -- but by convention, all email template files in `views/emails/` should '+ - 'be namespaced in this way. (This makes it easier to look up email templates by '+ - 'filename; e.g. when using CMD/CTRL+P in Sublime Text.)\n'+ - 'Continuing regardless...' + "The \"template\" that was passed in to `sendTemplateEmail()` does not begin with " + + "\"email-\" -- but by convention, all email template files in `views/emails/` should " + + "be namespaced in this way. (This makes it easier to look up email templates by " + + "filename; e.g. when using CMD/CTRL+P in Sublime Text.)\n" + + "Continuing regardless..." ); } - if (_.startsWith(inputs.template, 'views/') || _.startsWith(inputs.template, 'emails/')) { + if ( + _.startsWith(inputs.template, "views/") || + _.startsWith(inputs.template, "emails/") + ) { throw new Error( - 'The "template" that was passed in to `sendTemplateEmail()` was prefixed with\n'+ - '`emails/` or `views/` -- but that part is supposed to be omitted. Instead, please\n'+ - 'just specify the path to the desired email template relative from `views/emails/`.\n'+ - 'For example:\n'+ - ' template: \'email-reset-password\'\n'+ - 'Or:\n'+ - ' template: \'admin/email-contact-form\'\n'+ - ' [?] If you\'re unsure or need advice, see https://sailsjs.com/support' + "The \"template\" that was passed in to `sendTemplateEmail()` was prefixed with\n" + + "`emails/` or `views/` -- but that part is supposed to be omitted. Instead, please\n" + + "just specify the path to the desired email template relative from `views/emails/`.\n" + + "For example:\n" + + " template: 'email-reset-password'\n" + + "Or:\n" + + " template: 'admin/email-contact-form'\n" + + " [?] If you're unsure or need advice, see https://sailsjs.com/support" ); - }//• + } // • // Determine appropriate email layout and template to use. - var emailTemplatePath = path.join('emails/', inputs.template); + var emailTemplatePath = path.join("emails/", inputs.template); var layout; if (inputs.layout) { - layout = path.relative(path.dirname(emailTemplatePath), path.resolve('layouts/', inputs.layout)); + layout = path.relative( + path.dirname(emailTemplatePath), + path.resolve("layouts/", inputs.layout) + ); } else { layout = false; } @@ -127,18 +129,19 @@ module.exports = { // > Note that we set the layout, provide access to core `url` package (for // > building links and image srcs, etc.), and also provide access to core // > `util` package (for dumping debug data in internal emails). - var htmlEmailContents = await sails.renderView( - emailTemplatePath, - _.extend({layout, url, util }, inputs.templateData) - ) - .intercept((err)=>{ - err.message = - 'Could not compile view template.\n'+ - '(Usually, this means the provided data is invalid, or missing a piece.)\n'+ - 'Details:\n'+ - err.message; - return err; - }); + var htmlEmailContents = await sails + .renderView( + emailTemplatePath, + _.extend({ layout, url, util }, inputs.templateData) + ) + .intercept(err => { + err.message = + "Could not compile view template.\n" + + "(Usually, this means the provided data is invalid, or missing a piece.)\n" + + "Details:\n" + + err.message; + return err; + }); // Sometimes only log info to the console about the email that WOULD have been sent. // Specifically, if the "To" email address is anything "@example.com". @@ -151,54 +154,67 @@ module.exports = { // If that's the case, or if we're in the "test" environment, then log // the email instead of sending it: - var dontActuallySend = ( - sails.config.environment === 'test' || isToAddressConsideredFake - ); + var dontActuallySend = + sails.config.environment === "test" || isToAddressConsideredFake; if (dontActuallySend) { sails.log( - 'Skipped sending email, either because the "To" email address ended in "@example.com"\n'+ - 'or because the current \`sails.config.environment\` is set to "test".\n'+ - '\n'+ - 'But anyway, here is what WOULD have been sent:\n'+ - '-=-=-=-=-=-=-=-=-=-=-=-=-= Email log =-=-=-=-=-=-=-=-=-=-=-=-=-\n'+ - 'To: '+inputs.to+'\n'+ - 'Subject: '+inputs.subject+'\n'+ - '\n'+ - 'Body:\n'+ - htmlEmailContents+'\n'+ - '-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-' + "Skipped sending email, either because the \"To\" email address ended in \"@example.com\"\n" + + "or because the current `sails.config.environment` is set to \"test\".\n" + + "\n" + + "But anyway, here is what WOULD have been sent:\n" + + "-=-=-=-=-=-=-=-=-=-=-=-=-= Email log =-=-=-=-=-=-=-=-=-=-=-=-=-\n" + + "To: " + + inputs.to + + "\n" + + "Subject: " + + inputs.subject + + "\n" + + "\n" + + "Body:\n" + + htmlEmailContents + + "\n" + + "-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-" ); } else { // Otherwise, we'll check that all required Mailgun credentials are set up // and, if so, continue to actually send the email. - if (!sails.config.custom.mailgunSecret || !sails.config.custom.mailgunDomain) { + if ( + !sails.config.custom.mailgunSecret || + !sails.config.custom.mailgunDomain + ) { throw new Error( - 'Cannot deliver email to "'+inputs.to+'" because:\n'+ - (()=>{ - let problems = []; - if (!sails.config.custom.mailgunSecret) { - problems.push(' • Mailgun secret is missing from this app\'s configuration (`sails.config.custom.mailgunSecret`)'); - } - if (!sails.config.custom.mailgunDomain) { - problems.push(' • Mailgun domain is missing from this app\'s configuration (`sails.config.custom.mailgunDomain`)'); - } - return problems.join('\n'); - })()+ - '\n'+ - 'To resolve these configuration issues, add the missing config variables to\n'+ - '\`config/custom.js\`-- or in staging/production, set them up as system\n'+ - 'environment vars. (If you don\'t have a Mailgun domain or secret, you can\n'+ - 'sign up for free at https://mailgun.com to receive sandbox credentials.)\n'+ - '\n'+ - '> Note that, for convenience during development, there is another alternative:\n'+ - '> In lieu of setting up real Mailgun credentials, you can "fake" email\n'+ - '> delivery by using any email address that ends in "@example.com". This will\n'+ - '> write automated emails to your logs rather than actually sending them.\n'+ - '> (To simulate clicking on a link from an email, just copy and paste the link\n'+ - '> from the terminal output into your browser.)\n'+ - '\n'+ - '[?] If you\'re unsure, visit https://sailsjs.com/support' + "Cannot deliver email to \"" + + inputs.to + + "\" because:\n" + + (() => { + let problems = []; + if (!sails.config.custom.mailgunSecret) { + problems.push( + " • Mailgun secret is missing from this app's configuration (`sails.config.custom.mailgunSecret`)" + ); + } + if (!sails.config.custom.mailgunDomain) { + problems.push( + " • Mailgun domain is missing from this app's configuration (`sails.config.custom.mailgunDomain`)" + ); + } + return problems.join("\n"); + })() + + "\n" + + "To resolve these configuration issues, add the missing config variables to\n" + + "`config/custom.js`-- or in staging/production, set them up as system\n" + + "environment vars. (If you don't have a Mailgun domain or secret, you can\n" + + "sign up for free at https://mailgun.com to receive sandbox credentials.)\n" + + "\n" + + "> Note that, for convenience during development, there is another alternative:\n" + + "> In lieu of setting up real Mailgun credentials, you can \"fake\" email\n" + + "> delivery by using any email address that ends in \"@example.com\". This will\n" + + "> write automated emails to your logs rather than actually sending them.\n" + + "> (To simulate clicking on a link from an email, just copy and paste the link\n" + + "> from the terminal output into your browser.)\n" + + "\n" + + "[?] If you're unsure, visit https://sailsjs.com/support" ); } @@ -212,29 +228,27 @@ module.exports = { await deferred; } else { // FUTURE: take advantage of .background() here instead (when available) - deferred.exec((err)=>{ + deferred.exec(err => { if (err) { sails.log.error( - 'Background instruction failed: Could not deliver email:\n'+ - util.inspect(inputs,{depth:null})+'\n', - 'Error details:\n'+ - util.inspect(err) + "Background instruction failed: Could not deliver email:\n" + + util.inspect(inputs, { depth: null }) + + "\n", + "Error details:\n" + util.inspect(err) ); } else { sails.log.info( - 'Background instruction complete: Email sent (or at least queued):\n'+ - util.inspect(inputs,{depth:null}) + "Background instruction complete: Email sent (or at least queued):\n" + + util.inspect(inputs, { depth: null }) ); } - });//_∏_ - }//fi - }//fi + }); // _∏_ + } // fi + } // fi // All done! return { loggedInsteadOfSending: dontActuallySend }; - } - }; diff --git a/backend/api/hooks/custom/index.js b/backend/api/hooks/custom/index.js index 8bc8bb6..a8601c1 100644 --- a/backend/api/hooks/custom/index.js +++ b/backend/api/hooks/custom/index.js @@ -5,30 +5,38 @@ * @docs :: https://sailsjs.com/docs/concepts/extending-sails/hooks */ -module.exports = function defineCustomHook(sails) { - +module.exports = function defineCustomHook (sails) { return { - /** * Runs when a Sails app loads/lifts. */ initialize: async function () { - - sails.log.info('Initializing hook... (`api/hooks/custom`)'); + sails.log.info("Initializing hook... (`api/hooks/custom`)"); // Check Stripe/Mailgun configuration (for billing and emails). - var IMPORTANT_STRIPE_CONFIG = ['stripeSecret', 'stripePublishableKey']; - var IMPORTANT_MAILGUN_CONFIG = ['mailgunSecret', 'mailgunDomain', 'internalEmailAddress']; - var isMissingStripeConfig = _.difference(IMPORTANT_STRIPE_CONFIG, Object.keys(sails.config.custom)).length > 0; - var isMissingMailgunConfig = _.difference(IMPORTANT_MAILGUN_CONFIG, Object.keys(sails.config.custom)).length > 0; + var IMPORTANT_STRIPE_CONFIG = ["stripeSecret", "stripePublishableKey"]; + var IMPORTANT_MAILGUN_CONFIG = [ + "mailgunSecret", + "mailgunDomain", + "internalEmailAddress" + ]; + var isMissingStripeConfig = + _.difference(IMPORTANT_STRIPE_CONFIG, Object.keys(sails.config.custom)) + .length > 0; + var isMissingMailgunConfig = + _.difference(IMPORTANT_MAILGUN_CONFIG, Object.keys(sails.config.custom)) + .length > 0; if (isMissingStripeConfig || isMissingMailgunConfig) { - - let missingFeatureText = isMissingStripeConfig && isMissingMailgunConfig ? 'billing and email' : isMissingStripeConfig ? 'billing' : 'email'; - let suffix = ''; - if (_.contains(['silly'], sails.config.log.level)) { - suffix = -` + let missingFeatureText = + isMissingStripeConfig && isMissingMailgunConfig + ? "billing and email" + : isMissingStripeConfig + ? "billing" + : "email"; + let suffix = ""; + if (_.contains(["silly"], sails.config.log.level)) { + suffix = ` > Tip: To exclude sensitive credentials from source control, use: > • config/local.js (for local development) > • environment variables (for production) @@ -44,32 +52,43 @@ module.exports = function defineCustomHook(sails) { let problems = []; if (sails.config.custom.stripeSecret === undefined) { - problems.push('No `sails.config.custom.stripeSecret` was configured.'); + problems.push( + "No `sails.config.custom.stripeSecret` was configured." + ); } if (sails.config.custom.stripePublishableKey === undefined) { - problems.push('No `sails.config.custom.stripePublishableKey` was configured.'); + problems.push( + "No `sails.config.custom.stripePublishableKey` was configured." + ); } if (sails.config.custom.mailgunSecret === undefined) { - problems.push('No `sails.config.custom.mailgunSecret` was configured.'); + problems.push( + "No `sails.config.custom.mailgunSecret` was configured." + ); } if (sails.config.custom.mailgunDomain === undefined) { - problems.push('No `sails.config.custom.mailgunDomain` was configured.'); + problems.push( + "No `sails.config.custom.mailgunDomain` was configured." + ); } if (sails.config.custom.internalEmailAddress === undefined) { - problems.push('No `sails.config.custom.internalEmailAddress` was configured.'); + problems.push( + "No `sails.config.custom.internalEmailAddress` was configured." + ); } sails.log.verbose( -`Some optional settings have not been configured yet: + `Some optional settings have not been configured yet: --------------------------------------------------------------------- -${problems.join('\n')} +${problems.join("\n")} Until this is addressed, this app's ${missingFeatureText} features will be disabled and/or hidden in the UI. [?] If you're unsure or need advice, come by https://sailsjs.com/support ----------------------------------------------------------------------${suffix}`); - }//fi +---------------------------------------------------------------------${suffix}` + ); + } // fi // Set an additional config keys based on whether Stripe config is available. // This will determine whether or not to enable various billing features. @@ -77,8 +96,7 @@ will be disabled and/or hidden in the UI. // After "sails-hook-organics" finishes initializing, configure Stripe // and Mailgun packs with any available credentials. - sails.after('hook:organics:loaded', ()=>{ - + sails.after("hook:organics:loaded", () => { sails.helpers.stripe.configure({ secret: sails.config.custom.stripeSecret }); @@ -87,19 +105,15 @@ will be disabled and/or hidden in the UI. secret: sails.config.custom.mailgunSecret, domain: sails.config.custom.mailgunDomain, from: sails.config.custom.fromEmailAddress, - fromName: sails.config.custom.fromName, + fromName: sails.config.custom.fromName }); - - });//_∏_ + }); // _∏_ // ... Any other app-specific setup code that needs to run on lift, // even in production, goes here ... - }, - routes: { - /** * Runs before every matching route. * @@ -108,21 +122,21 @@ will be disabled and/or hidden in the UI. * @param {Function} next */ before: { - '/*': { + "/*": { skipAssets: true, - fn: async function(req, res, next){ - - var url = require('url'); + fn: async function (req, res, next) { + var url = require("url"); // First, if this is a GET request (and thus potentially a view), // attach a couple of guaranteed locals. - if (req.method === 'GET') { - + if (req.method === "GET") { // The `_environment` local lets us do a little workaround to make Vue.js // run in "production mode" without unnecessarily involving complexities // with webpack et al.) if (res.locals._environment !== undefined) { - throw new Error('Cannot attach Sails environment as the view local `_environment`, because this view local already exists! (Is it being attached somewhere else?)'); + throw new Error( + "Cannot attach Sails environment as the view local `_environment`, because this view local already exists! (Is it being attached somewhere else?)" + ); } res.locals._environment = sails.config.environment; @@ -131,10 +145,12 @@ will be disabled and/or hidden in the UI. // > Note that, depending on the request, this may or may not be set to the // > logged-in user record further below. if (res.locals.me !== undefined) { - throw new Error('Cannot attach view local `me`, because this view local already exists! (Is it being attached somewhere else?)'); + throw new Error( + "Cannot attach view local `me`, because this view local already exists! (Is it being attached somewhere else?)" + ); } res.locals.me = undefined; - }//fi + } // fi // Next, if we're running in our actual "production" or "staging" Sails // environment, check if this is a GET request via some other host, @@ -150,19 +166,38 @@ will be disabled and/or hidden in the UI. // > case you want to run it on a real, physical mobile/IoT device) var configuredBaseHostname; try { - configuredBaseHostname = url.parse(sails.config.custom.baseUrl).host; - } catch (unusedErr) { /*…*/} - if ((sails.config.environment === 'staging' || sails.config.environment === 'production') && !req.isSocket && req.method === 'GET' && req.hostname !== configuredBaseHostname) { - sails.log.info('Redirecting GET request from `'+req.hostname+'` to configured expected host (`'+configuredBaseHostname+'`)...'); - return res.redirect(sails.config.custom.baseUrl+req.url); - }//• + configuredBaseHostname = url.URL(sails.config.custom.baseUrl) + .host; + } catch (unusedErr) { + + } + if ( + (sails.config.environment === "staging" || + sails.config.environment === "production") && + !req.isSocket && + req.method === "GET" && + req.hostname !== configuredBaseHostname + ) { + sails.log.info( + "Redirecting GET request from `" + + req.hostname + + "` to configured expected host (`" + + configuredBaseHostname + + "`)..." + ); + return res.redirect(sails.config.custom.baseUrl + req.url); + } // • // No session? Proceed as usual. // (e.g. request for a static asset) - if (!req.session) { return next(); } + if (!req.session) { + return next(); + } // Not logged in? Proceed as usual. - if (!req.session.userId) { return next(); } + if (!req.session.userId) { + return next(); + } // Otherwise, look up the logged-in user. var loggedInUser = await User.findOne({ @@ -173,21 +208,30 @@ will be disabled and/or hidden in the UI. // wipe the user id from the requesting user agent's session, // and then send the "unauthorized" response. if (!loggedInUser) { - sails.log.warn('Somehow, the user record for the logged-in user (`'+req.session.userId+'`) has gone missing....'); + sails.log.warn( + "Somehow, the user record for the logged-in user (`" + + req.session.userId + + "`) has gone missing...." + ); delete req.session.userId; return res.unauthorized(); } // Add additional information for convenience when building top-level navigation. // (i.e. whether to display "Dashboard", "My Account", etc.) - if (!loggedInUser.password || loggedInUser.emailStatus === 'unconfirmed') { + if ( + !loggedInUser.password || + loggedInUser.emailStatus === "unconfirmed" + ) { loggedInUser.dontDisplayAccountLinkInNav = true; } // Expose the user record as an extra property on the request object (`req.me`). // > Note that we make sure `req.me` doesn't already exist first. if (req.me !== undefined) { - throw new Error('Cannot attach logged-in user as `req.me` because this property already exists! (Is it being attached somewhere else?)'); + throw new Error( + "Cannot attach logged-in user as `req.me` because this property already exists! (Is it being attached somewhere else?)" + ); } req.me = loggedInUser; @@ -195,28 +239,38 @@ will be disabled and/or hidden in the UI. // to the current timestamp. // // (Note: As an optimization, this is run behind the scenes to avoid adding needless latency.) - var MS_TO_BUFFER = 60*1000; + var MS_TO_BUFFER = 60 * 1000; var now = Date.now(); if (loggedInUser.lastSeenAt < now - MS_TO_BUFFER) { - User.updateOne({id: loggedInUser.id}) - .set({ lastSeenAt: now }) - .exec((err)=>{ - if (err) { - sails.log.error('Background task failed: Could not update user (`'+loggedInUser.id+'`) with a new `lastSeenAt` timestamp. Error details: '+err.stack); - return; - }//• - sails.log.verbose('Updated the `lastSeenAt` timestamp for user `'+loggedInUser.id+'`.'); - // Nothing else to do here. - });//_∏_ (Meanwhile...) - }//fi - + User.updateOne({ id: loggedInUser.id }) + .set({ lastSeenAt: now }) + .exec(err => { + if (err) { + sails.log.error( + "Background task failed: Could not update user (`" + + loggedInUser.id + + "`) with a new `lastSeenAt` timestamp. Error details: " + + err.stack + ); + return; + } // • + sails.log.verbose( + "Updated the `lastSeenAt` timestamp for user `" + + loggedInUser.id + + "`." + ); + // Nothing else to do here. + }); // _∏_ (Meanwhile...) + } // fi // If this is a GET request, then also expose an extra view local (`<%= me %>`). // > Note that we make sure a local named `me` doesn't already exist first. // > Also note that we strip off any properties that correspond with protected attributes. - if (req.method === 'GET') { + if (req.method === "GET") { if (res.locals.me !== undefined) { - throw new Error('Cannot attach logged-in user as the view local `me`, because this view local already exists! (Is it being attached somewhere else?)'); + throw new Error( + "Cannot attach logged-in user as the view local `me`, because this view local already exists! (Is it being attached somewhere else?)" + ); } // Exclude any fields corresponding with attributes that have `protect: true`. @@ -225,37 +279,37 @@ will be disabled and/or hidden in the UI. if (User.attributes[attrName].protect) { delete sanitizedUser[attrName]; } - }//∞ + } // ∞ // If there is still a "password" in sanitized user data, then delete it just to be safe. // (But also log a warning so this isn't hopelessly confusing.) if (sanitizedUser.password) { - sails.log.warn('The logged in user record has a `password` property, but it was still there after pruning off all properties that match `protect: true` attributes in the User model. So, just to be safe, removing the `password` property anyway...'); + sails.log.warn( + "The logged in user record has a `password` property, but it was still there after pruning off all properties that match `protect: true` attributes in the User model. So, just to be safe, removing the `password` property anyway..." + ); delete sanitizedUser.password; - }//fi + } // fi res.locals.me = sanitizedUser; // Include information on the locals as to whether billing features // are enabled for this app, and whether email verification is required. - res.locals.isBillingEnabled = sails.config.custom.enableBillingFeatures; - res.locals.isEmailVerificationRequired = sails.config.custom.verifyEmailAddresses; - - }//fi + res.locals.isBillingEnabled = + sails.config.custom.enableBillingFeatures; + res.locals.isEmailVerificationRequired = + sails.config.custom.verifyEmailAddresses; + } // fi // Prevent the browser from caching logged-in users' pages. // (including w/ the Chrome back button) // > • https://mixmax.com/blog/chrome-back-button-cache-no-store // > • https://madhatted.com/2013/6/16/you-do-not-understand-browser-history - res.setHeader('Cache-Control', 'no-cache, no-store'); + res.setHeader("Cache-Control", "no-cache, no-store"); return next(); } } } } - - }; - }; diff --git a/backend/api/models/Todos.js b/backend/api/models/Todos.js index 46f42a9..b1a115a 100644 --- a/backend/api/models/Todos.js +++ b/backend/api/models/Todos.js @@ -6,24 +6,15 @@ */ module.exports = { - attributes: { - // ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗ // ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗ // ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝ - - // ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗ // ║╣ ║║║╠╩╗║╣ ║║╚═╗ // ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝ - - // ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ // ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗ // ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝ - - }, - + } }; - diff --git a/backend/api/models/User.js b/backend/api/models/User.js index 983627d..d9489e7 100644 --- a/backend/api/models/User.js +++ b/backend/api/models/User.js @@ -5,29 +5,26 @@ */ module.exports = { - attributes: { - // ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗ // ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗ // ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝ emailAddress: { - type: 'string', + type: "string", required: true, unique: true, isEmail: true, maxLength: 200, - example: 'mary.sue@example.com' + example: "mary.sue@example.com" }, emailStatus: { - type: 'string', - isIn: ['unconfirmed', 'change-requested', 'confirmed'], - defaultsTo: 'confirmed', - description: 'The confirmation status of the user\'s email address.', - extendedDescription: -`Users might be created as "unconfirmed" (e.g. normal signup) or as "confirmed" (e.g. hard-coded + type: "string", + isIn: ["unconfirmed", "change-requested", "confirmed"], + defaultsTo: "confirmed", + description: "The confirmation status of the user's email address.", + extendedDescription: `Users might be created as "unconfirmed" (e.g. normal signup) or as "confirmed" (e.g. hard-coded admin users). When the email verification feature is enabled, new users created via the signup form have \`emailStatus: 'unconfirmed'\` until they click the link in the confirmation email. Similarly, when an existing user changes their email address, they switch to the "change-requested" @@ -35,32 +32,34 @@ email status until they click the link in the confirmation email.` }, emailChangeCandidate: { - type: 'string', + type: "string", isEmail: true, - description: 'A still-unconfirmed email address that this user wants to change to (if relevant).' + description: + "A still-unconfirmed email address that this user wants to change to (if relevant)." }, password: { - type: 'string', + type: "string", required: true, - description: 'Securely hashed representation of the user\'s login password.', + description: + "Securely hashed representation of the user's login password.", protect: true, - example: '2$28a8eabna301089103-13948134nad' + example: "2$28a8eabna301089103-13948134nad" }, fullName: { - type: 'string', + type: "string", required: true, - description: 'Full representation of the user\'s name.', + description: "Full representation of the user's name.", maxLength: 120, - example: 'Mary Sue van der McHenst' + example: "Mary Sue van der McHenst" }, isSuperAdmin: { - type: 'boolean', - description: 'Whether this user is a "super admin" with extra permissions, etc.', - extendedDescription: -`Super admins might have extra permissions, see a different default home page when they log in, + type: "boolean", + description: + "Whether this user is a \"super admin\" with extra permissions, etc.", + extendedDescription: `Super admins might have extra permissions, see a different default home page when they log in, or even have a completely different feature set from normal users. In this app, the \`isSuperAdmin\` flag is just here as a simple way to represent two different kinds of users. Usually, it's a good idea to keep the data model as simple as possible, only adding attributes when you actually need them for @@ -75,85 +74,100 @@ So, while this \`isSuperAdmin\` demarcation might not be the right approach fore }, passwordResetToken: { - type: 'string', - description: 'A unique token used to verify the user\'s identity when recovering a password. Expires after 1 use, or after a set amount of time has elapsed.' + type: "string", + description: + "A unique token used to verify the user's identity when recovering a password. Expires after 1 use, or after a set amount of time has elapsed." }, passwordResetTokenExpiresAt: { - type: 'number', - description: 'A JS timestamp (epoch ms) representing the moment when this user\'s `passwordResetToken` will expire (or 0 if the user currently has no such token).', + type: "number", + description: + "A JS timestamp (epoch ms) representing the moment when this user's `passwordResetToken` will expire (or 0 if the user currently has no such token).", example: 1502844074211 }, emailProofToken: { - type: 'string', - description: 'A pseudorandom, probabilistically-unique token for use in our account verification emails.' + type: "string", + description: + "A pseudorandom, probabilistically-unique token for use in our account verification emails." }, emailProofTokenExpiresAt: { - type: 'number', - description: 'A JS timestamp (epoch ms) representing the moment when this user\'s `emailProofToken` will expire (or 0 if the user currently has no such token).', + type: "number", + description: + "A JS timestamp (epoch ms) representing the moment when this user's `emailProofToken` will expire (or 0 if the user currently has no such token).", example: 1502844074211 }, stripeCustomerId: { - type: 'string', + type: "string", protect: true, - description: 'The id of the customer entry in Stripe associated with this user (or empty string if this user is not linked to a Stripe customer -- e.g. if billing features are not enabled).', - extendedDescription: -`Just because this value is set doesn't necessarily mean that this user has a billing card. + description: + "The id of the customer entry in Stripe associated with this user (or empty string if this user is not linked to a Stripe customer -- e.g. if billing features are not enabled).", + extendedDescription: `Just because this value is set doesn't necessarily mean that this user has a billing card. It just means they have a customer entry in Stripe, which might or might not have a billing card.` }, hasBillingCard: { - type: 'boolean', - description: 'Whether this user has a default billing card hooked up as their payment method.', - extendedDescription: -`More specifically, this indcates whether this user record's linked customer entry in Stripe has + type: "boolean", + description: + "Whether this user has a default billing card hooked up as their payment method.", + extendedDescription: `More specifically, this indcates whether this user record's linked customer entry in Stripe has a default payment source (i.e. credit card). Note that a user have a \`stripeCustomerId\` without necessarily having a billing card.` }, billingCardBrand: { - type: 'string', - example: 'Visa', - description: 'The brand of this user\'s default billing card (or empty string if no billing card is set up).', - extendedDescription: 'To ensure PCI compliance, this data comes from Stripe, where it reflects the user\'s default payment source.' + type: "string", + example: "Visa", + description: + "The brand of this user's default billing card (or empty string if no billing card is set up).", + extendedDescription: + "To ensure PCI compliance, this data comes from Stripe, where it reflects the user's default payment source." }, billingCardLast4: { - type: 'string', - example: '4242', - description: 'The last four digits of the card number for this user\'s default billing card (or empty string if no billing card is set up).', - extendedDescription: 'To ensure PCI compliance, this data comes from Stripe, where it reflects the user\'s default payment source.' + type: "string", + example: "4242", + description: + "The last four digits of the card number for this user's default billing card (or empty string if no billing card is set up).", + extendedDescription: + "To ensure PCI compliance, this data comes from Stripe, where it reflects the user's default payment source." }, billingCardExpMonth: { - type: 'string', - example: '08', - description: 'The two-digit expiration month from this user\'s default billing card, formatted as MM (or empty string if no billing card is set up).', - extendedDescription: 'To ensure PCI compliance, this data comes from Stripe, where it reflects the user\'s default payment source.' + type: "string", + example: "08", + description: + "The two-digit expiration month from this user's default billing card, formatted as MM (or empty string if no billing card is set up).", + extendedDescription: + "To ensure PCI compliance, this data comes from Stripe, where it reflects the user's default payment source." }, billingCardExpYear: { - type: 'string', - example: '2023', - description: 'The four-digit expiration year from this user\'s default billing card, formatted as YYYY (or empty string if no credit card is set up).', - extendedDescription: 'To ensure PCI compliance, this data comes from Stripe, where it reflects the user\'s default payment source.' + type: "string", + example: "2023", + description: + "The four-digit expiration year from this user's default billing card, formatted as YYYY (or empty string if no credit card is set up).", + extendedDescription: + "To ensure PCI compliance, this data comes from Stripe, where it reflects the user's default payment source." }, tosAcceptedByIp: { - type: 'string', - description: 'The IP (ipv4) address of the request that accepted the terms of service.', - extendedDescription: 'Useful for certain types of businesses and regulatory requirements (KYC, etc.)', - moreInfoUrl: 'https://en.wikipedia.org/wiki/Know_your_customer' + type: "string", + description: + "The IP (ipv4) address of the request that accepted the terms of service.", + extendedDescription: + "Useful for certain types of businesses and regulatory requirements (KYC, etc.)", + moreInfoUrl: "https://en.wikipedia.org/wiki/Know_your_customer" }, lastSeenAt: { - type: 'number', - description: 'A JS timestamp (epoch ms) representing the moment at which this user most recently interacted with the backend while logged in (or 0 if they have not interacted with the backend at all yet).', + type: "number", + description: + "A JS timestamp (epoch ms) representing the moment at which this user most recently interacted with the backend while logged in (or 0 if they have not interacted with the backend at all yet).", example: 1502844074211 - }, + } // ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗ // ║╣ ║║║╠╩╗║╣ ║║╚═╗ @@ -164,8 +178,5 @@ without necessarily having a billing card.` // ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗ // ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝ // n/a - - }, - - + } }; diff --git a/backend/api/policies/is-logged-in.js b/backend/api/policies/is-logged-in.js index 0c03b1a..24d186f 100644 --- a/backend/api/policies/is-logged-in.js +++ b/backend/api/policies/is-logged-in.js @@ -9,7 +9,6 @@ * https://sailsjs.com/docs/concepts/policies/access-control-and-permissions */ module.exports = async function (req, res, proceed) { - // If `req.me` is set, then we know that this request originated // from a logged-in user. So we can safely proceed to the next policy-- // or, if this is the last policy, the relevant action. @@ -19,8 +18,7 @@ module.exports = async function (req, res, proceed) { return proceed(); } - //--• + // --• // Otherwise, this request did not come from a logged-in user. return res.unauthorized(); - }; diff --git a/backend/api/policies/is-super-admin.js b/backend/api/policies/is-super-admin.js index 09c473a..f08b412 100644 --- a/backend/api/policies/is-super-admin.js +++ b/backend/api/policies/is-super-admin.js @@ -9,20 +9,18 @@ * https://sailsjs.com/docs/concepts/policies/access-control-and-permissions */ module.exports = async function (req, res, proceed) { - // First, check whether the request comes from a logged-in user. // > For more about where `req.me` comes from, check out this app's // > custom hook (`api/hooks/custom/index.js`). if (!req.me) { return res.unauthorized(); - }//• + }// • // Then check that this user is a "super admin". if (!req.me.isSuperAdmin) { return res.forbidden(); - }//• + }// • // IWMIH, we've got ourselves a "super admin". return proceed(); - }; diff --git a/backend/api/responses/expired.js b/backend/api/responses/expired.js index c71239a..5a067ed 100644 --- a/backend/api/responses/expired.js +++ b/backend/api/responses/expired.js @@ -20,18 +20,15 @@ * } * ``` */ -module.exports = function expired() { - +module.exports = function expired () { var req = this.req; var res = this.res; - sails.log.verbose('Ran custom response: res.expired()'); + sails.log.verbose("Ran custom response: res.expired()"); if (req.wantsJSON) { - return res.status(498).send('Token Expired/Invalid'); - } - else { - return res.status(498).view('498'); + return res.status(498).send("Token Expired/Invalid"); + } else { + return res.status(498).view("498"); } - }; diff --git a/backend/api/responses/unauthorized.js b/backend/api/responses/unauthorized.js index 650cb99..dfa3601 100644 --- a/backend/api/responses/unauthorized.js +++ b/backend/api/responses/unauthorized.js @@ -20,24 +20,20 @@ * } * ``` */ -module.exports = function unauthorized() { - +module.exports = function unauthorized () { var req = this.req; var res = this.res; - sails.log.verbose('Ran custom response: res.unauthorized()'); + sails.log.verbose("Ran custom response: res.unauthorized()"); if (req.wantsJSON) { return res.sendStatus(401); - } - // Or log them out (if necessary) and then redirect to the login page. - else { - + } else { + // Or log them out (if necessary) and then redirect to the login page. if (req.session.userId) { delete req.session.userId; } - return res.redirect('/login'); + return res.redirect("/login"); } - }; diff --git a/backend/app.js b/backend/app.js index f2c5f4e..8ba33b7 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1,3 +1,5 @@ +/* eslint-disable */ + /** * app.js * diff --git a/backend/scripts/rebuild-cloud-sdk.js b/backend/scripts/rebuild-cloud-sdk.js index 045d6ad..e5aef95 100644 --- a/backend/scripts/rebuild-cloud-sdk.js +++ b/backend/scripts/rebuild-cloud-sdk.js @@ -1,14 +1,11 @@ module.exports = { + friendlyName: "Rebuild Cloud SDK", - friendlyName: 'Rebuild Cloud SDK', + description: + "Regenerate the configuration for the \"Cloud SDK\" -- the JavaScript module used for AJAX and WebSockets.", - - description: 'Regenerate the configuration for the "Cloud SDK" -- the JavaScript module used for AJAX and WebSockets.', - - - fn: async function(){ - - var path = require('path'); + fn: async function () { + var path = require("path"); var endpointsByMethodName = {}; var extraEndpointsOnlyForTestsByMethodName = {}; @@ -20,7 +17,7 @@ module.exports = { // the very last sub-target in the array. if (_.isArray(target)) { target = _.last(target); - }//fi + } // fi // Skip redirects // (Note that, by doing this, we also skip traditional shorthand @@ -44,17 +41,17 @@ module.exports = { // Skip routes that just serve views. // (but still generate them for use in tests, for convenience) - if (target.view || (bareActionName.match(/^view-/))) { + if (target.view || bareActionName.match(/^view-/)) { extraEndpointsOnlyForTestsByMethodName[methodName] = { - verb: (expandedAddress.method||'get').toUpperCase(), + verb: (expandedAddress.method || "get").toUpperCase(), url: expandedAddress.url }; continue; - }//• + } // • endpointsByMethodName[methodName] = { - verb: (expandedAddress.method||'get').toUpperCase(), - url: expandedAddress.url, + verb: (expandedAddress.method || "get").toUpperCase(), + url: expandedAddress.url }; // If this is an actions2 action, then determine appropriate serial usage. @@ -63,7 +60,7 @@ module.exports = { // > method for this one. var requestable = sails.getActions()[target.action]; if (!requestable) { - sails.log.warn('Skipping unrecognized action: `'+target.action+'`'); + sails.log.warn("Skipping unrecognized action: `" + target.action + "`"); continue; } var def = requestable.toJSON && requestable.toJSON(); @@ -71,28 +68,36 @@ module.exports = { if (def.args !== undefined) { endpointsByMethodName[methodName].args = def.args; } else { - endpointsByMethodName[methodName].args = _.reduce(def.inputs, (args, inputDef, inputCodeName)=>{ - args.push(inputCodeName); - return args; - }, []); + endpointsByMethodName[methodName].args = _.reduce( + def.inputs, + (args, inputDef, inputCodeName) => { + args.push(inputCodeName); + return args; + }, + [] + ); } } // And we determine whether it needs to communicate over WebSockets // by checking for an additional property in the route target. if (target.isSocket) { - endpointsByMethodName[methodName].protocol = 'io.socket'; + endpointsByMethodName[methodName].protocol = "io.socket"; } - }//∞ + } // ∞ // Smash and rewrite the `cloud.setup.js` file in the assets folder to // reflect the latest set of available cloud actions exposed by this Sails // app (as determined by its routes above) await sails.helpers.fs.write.with({ - destination: path.resolve(sails.config.appPath, 'assets/js/cloud.setup.js'), + destination: path.resolve( + sails.config.appPath, + "assets/js/cloud.setup.js" + ), force: true, - string: ``+ -`/** + string: + "" + + `/** * cloud.setup.js * * Configuration for this Sails app's generated browser SDK ("Cloud"). @@ -110,25 +115,33 @@ Cloud.setup({ methods: ${JSON.stringify(endpointsByMethodName)} /* eslint-enable */ -});`+ - `\n` +});` + + "\n" }); // Also, if a `test/` folder exists, set up a barebones bounce of this data // as a JSON file inside of it, for testing purposes: - var hasTestFolder = await sails.helpers.fs.exists(path.resolve(sails.config.appPath, 'test/')); + var hasTestFolder = await sails.helpers.fs.exists( + path.resolve(sails.config.appPath, "test/") + ); if (hasTestFolder) { await sails.helpers.fs.write.with({ - destination: path.resolve(sails.config.appPath, 'test/private/CLOUD_SDK_METHODS.json'), - string: JSON.stringify(_.extend(endpointsByMethodName, extraEndpointsOnlyForTestsByMethodName)), + destination: path.resolve( + sails.config.appPath, + "test/private/CLOUD_SDK_METHODS.json" + ), + string: JSON.stringify( + _.extend( + endpointsByMethodName, + extraEndpointsOnlyForTestsByMethodName + ) + ), force: true }); } - sails.log.info('--'); - sails.log.info('Successfully rebuilt Cloud SDK for use in the browser.'); - sails.log.info('(and CLOUD_SDK_METHODS.json for use in automated tests)'); - + sails.log.info("--"); + sails.log.info("Successfully rebuilt Cloud SDK for use in the browser."); + sails.log.info("(and CLOUD_SDK_METHODS.json for use in automated tests)"); } - }; diff --git a/backend/tasks/pipeline.js b/backend/tasks/pipeline.js index d6bb9e4..c543d6b 100644 --- a/backend/tasks/pipeline.js +++ b/backend/tasks/pipeline.js @@ -1,3 +1,5 @@ +/* eslint-disable */ + /** * tasks/pipeline.js * @@ -42,7 +44,6 @@ var cssFilesToInject = [ 'styles/**/*.css' ]; - // ██████╗██╗ ██╗███████╗███╗ ██╗████████╗ ███████╗██╗██████╗ ███████╗ // ██╔════╝██║ ██║██╔════╝████╗ ██║╚══██╔══╝ ██╔════╝██║██╔══██╗██╔════╝ // ██║ ██║ ██║█████╗ ██╔██╗ ██║ ██║█████╗███████╗██║██║ ██║█████╗ @@ -135,21 +136,21 @@ var tmpPath = '.tmp/public/'; // Prefix relative paths to source files so they point to the proper locations // (i.e. where the other Grunt tasks spit them out, or in some cases, where // they reside in the first place) -module.exports.cssFilesToInject = cssFilesToInject.map((cssPath)=>{ +module.exports.cssFilesToInject = cssFilesToInject.map((cssPath) => { // If we're ignoring the file, make sure the ! is at the beginning of the path if (cssPath[0] === '!') { return require('path').join('!' + tmpPath, cssPath.substr(1)); } return require('path').join(tmpPath, cssPath); }); -module.exports.jsFilesToInject = jsFilesToInject.map((jsPath)=>{ +module.exports.jsFilesToInject = jsFilesToInject.map((jsPath) => { // If we're ignoring the file, make sure the ! is at the beginning of the path if (jsPath[0] === '!') { return require('path').join('!' + tmpPath, jsPath.substr(1)); } return require('path').join(tmpPath, jsPath); }); -module.exports.templateFilesToInject = templateFilesToInject.map((tplPath)=>{ +module.exports.templateFilesToInject = templateFilesToInject.map((tplPath) => { // If we're ignoring the file, make sure the ! is at the beginning of the path if (tplPath[0] === '!') { return require('path').join('!assets/', tplPath.substr(1)); diff --git a/frontend/components/todo-editor.js b/frontend/components/todo-editor.js index 4c7559a..db5789a 100644 --- a/frontend/components/todo-editor.js +++ b/frontend/components/todo-editor.js @@ -1,63 +1,71 @@ -import { bus } from '../scripts.js' +import { bus } from "../scripts.js"; const TodoEditor = { - template: '#todo-editor-template', - methods: { - addTodo: function() { - if (!this.todoText) { - alert("We need something here."); - // here we can put an alert from sweet alert library, you know it? - // No, but I can look when I'm done - // Ok, here is the link https://sweetalert2.github.io/ - } - else { - - const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'] - - function getMonthName(ind) { - - - return months[ind] - } - - const date = new Date(); - - this.todos = JSON.parse(localStorage.getItem('todos')); - - // The newest todos go to the front of the list - this.todos.push({ - name: this.todoText, - created: `${getMonthName(date.getMonth())} ${date.getDate()}`, - id: Math.floor(Math.random() * 100), - state: 'pending' - }); - - // You might also check that the id is not taken. - // Or put in a more random number. - localStorage.setItem('todos', JSON.stringify(this.todos)); - this.todoText = ''; - - bus.$emit('add-button-click', 'clicked'); - } - } - }, - - mounted: function() { - const local = JSON.parse(localStorage.getItem("todos")); - - // Null because an empty localstorage is null. - if (local !== null) { - this.todos = local; - } - }, + template: "#todo-editor-template", + methods: { + addTodo: function () { + if (!this.todoText) { + alert("We need something here."); + // here we can put an alert from sweet alert library, you know it? + // No, but I can look when I'm done + // Ok, here is the link https://sweetalert2.github.io/ + } else { + const months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ]; - data: function() { - return { - todoText: "", - todos: [], - + function getMonthName (ind) { + return months[ind]; } + + const date = new Date(); + + this.todos = JSON.parse(localStorage.getItem("todos")); + + // The newest todos go to the front of the list + this.todos.push({ + name: this.todoText, + created: `${getMonthName(date.getMonth())} ${date.getDate()}`, + id: Math.floor(Math.random() * 100), + state: "pending" + }); + + // You might also check that the id is not taken. + // Or put in a more random number. + localStorage.setItem("todos", JSON.stringify(this.todos)); + this.todoText = ""; + + bus.$emit("add-button-click", "clicked"); + } } -} + }, + + mounted: function () { + const local = JSON.parse(localStorage.getItem("todos")); + + // Null because an empty localstorage is null. + if (local !== null) { + this.todos = local; + } + }, + + data: function () { + return { + todoText: "", + todos: [] + }; + } +}; -export { TodoEditor } \ No newline at end of file +export { TodoEditor }; diff --git a/frontend/components/todo-list.js b/frontend/components/todo-list.js index bce652e..7b57706 100644 --- a/frontend/components/todo-list.js +++ b/frontend/components/todo-list.js @@ -1,108 +1,104 @@ -import { bus } from '../scripts.js' +import { bus } from "../scripts.js"; const TodoList = { - template: '#todo-list-template', - methods: { - action(actionValue, index, todo) { - switch (actionValue) { - case 'done': - this.actionValue = "" - this.changeState(index); - break; - - case 'edit': - this.actionValue = "" - this.showTodoEditor(todo, index); - break; - - case 'delete': - this.actionValue = "" - this.deleteTodo(index, todo); - break; - - default: - console.error('actionValue undefined'); - } - - }, - - deleteTodo(ind, item) { - - this.showEditor = false; - - // TODO refactor to just use this.display - const todos = JSON.parse(localStorage.getItem("todos")); - - todos.splice(ind, 1); - - this.display = todos; - - localStorage.setItem("todos", JSON.stringify(todos)); - - console.info("Index to delete", ind, item, this.display); - - }, - - showTodoEditor(item, ind) { - console.info("edit", item); - - this.currentTodo = item; - this.showEditor = true; - - }, - - updateTodo() { - console.info("Update", this.currentTodo, this.newName); - this.currentTodo.name = this.newName; - this.display[this.editIndex] = this.currentTodo; - localStorage.setItem("todos", JSON.stringify(this.display)); - this.newName = ''; - }, - - refreshList() { - // This will load all ToDO's when the component is mounted - try { - this.display = JSON.parse(localStorage.getItem("todos")); - } - catch (err) { - return console.error(err, err.message); - } - }, - - changeState(index) { - this.display[index].state = this.display[index].state === 'pending' ? 'finished' : 'pending'; - localStorage.setItem('todos', JSON.stringify(this.display)); - } + template: "#todo-list-template", + methods: { + action (actionValue, index, todo) { + switch (actionValue) { + case "done": + this.actionValue = ""; + this.changeState(index); + break; + + case "edit": + this.actionValue = ""; + this.showTodoEditor(todo, index); + break; + + case "delete": + this.actionValue = ""; + this.deleteTodo(index, todo); + break; + + default: + console.error("actionValue undefined"); + } }, - data: function() { - return { - display: [], - showEditor: false, - currentTodo: {}, - newName: "", - editIndex: -1, - isDone: false, - actionValue: '', - actionState: [] - } + deleteTodo (ind, item) { + this.showEditor = false; + + // TODO refactor to just use this.display + const todos = JSON.parse(localStorage.getItem("todos")); + + todos.splice(ind, 1); + + this.display = todos; + + localStorage.setItem("todos", JSON.stringify(todos)); + + console.info("Index to delete", ind, item, this.display); + }, + + showTodoEditor (item, ind) { + console.info("edit", item); + + this.currentTodo = item; + this.showEditor = true; }, - mounted: function() { - this.refreshList() - - this.display.map(dis => { - this.actionState.push("") - }) - - console.info("ActionState mounted", this.actionState) - - const vm = this - - bus.$on('add-button-click', function (msg) { - vm.refreshList(); - }) + updateTodo () { + console.info("Update", this.currentTodo, this.newName); + this.currentTodo.name = this.newName; + this.display[this.editIndex] = this.currentTodo; + localStorage.setItem("todos", JSON.stringify(this.display)); + this.newName = ""; + }, + + refreshList () { + // This will load all ToDO's when the component is mounted + try { + this.display = JSON.parse(localStorage.getItem("todos")); + } catch (err) { + return console.error(err, err.message); + } + }, + + changeState (index) { + this.display[index].state = + this.display[index].state === "pending" ? "finished" : "pending"; + localStorage.setItem("todos", JSON.stringify(this.display)); } -} + }, + + data: function () { + return { + display: [], + showEditor: false, + currentTodo: {}, + newName: "", + editIndex: -1, + isDone: false, + actionValue: "", + actionState: [] + }; + }, + + mounted: function () { + this.refreshList(); + + this.display.map(dis => { + this.actionState.push(""); + }); + + console.info("ActionState mounted", this.actionState); + + const vm = this; + + bus.$on("add-button-click", function (msg) { + vm.refreshList(); + }); + } +}; -export { TodoList } \ No newline at end of file +export { TodoList }; diff --git a/frontend/index.html b/frontend/index.html index 94c78a1..9a6fb12 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,81 +1,85 @@ - - - - TODO App with VueJS - - - - - - - + + + TODO App with VueJS + + + + + + + + + +
+ + - - - - - \ No newline at end of file + + + + + + + + + + + + + + diff --git a/frontend/scripts.js b/frontend/scripts.js index ae76b17..1d5df2e 100644 --- a/frontend/scripts.js +++ b/frontend/scripts.js @@ -1,22 +1,29 @@ -import { TodoEditor } from './components/todo-editor.js' -import { TodoList } from './components/todo-list.js' +/* eslint-disable no-unused-vars */ +import { TodoEditor } from "./components/todo-editor.js"; +import { TodoList } from "./components/todo-list.js"; const bus = new Vue(); -const signupFormTemplate = {"fullName":"","emailAddress":"","password":"","confirmPassword":""} +const signupFormTemplate = { + fullName: "", + emailAddress: "", + password: "", + confirmPassword: "" +}; +// eslint-disable-next-line no-new new Vue({ - el: '#app', - components: { - 'todo-editor': TodoEditor, - 'todo-list': TodoList - }, - data: function() { - return { - showSignup: false - } - }, - template: ` + el: "#app", + components: { + "todo-editor": TodoEditor, + "todo-list": TodoList + }, + data: function () { + return { + showSignup: false + }; + }, + template: `
` -}) +}); -export { bus } \ No newline at end of file +export { bus }; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ef3a5a2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1386 @@ +{ + "name": "vuejs-todo-app", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/highlight": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", + "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "acorn": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.4.tgz", + "integrity": "sha512-VY4i5EKSKkofY2I+6QLTbTTN/UvEQPCo6eiwzzSaSWfpaDhOmStMCMod6wmuPciNq+XS0faCglFu2lHZpdHUtg==", + "dev": true + }, + "acorn-jsx": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz", + "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==", + "dev": true + }, + "ajv": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.2.tgz", + "integrity": "sha512-FBHEW6Jf5TB9MGBgUUA9XHkTbjXYfAUjY43ACMfmdMRHniyoMHjHjzD50OK8LGDWQwp4rWEsIq5kEqq7rvIM1g==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-escapes": { + "version": "3.1.0", + "resolved": "http://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", + "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==", + "dev": true + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "^0.2.0" + } + }, + "callsites": { + "version": "0.2.0", + "resolved": "http://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "debug": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz", + "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.10.0.tgz", + "integrity": "sha512-HpqzC+BHULKlnPwWae9MaVZ5AXJKpkxCVXQHrFaRw3hbDj26V/9ArYM4Rr/SQ8pi6qUPLXSSXC4RBJlyq2Z2OQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.5.3", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^2.1.0", + "eslint-scope": "^4.0.0", + "eslint-utils": "^1.3.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^5.0.0", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^2.0.0", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.7.0", + "ignore": "^4.0.6", + "imurmurhash": "^0.1.4", + "inquirer": "^6.1.0", + "js-yaml": "^3.12.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.5", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "pluralize": "^7.0.0", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "require-uncached": "^1.0.3", + "semver": "^5.5.1", + "strip-ansi": "^4.0.0", + "strip-json-comments": "^2.0.1", + "table": "^5.0.2", + "text-table": "^0.2.0" + } + }, + "eslint-config-standard": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-12.0.0.tgz", + "integrity": "sha512-COUz8FnXhqFitYj4DTqHzidjIL/t4mumGZto5c7DrBpvWoie+Sn3P4sLEzUGeYhRElWuFEf8K1S1EfvD1vixCQ==", + "dev": true + }, + "eslint-import-resolver-node": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", + "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "resolve": "^1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-module-utils": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.2.0.tgz", + "integrity": "sha1-snA2LNiLGkitMIl2zn+lTphBF0Y=", + "dev": true, + "requires": { + "debug": "^2.6.8", + "pkg-dir": "^1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-plugin-es": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-1.4.0.tgz", + "integrity": "sha512-XfFmgFdIUDgvaRAlaXUkxrRg5JSADoRC8IkKLc/cISeR3yHVMefFHQZpcyXXEUUPHfy5DwviBcrfqlyqEwlQVw==", + "dev": true, + "requires": { + "eslint-utils": "^1.3.0", + "regexpp": "^2.0.1" + } + }, + "eslint-plugin-import": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.14.0.tgz", + "integrity": "sha512-FpuRtniD/AY6sXByma2Wr0TXvXJ4nA/2/04VPlfpmUDPOpOY264x+ILiwnrk/k4RINgDAyFZByxqPUbSQ5YE7g==", + "dev": true, + "requires": { + "contains-path": "^0.1.0", + "debug": "^2.6.8", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.1", + "eslint-module-utils": "^2.2.0", + "has": "^1.0.1", + "lodash": "^4.17.4", + "minimatch": "^3.0.3", + "read-pkg-up": "^2.0.0", + "resolve": "^1.6.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "1.5.0", + "resolved": "http://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-plugin-node": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-8.0.0.tgz", + "integrity": "sha512-Y+ln8iQ52scz9+rSPnSWRaAxeWaoJZ4wIveDR0vLHkuSZGe44Vk1J4HX7WvEP5Cm+iXPE8ixo7OM7gAO3/OKpQ==", + "dev": true, + "requires": { + "eslint-plugin-es": "^1.3.1", + "eslint-utils": "^1.3.1", + "ignore": "^5.0.2", + "minimatch": "^3.0.4", + "resolve": "^1.8.1", + "semver": "^5.5.0" + }, + "dependencies": { + "ignore": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.0.4.tgz", + "integrity": "sha512-WLsTMEhsQuXpCiG173+f3aymI43SXa+fB1rSfbzyP4GkPP+ZFVuO0/3sFUGNBtifisPeDcl/uD/Y2NxZ7xFq4g==", + "dev": true + } + } + }, + "eslint-plugin-promise": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.0.1.tgz", + "integrity": "sha512-Si16O0+Hqz1gDHsys6RtFRrW7cCTB6P7p3OJmKp3Y3dxpQE2qwOA7d3xnV+0mBmrPoi0RBnxlCKvqu70te6wjg==", + "dev": true + }, + "eslint-plugin-standard": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-4.0.0.tgz", + "integrity": "sha512-OwxJkR6TQiYMmt1EsNRMe5qG3GsbjlcOhbGUBY4LtavF9DsLaTcoR+j2Tdjqi23oUwKNUqX7qcn5fPStafMdlA==", + "dev": true + }, + "eslint-scope": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.0.tgz", + "integrity": "sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz", + "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==", + "dev": true + }, + "eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", + "dev": true + }, + "espree": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.0.tgz", + "integrity": "sha512-1MpUfwsdS9MMoN7ZXqAr9e9UKdVHDcvrJpyx7mm1WuQlx/ygErEQBzgi5Nh5qBHIoYweprhtMkTCb9GhcAIcsA==", + "dev": true, + "requires": { + "acorn": "^6.0.2", + "acorn-jsx": "^5.0.0", + "eslint-visitor-keys": "^1.0.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "dev": true, + "requires": { + "estraverse": "^4.0.0" + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "external-editor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz", + "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true, + "requires": { + "flat-cache": "^1.2.1", + "object-assign": "^4.0.1" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "flat-cache": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", + "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", + "dev": true, + "requires": { + "circular-json": "^0.3.1", + "graceful-fs": "^4.1.2", + "rimraf": "~2.6.2", + "write": "^0.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globals": { + "version": "11.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.9.0.tgz", + "integrity": "sha512-5cJVtyXWH8PiJPVLZzzoIizXx944O4OmRro5MWKx5fT4MgcN7OfaMutPeaTdJCCURwbWdhhcCWcKIffPnmTzBg==", + "dev": true + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "hosted-git-info": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "inquirer": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.1.tgz", + "integrity": "sha512-088kl3DRT2dLU5riVMKKr1DlImd6X7smDhpXUCkJDCKvTEJeRiXh0G132HG9u5a+6Ylw9plFRY7RuTnwohYSpg==", + "dev": true, + "requires": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.0", + "figures": "^2.0.0", + "lodash": "^4.17.10", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.1.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.0.0.tgz", + "integrity": "sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w==", + "dev": true + }, + "strip-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.0.0.tgz", + "integrity": "sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow==", + "dev": true, + "requires": { + "ansi-regex": "^4.0.0" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "http://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pkg-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", + "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", + "dev": true, + "requires": { + "find-up": "^1.0.0" + } + }, + "pluralize": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", + "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + } + } + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "require-uncached": { + "version": "1.0.3", + "resolved": "http://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "^0.1.0", + "resolve-from": "^1.0.0" + } + }, + "resolve": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.9.0.tgz", + "integrity": "sha512-TZNye00tI67lwYvzxCxHGjwTNlUV70io54/Ed4j6PscB8xVfuBJpRenI/o6dVk0cY0PYTY27AgCoGGxRnYuItQ==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "^7.0.5" + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "^2.1.0" + } + }, + "rxjs": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz", + "integrity": "sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "slice-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.0.0.tgz", + "integrity": "sha512-4j2WTWjp3GsZ+AOagyzVbzp4vWGtZ0hEZ/gDY/uTvm6MTxUfTUIsnMIFb1bn8o0RuXiqUw15H1bue8f22Vw2oQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + } + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.2.tgz", + "integrity": "sha512-qky9CVt0lVIECkEsYbNILVnPvycuEBkXoMFLRWsREkomQLevYhtRKC+R91a5TOAQ3bCMjikRwhyaRqj1VYatYg==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "http://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "table": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/table/-/table-5.1.1.tgz", + "integrity": "sha512-NUjapYb/qd4PeFW03HnAuOJ7OMcBkJlqeClWxeNlQ0lXGSb52oZXGzkO0/I0ARegQ2eUT1g2VDJH0eUxDRcHmw==", + "dev": true, + "requires": { + "ajv": "^6.6.1", + "lodash": "^4.17.11", + "slice-ansi": "2.0.0", + "string-width": "^2.1.1" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a2c5771 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "vuejs-todo-app", + "version": "1.0.0", + "description": "A todo app in Vue.js", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/arswaw/todo.git" + }, + "author": "Alex and Jose", + "license": "MIT", + "bugs": { + "url": "https://github.com/arswaw/todo/issues" + }, + "homepage": "https://github.com/arswaw/todo#readme", + "devDependencies": { + "eslint": "^5.10.0", + "eslint-config-standard": "^12.0.0", + "eslint-plugin-import": "^2.14.0", + "eslint-plugin-node": "^8.0.0", + "eslint-plugin-promise": "^4.0.1", + "eslint-plugin-standard": "^4.0.0" + } +}