diff --git a/.env.example b/.env.example index 544b511..95f79df 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,8 @@ APP_PORT = - DB_URI = +SECRET = -SECRET = \ No newline at end of file +GOOGLE_ENABLED = +GOOGLE_CLIENT_ID = +GOOGLE_CLIENT_SECRET = +HOST = \ No newline at end of file diff --git a/app.js b/app.js index 0f2c89f..3688b04 100644 --- a/app.js +++ b/app.js @@ -74,6 +74,7 @@ app.use((error, req, res, next) => { switch (error.name) { case 'ValidationError': case 'MissingPasswordError': + case 'BadRequest': case 'BadRequestError': res.status(400); res.json({ success: false, error }); @@ -91,6 +92,12 @@ app.use((error, req, res, next) => { res.status(500); res.json({ success: false, error }); } + if ( + process.env.NODE_ENV === 'production' || + process.env.NODE_ENV === 'test' + ) { + console.error(error); + } next(error); }); diff --git a/config/index.js b/config/index.js index c94548a..9162ec6 100644 --- a/config/index.js +++ b/config/index.js @@ -10,6 +10,12 @@ const production = { process.env.MONGODB_URI || 'mongodb://localhost/todolist', }, + googleOAuth: { + googleEnabled: process.env.GOOGLE_ENABLED, + googleClientId: process.env.GOOGLE_CLIENT_ID, + googleClientSecret: process.env.GOOGLE_CLIENT_SECRET, + googleCallback: `${process.env.HOST}/api/users/login/google/callback`, + }, secret: process.env.SECRET, }; diff --git a/config/passport.js b/config/passport.js index a4fe1b5..fc94e80 100644 --- a/config/passport.js +++ b/config/passport.js @@ -1,8 +1,31 @@ const passport = require('passport'); const mongoose = require('mongoose'); +const GoogleStrategy = require('passport-google-oauth').OAuth2Strategy; +const { + googleClientId, + googleClientSecret, + googleCallback, + googleEnabled, +} = require('./').googleOAuth; const User = mongoose.model('User'); passport.use(User.createStrategy()); +if (googleEnabled) { + passport.use( + new GoogleStrategy( + { + clientID: googleClientId, + clientSecret: googleClientSecret, + callbackURL: googleCallback, + }, + (accessToken, refreshToken, profile, done) => { + User.findOrCreate({ googleId: profile.id }, (err, user) => + done(err, user), + ); + }, + ), + ); +} module.exports = passport; diff --git a/models/User.js b/models/User.js index ff5d7b8..5bc4911 100644 --- a/models/User.js +++ b/models/User.js @@ -2,6 +2,8 @@ const mongoose = require('mongoose'); const passportLocalMongoose = require('passport-local-mongoose'); const jwt = require('jsonwebtoken'); const uniqueValidator = require('mongoose-unique-validator'); +const findOrCreate = require('mongoose-findorcreate'); +const { BadRequestError } = require('../errors'); const { secret } = require('../config'); @@ -10,13 +12,17 @@ const { Schema } = mongoose; const UserSchema = Schema({ username: { type: String, - required: true, unique: true, validate: /^\S*$/, minLength: 3, maxLength: 50, trim: true, }, + googleId: { + type: String, + unique: true, + sparse: true, + }, lists: [{ type: Schema.Types.ObjectId, ref: 'TodoList' }], todos: [{ type: Schema.Types.ObjectId, ref: 'Todo' }], }); @@ -26,6 +32,13 @@ UserSchema.plugin(passportLocalMongoose, { maxAttempts: 20, }); UserSchema.plugin(uniqueValidator); +UserSchema.plugin(findOrCreate); + +UserSchema.pre('validate', async function() { + if (!this.username && !this.googleId) { + throw new BadRequestError('username is required'); + } +}); UserSchema.pre('remove', async function() { await this.model('TodoList') diff --git a/package-lock.json b/package-lock.json index 60df10d..ce0eeca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -171,6 +171,15 @@ } } }, + "agent-base": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", + "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", + "dev": true, + "requires": { + "es6-promisify": "^5.0.0" + } + }, "ajv": { "version": "5.5.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", @@ -1075,10 +1084,9 @@ } }, "bluebird": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", - "dev": true + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", + "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" }, "body-parser": { "version": "1.18.3", @@ -2246,6 +2254,21 @@ "is-symbol": "^1.0.1" } }, + "es6-promise": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", + "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==", + "dev": true + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "dev": true, + "requires": { + "es6-promise": "^4.0.3" + } + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2409,9 +2432,9 @@ "dev": true }, "eslint-plugin-import": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.12.0.tgz", - "integrity": "sha1-2tMXgSktZmSyUxf9BJ0uKy8CIF0=", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.13.0.tgz", + "integrity": "sha512-t6hGKQDMIt9N8R7vLepsYXgDfeuhp6ZJSgtrLEDxonpSubyxUZHjhm6LsAaZX8q6GYVxkbT3kTsV9G5mBCFR6A==", "dev": true, "requires": { "contains-path": "^0.1.0", @@ -2445,9 +2468,9 @@ "dev": true }, "eslint-plugin-prettier": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-2.6.1.tgz", - "integrity": "sha512-wNZ2z0oVCWnf+3BSI7roS+z4gGu2AwcPKUek+SlLZMZg+X0KbZLsB2knul7fd0K3iuIp402HIYzm4f2+OyfXxA==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-2.6.2.tgz", + "integrity": "sha512-tGek5clmW5swrAx1mdPYM8oThrBE83ePh7LeseZHBWfHVGrHPhKn7Y5zgRMbU/9D5Td9K4CEmUPjGxA7iw98Og==", "dev": true, "requires": { "fast-diff": "^1.1.1", @@ -3946,6 +3969,27 @@ "sshpk": "^1.7.0" } }, + "https-proxy-agent": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", + "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", + "dev": true, + "requires": { + "agent-base": "^4.1.0", + "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, "iconv-lite": { "version": "0.4.23", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", @@ -5519,26 +5563,27 @@ } }, "mongodb": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.0.10.tgz", - "integrity": "sha512-jy9s4FgcM4rl8sHNETYHGeWcuRh9AlwQCUuMiTj041t/HD02HwyFgmm2VZdd9/mA9YNHaUJLqj0tzBx2QFivtg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.1.0.tgz", + "integrity": "sha512-fSDZRq9FomRqeDSM7MpMTLa8sz+STs3nZ7Ib0+xvmaKZ6nquNDN4zGDsVhjto6UozFvHMDYJMAfJwhqUygXs9g==", "requires": { - "mongodb-core": "3.0.9" + "mongodb-core": "3.1.0" } }, "mongodb-core": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.0.9.tgz", - "integrity": "sha512-buOWjdLLBlEqjHDeHYSXqXx173wHMVp7bafhdHxSjxWdB9V6Ri4myTqxjYZwL/eGFZxvd8oRQSuhwuIDbaaB+g==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.1.0.tgz", + "integrity": "sha512-qRjG62Fu//CZhkgn0jA/k8jh5MhACIq8cOJUryH6sck87pgt+C222MSD02tsCq5zNo/B6ZFHtNodZ2qpf8E86g==", "requires": { "bson": "~1.0.4", - "require_optional": "^1.0.1" + "require_optional": "^1.0.1", + "saslprep": "^1.0.0" } }, "mongodb-memory-server": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-1.8.0.tgz", - "integrity": "sha512-3Pzgv7UruHu99aJ4yVPT4xmd1cNt8QBXZvYbB3Vs4IvPcA0h1wT8+I3zguwqQPIHpeqFXafVfjIiUfXiax3bjA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-1.9.0.tgz", + "integrity": "sha512-sVAOe68NoQwgORI9YP/FkOZi7YNX5efD452FKS/WPBXlIPqt/baoHWzP+GnmHHqyMrUEdPJdTmcOZVOoVs/aGw==", "dev": true, "requires": { "babel-runtime": "^6.26.0", @@ -5547,11 +5592,10 @@ "fs-extra": "^6.0.1", "get-port": "^3.2.0", "getos": "^3.1.0", + "https-proxy-agent": "^2.2.1", "lockfile": "^1.0.4", "md5-file": "^4.0.0", "mkdirp": "^0.5.1", - "request": "^2.87.0", - "request-promise": "^4.2.2", "tmp": "^0.0.33", "uuid": "^3.2.1" }, @@ -5564,47 +5608,20 @@ "requires": { "ms": "2.0.0" } - }, - "request": { - "version": "2.87.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", - "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.6.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.1", - "forever-agent": "~0.6.1", - "form-data": "~2.3.1", - "har-validator": "~5.0.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.17", - "oauth-sign": "~0.8.2", - "performance-now": "^2.1.0", - "qs": "~6.5.1", - "safe-buffer": "^5.1.1", - "tough-cookie": "~2.3.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.1.0" - } } } }, "mongoose": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.1.6.tgz", - "integrity": "sha512-p8p/3Z2kfXViqawN1TV+cZ8XbHz6SsllkytKTog+CDWfCNObyGraHQlUuRv/9aYPNKiZfq6WWITgLpJLZW/o/A==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.2.1.tgz", + "integrity": "sha512-WB5F/T2B3W7p2+uftd3WkIrNLhg8VzSbxxtFGbTIqsZ4KCOhjhYALN0ltZPLaBlIrLtEoGFKTNwyWEcOtxY+oA==", "requires": { "async": "2.6.1", "bson": "~1.0.5", "kareem": "2.2.1", "lodash.get": "4.4.2", - "mongodb": "3.0.10", + "mongodb": "3.1.0", + "mongodb-core": "3.1.0", "mongoose-legacy-pluralize": "1.0.2", "mpath": "0.4.1", "mquery": "3.0.0", @@ -5623,6 +5640,11 @@ } } }, + "mongoose-findorcreate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mongoose-findorcreate/-/mongoose-findorcreate-3.0.0.tgz", + "integrity": "sha512-kQhDe5XDj6tMv8kq1wjK+hITGIGUl60rj8oGLupF9poNsqIDkAJBXudZKcCdSyBZ7p6DLK2+0jSBthrb26tSYQ==" + }, "mongoose-legacy-pluralize": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz", @@ -5665,11 +5687,6 @@ "sliced": "0.0.5" }, "dependencies": { - "bluebird": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", - "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" - }, "sliced": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/sliced/-/sliced-0.0.5.tgz", @@ -5859,6 +5876,11 @@ "integrity": "sha512-3iuY4N5dhgMpCUrOVnuAdGrgxVqV2cJpM+XNccjR2DKOB1RUP0aA+wGXEiNziG/UKboFyGBIoKOaNlJxx8bciQ==", "dev": true }, + "oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE=" + }, "oauth-sign": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", @@ -6127,6 +6149,31 @@ "pause": "0.0.1" } }, + "passport-google-oauth": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth/-/passport-google-oauth-1.0.0.tgz", + "integrity": "sha1-ZfUGMxkq0GJ6GLCJYAdxCdhOt20=", + "requires": { + "passport-google-oauth1": "1.x.x", + "passport-google-oauth20": "1.x.x" + } + }, + "passport-google-oauth1": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth1/-/passport-google-oauth1-1.0.0.tgz", + "integrity": "sha1-r3SoA99R7GRvZqRNgigr5vEI4Mw=", + "requires": { + "passport-oauth1": "1.x.x" + } + }, + "passport-google-oauth20": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-1.0.0.tgz", + "integrity": "sha1-O5YOih1w0dvnlGFcgnxoxAOSpdA=", + "requires": { + "passport-oauth2": "1.x.x" + } + }, "passport-local": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", @@ -6157,6 +6204,27 @@ } } }, + "passport-oauth1": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/passport-oauth1/-/passport-oauth1-1.1.0.tgz", + "integrity": "sha1-p96YiiEfnPRoc3cTDqdN8ycwyRg=", + "requires": { + "oauth": "0.9.x", + "passport-strategy": "1.x.x", + "utils-merge": "1.x.x" + } + }, + "passport-oauth2": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.4.0.tgz", + "integrity": "sha1-9i+BWDy+EmCb585vFguTlaJ7hq0=", + "requires": { + "oauth": "0.9.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + } + }, "passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", @@ -6690,18 +6758,6 @@ "uuid": "^3.1.0" } }, - "request-promise": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.2.tgz", - "integrity": "sha1-0epG1lSm7k+O5qT+oQGMIpEZBLQ=", - "dev": true, - "requires": { - "bluebird": "^3.5.0", - "request-promise-core": "1.1.1", - "stealthy-require": "^1.1.0", - "tough-cookie": ">=2.3.3" - } - }, "request-promise-core": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", @@ -6768,9 +6824,9 @@ } }, "resolve": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", - "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", + "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", "dev": true, "requires": { "path-parse": "^1.0.5" @@ -7187,6 +7243,12 @@ } } }, + "saslprep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.0.tgz", + "integrity": "sha512-5lvKUEQ7lAN5/vPl5d3k8FQeDbEamu9kizfATfLLWV5h6Mkh1xcieR1FSsJkcSRUk49lF2tAW8gzXWVwtwZVhw==", + "optional": true + }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -8354,6 +8416,11 @@ "dev": true, "optional": true }, + "uid2": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", + "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=" + }, "unbzip2-stream": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.2.5.tgz", @@ -8418,9 +8485,9 @@ } }, "universalify": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", - "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true }, "unpipe": { @@ -8894,9 +8961,9 @@ } }, "yauzl": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.9.2.tgz", - "integrity": "sha1-T7G8euH8L1cDe1SvasyP4QMcW3c=", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", "dev": true, "requires": { "buffer-crc32": "~0.2.3", diff --git a/package.json b/package.json index 4bbe124..87502c4 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,12 @@ "express-jwt": "^5.3.1", "hsts": "^2.1.0", "jsonwebtoken": "^8.3.0", - "mongoose": "^5.1.6", + "mongoose": "^5.2.1", + "mongoose-findorcreate": "^3.0.0", "mongoose-unique-validator": "^2.0.1", "morgan": "^1.9.0", "passport": "^0.4.0", + "passport-google-oauth": "^1.0.0", "passport-local": "^1.0.0", "passport-local-mongoose": "^5.0.1" }, @@ -41,16 +43,19 @@ "eslint-config-airbnb-base": "^12.1.0", "eslint-config-node": "^2.0.0", "eslint-config-prettier": "^2.9.0", - "eslint-plugin-import": "^2.12.0", + "eslint-plugin-import": "^2.13.0", "eslint-plugin-jest": "^21.17.0", - "eslint-plugin-prettier": "^2.6.1", + "eslint-plugin-prettier": "^2.6.2", "jest": "^22.4.4", - "mongodb-memory-server": "^1.8.0", + "mongodb-memory-server": "^1.9.0", "nodemon": "^1.17.5", "prettier-eslint": "^8.8.2", "supertest": "^3.1.0" }, "jest": { - "testEnvironment": "node" + "testEnvironment": "node", + "roots": [ + "/tests/" + ] } } diff --git a/react/package-lock.json b/react/package-lock.json index 226f114..2e06f9b 100644 --- a/react/package-lock.json +++ b/react/package-lock.json @@ -5,25 +5,30 @@ "requires": true, "dependencies": { "@babel/runtime": { - "version": "7.0.0-beta.51", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0-beta.51.tgz", - "integrity": "sha1-SLjtGDBwNMZiD2Q1FGUMoszAFlo=", + "version": "7.0.0-beta.52", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0-beta.52.tgz", + "integrity": "sha1-PztCuCuStOGig/x43xuy/Uuo0Mc=", "requires": { "core-js": "^2.5.7", - "regenerator-runtime": "^0.11.1" + "regenerator-runtime": "^0.12.0" }, "dependencies": { "core-js": { "version": "2.5.7", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" + }, + "regenerator-runtime": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.0.tgz", + "integrity": "sha512-SpV2LhF5Dm9UYMEprB3WwsBnWwqTrmjrm2UZb42cl2G02WVGgx7Mg8aa9pdLEKp6hZ+/abcMc2NxKA8f02EG2w==" } } }, "@material-ui/core": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-1.2.3.tgz", - "integrity": "sha512-5Z4LhIrFJcvp1a7E8C3DPxL4W0RkjxWO9OwqOlRsr8YCF2sJgqCMDWn8DMW9eg1VD50JnZQ8bmx1esE0GBo71Q==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-1.3.1.tgz", + "integrity": "sha512-h5pVkHgYrKExTdll4Y2Kmvkd5Hr4MxqEQLhRxzGTaXJ8RjOuRd+plfRk5r5ZauAdrIkKEsNcEt75VlEFX9aSGw==", "requires": { "@babel/runtime": "^7.0.0-beta.42", "@types/jss": "^9.5.3", @@ -49,7 +54,7 @@ "react-jss": "^8.1.0", "react-popper": "^0.10.0", "react-transition-group": "^2.2.1", - "recompose": "^0.26.0 || ^0.27.0", + "recompose": "^0.27.0", "scroll": "^2.0.3", "warning": "^4.0.1" } @@ -63,9 +68,9 @@ } }, "@redux-offline/redux-offline": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@redux-offline/redux-offline/-/redux-offline-2.3.3.tgz", - "integrity": "sha512-uv0DW9ZAFzL+lc7WzjQoDoWydReJiZe+Rpz6suGCS5ux+ZJWkr3jpRzeWlTW149uo+OrEk1BchNAE7FCTakXjQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@redux-offline/redux-offline/-/redux-offline-2.4.0.tgz", + "integrity": "sha512-idNVqcRax5bxHMu6nF6Lrxe6UfDU+Ha4TdJEJgysq0A1vvLbh6HFrRU02T+wBKA6j/L+kOZwzlDmnjYuyI1a6w==", "requires": { "babel-runtime": "^6.26.0", "redux-persist": "^4.5.0" @@ -81,9 +86,9 @@ } }, "@types/react": { - "version": "16.4.1", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.4.1.tgz", - "integrity": "sha512-uZP8Fd4f7rwHKztnOhFJYEJsKXO7opmcyKk5P9vRC8UJAx3AiWaGFiLxDqPJqzO3n3IhF/v6rdscxadarEXnag==", + "version": "16.4.6", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.4.6.tgz", + "integrity": "sha512-9LDZdhsuKSc+DjY65SjBkA958oBWcTWSVWAd2cD9XqKBjhGw1KzAkRhWRw2eIsXvaIE/TOTjjKMFVC+JA1iU4g==", "requires": { "csstype": "^2.2.0" } @@ -3364,9 +3369,9 @@ } }, "eslint-plugin-import": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.12.0.tgz", - "integrity": "sha1-2tMXgSktZmSyUxf9BJ0uKy8CIF0=", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.13.0.tgz", + "integrity": "sha512-t6hGKQDMIt9N8R7vLepsYXgDfeuhp6ZJSgtrLEDxonpSubyxUZHjhm6LsAaZX8q6GYVxkbT3kTsV9G5mBCFR6A==", "dev": true, "requires": { "contains-path": "^0.1.0", @@ -3448,30 +3453,61 @@ "dev": true }, "eslint-plugin-jsx-a11y": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.0.3.tgz", - "integrity": "sha1-VFg9GuRCSDFi4EDhPMMYZUZRAOU=", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.1.0.tgz", + "integrity": "sha512-hnhf28u7Z9zlh7Y56tETrwnPeBvXgcqlP7ntHvZsWQs/n/p/vPnfNMNFWTqJAFcbd8PrDEifX1NRGHsjnUmqMw==", "dev": true, "requires": { - "aria-query": "^0.7.0", + "aria-query": "^3.0.0", "array-includes": "^3.0.3", - "ast-types-flow": "0.0.7", - "axobject-query": "^0.1.0", - "damerau-levenshtein": "^1.0.0", - "emoji-regex": "^6.1.0", - "jsx-ast-utils": "^2.0.0" + "ast-types-flow": "^0.0.7", + "axobject-query": "^2.0.1", + "damerau-levenshtein": "^1.0.4", + "emoji-regex": "^6.5.1", + "has": "^1.0.3", + "jsx-ast-utils": "^2.0.1" + }, + "dependencies": { + "aria-query": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", + "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=", + "dev": true, + "requires": { + "ast-types-flow": "0.0.7", + "commander": "^2.11.0" + } + }, + "axobject-query": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.1.tgz", + "integrity": "sha1-Bd+nBa2orZ25k/polvItOVsLCgc=", + "dev": true, + "requires": { + "ast-types-flow": "0.0.7" + } + }, + "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" + } + } } }, "eslint-plugin-react": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.9.1.tgz", - "integrity": "sha512-uvq+2ZkiqzjwF+pMZ8xqIC3pChV4KviPvvPIyQOvKWnjtvyW3iGfHIRqVumw05L3itby0QGmA4VdBA9m1OdMmg==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.10.0.tgz", + "integrity": "sha512-18rzWn4AtbSUxFKKM7aCVcj5LXOhOKdwBino3KKWy4psxfPW0YtIbE8WNRDUdyHFL50BeLb6qFd4vpvNYyp7hw==", "dev": true, "requires": { "doctrine": "^2.1.0", - "has": "^1.0.2", + "has": "^1.0.3", "jsx-ast-utils": "^2.0.1", - "prop-types": "^15.6.1" + "prop-types": "^15.6.2" }, "dependencies": { "has": { @@ -4146,13 +4182,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4165,18 +4199,15 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "optional": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -4279,8 +4310,7 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -4290,7 +4320,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4303,20 +4332,17 @@ "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true, - "optional": true + "bundled": true }, "minipass": { "version": "2.2.4", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -4333,7 +4359,6 @@ "mkdirp": { "version": "0.5.1", "bundled": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -4406,8 +4431,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -4417,7 +4441,6 @@ "once": { "version": "1.4.0", "bundled": true, - "optional": true, "requires": { "wrappy": "1" } @@ -4523,7 +4546,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -9424,9 +9446,9 @@ } }, "react-jss": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/react-jss/-/react-jss-8.5.1.tgz", - "integrity": "sha512-5R3qCdGkE+K0+B4tuRyx8idLV7q2pT1QbGomGqberCQ/xLKEQbDukH7ER2QLkpIYqtRkeciG9S03uDJwC1o2gw==", + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/react-jss/-/react-jss-8.6.1.tgz", + "integrity": "sha512-SH6XrJDJkAphp602J14JTy3puB2Zxz1FkM3bKVE8wON+va99jnUTKWnzGECb3NfIn9JPR5vHykge7K3/A747xQ==", "requires": { "hoist-non-react-statics": "^2.5.0", "jss": "^9.7.0", @@ -9673,19 +9695,19 @@ } }, "react-spring": { - "version": "5.3.18", - "resolved": "https://registry.npmjs.org/react-spring/-/react-spring-5.3.18.tgz", - "integrity": "sha512-gPLxo0wk1OYK1b3ZL9emWIAoWuQvTjuLDT8+yKXG+PTyEOYHvhaqEWShykL4bN6AUjPJyY4+7CLioHxJeAefZA==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/react-spring/-/react-spring-5.4.0.tgz", + "integrity": "sha512-Ws5+L4x/M9C1yVR6BNzf96er8yO/7euGS1Uv11fMoVDdS6/SgX0JeNszJo7o6maG+v7VxoIbvhyG3wVW8wQZFQ==", "requires": { - "@babel/runtime": "7.0.0-beta.49" + "@babel/runtime": "7.0.0-beta.51" }, "dependencies": { "@babel/runtime": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0-beta.49.tgz", - "integrity": "sha1-A7O/B+uYIHLI6FHdLd1RECguYb8=", + "version": "7.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0-beta.51.tgz", + "integrity": "sha1-SLjtGDBwNMZiD2Q1FGUMoszAFlo=", "requires": { - "core-js": "^2.5.6", + "core-js": "^2.5.7", "regenerator-runtime": "^0.11.1" } }, @@ -9697,13 +9719,14 @@ } }, "react-transition-group": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.3.1.tgz", - "integrity": "sha512-hu4/LAOFSKjWt1+1hgnOv3ldxmt6lvZGTWz4KUkFrqzXrNDIVSu6txIcPszw7PNduR8en9YTN55JLRyd/L1ZiQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.4.0.tgz", + "integrity": "sha512-Xv5d55NkJUxUzLCImGSanK8Cl/30sgpOEMGc5m86t8+kZwrPxPCPcFqyx83kkr+5Lz5gs6djuvE5By+gce+VjA==", "requires": { "dom-helpers": "^3.3.1", "loose-envify": "^1.3.1", - "prop-types": "^15.6.1" + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" } }, "read-pkg": { diff --git a/react/package.json b/react/package.json index 1ac30e8..d252431 100644 --- a/react/package.json +++ b/react/package.json @@ -3,9 +3,9 @@ "version": "0.1.0", "private": true, "dependencies": { - "@material-ui/core": "^1.2.3", + "@material-ui/core": "^1.3.1", "@material-ui/icons": "^1.1.0", - "@redux-offline/redux-offline": "^2.3.3", + "@redux-offline/redux-offline": "^2.4.0", "localforage": "^1.7.2", "prop-types": "^15.6.2", "react": "^16.4.1", @@ -14,7 +14,7 @@ "react-router-dom": "^4.3.1", "react-router-redux": "^4.0.8", "react-scripts": "1.1.4", - "react-spring": "^5.3.18", + "react-spring": "^5.4.0", "redux": "^4.0.0", "redux-form": "^7.4.2", "redux-thunk": "^2.3.0" @@ -25,14 +25,19 @@ "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, - "proxy": "http://localhost:4000", + "proxy": { + "/api": { + "target": "http://localhost:4000", + "ws": true + } + }, "devDependencies": { "eslint-config-airbnb": "^16.1.0", "eslint-config-prettier": "^2.9.0", - "eslint-plugin-import": "^2.12.0", + "eslint-plugin-import": "^2.13.0", "eslint-plugin-jest": "^21.17.0", - "eslint-plugin-jsx-a11y": "^6.0.3", - "eslint-plugin-react": "^7.9.1", + "eslint-plugin-jsx-a11y": "^6.1.0", + "eslint-plugin-react": "^7.10.0", "prettier-eslint": "^8.8.2" } } diff --git a/react/src/actions/user.js b/react/src/actions/user.js index 767b351..bfd111c 100644 --- a/react/src/actions/user.js +++ b/react/src/actions/user.js @@ -71,6 +71,27 @@ export function login(user) { }; } +export function loginJWT(jwt) { + return async dispatch => { + dispatch(startLogin()); + const response = await fetch(`${API_ROOT}/users/user`, { + headers: { + 'content-type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + method: 'GET', + }); + const json = await response.json(); + if (json.success) { + setToken(jwt); + dispatch(loginSuccess(json.data)); + dispatch(fetchLists()); + } else { + dispatch(loginFail(json.error)); + } + }; +} + function signupSuccess(user) { return { type: SIGNUP_SUCCESS, user }; } diff --git a/react/src/components/Form.css b/react/src/components/Form.css index bdba95a..6311fa6 100644 --- a/react/src/components/Form.css +++ b/react/src/components/Form.css @@ -16,8 +16,18 @@ form { color: red; } +#googlebutton { + margin: auto; + margin-left: 0rem; +} + #submitbutton { margin: auto; - margin-top: 1rem; - margin-right: 0.5rem; + margin-right: 0rem; +} + +#buttons { + margin-top: 1rem; + display: flex; + justify-content: space-around; } diff --git a/react/src/components/Todos.js b/react/src/components/Todos.js index 069e08c..1e9ff31 100644 --- a/react/src/components/Todos.js +++ b/react/src/components/Todos.js @@ -8,7 +8,7 @@ import Header from './Header'; export default class Todos extends React.Component { componentDidUpdate() { if (!this.props.user.user && !this.props.user.dirty) { - this.props.history.push('/login'); + this.props.history.replace('/login'); } } render() { diff --git a/react/src/components/user/InputField.js b/react/src/components/user/InputField.js index 4fa69d8..d468d10 100644 --- a/react/src/components/user/InputField.js +++ b/react/src/components/user/InputField.js @@ -11,7 +11,13 @@ export default function InputField({ }) { return ( - + {touched && error && {error}} ); diff --git a/react/src/components/user/LoginForm.js b/react/src/components/user/LoginForm.js index fe129c7..5701a3a 100644 --- a/react/src/components/user/LoginForm.js +++ b/react/src/components/user/LoginForm.js @@ -10,55 +10,83 @@ import UserErrors from './UserErrors'; import '../Form.css'; -import { login, reset } from '../../actions/user'; +import { login, reset, loginJWT } from '../../actions/user'; -function LoginForm({ handleSubmit, onLogin, user, history, resetUser }) { - if (user.user) { - history.push('/'); +class LoginForm extends React.Component { + componentDidMount() { + const params = new URLSearchParams(new URL(window.location).search); + if (params.has('jwt')) { + const jwt = params.get('jwt'); + this.props.setJWT(jwt); + } + } + + componentDidUpdate() { + if (this.props.user.user) { + this.props.history.push('/'); + } + } + + render() { + return ( + +
+ { + this.props.resetUser(); + this.props.history.push('/signup'); + }} + > + signup + +
+
+
+ + + + +
+ + +
+ +
+
+ ); } - return ( - -
- { - resetUser(); - history.push('/signup'); - }} - > - signup - -
-
-
- - - -
- -
- -
-
- ); } LoginForm.propTypes = { @@ -67,6 +95,7 @@ LoginForm.propTypes = { user: PropTypes.object.isRequired, history: PropTypes.any.isRequired, resetUser: PropTypes.func.isRequired, + setJWT: PropTypes.func.isRequired, }; function mapStateToProps(state) { @@ -80,6 +109,7 @@ function mapDispatchToProps(dispatch) { resetUser: () => dispatch(reset()), onLogin: ({ username, password }) => dispatch(login({ username, password })), + setJWT: jwt => dispatch(loginJWT(jwt)), }; } diff --git a/react/src/components/user/SignupForm.js b/react/src/components/user/SignupForm.js index 63267f0..021477d 100644 --- a/react/src/components/user/SignupForm.js +++ b/react/src/components/user/SignupForm.js @@ -65,8 +65,13 @@ function SignupForm({ handleSubmit, onSignup, user, history, resetUser }) { component={InputField} type="password" /> -
-
diff --git a/routes/google.js b/routes/google.js new file mode 100644 index 0000000..d40a04d --- /dev/null +++ b/routes/google.js @@ -0,0 +1,23 @@ +const express = require('express'); +const passport = require('passport'); + +const router = express.Router(); + +const asyncHelper = require('../asyncHelper'); + +router.get( + '/google', + passport.authenticate('google', { + scope: ['https://www.googleapis.com/auth/plus.login'], + }), +); + +router.get( + '/google/callback', + passport.authenticate('google', { session: false, failWithError: true }), + asyncHelper(async (req, res) => { + res.redirect(`/login?jwt=${req.user.generateJwt()}`); + }), +); + +module.exports = router; diff --git a/routes/users.js b/routes/users.js index 8da1e6b..f2faf99 100644 --- a/routes/users.js +++ b/routes/users.js @@ -8,6 +8,8 @@ const router = express.Router(); const asyncHelper = require('../asyncHelper'); const auth = require('./auth'); +const { googleEnabled } = require('../config').googleOAuth; +const googleOAuth = require('./google'); const { NotFoundError } = require('../errors'); @@ -69,6 +71,9 @@ router.delete( }), ); +if (googleEnabled) { + router.use('/login', googleOAuth); +} router.post( '/login', passport.authenticate('local', { session: false, failWithError: true }),