diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a600a6..2bb16bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## **2.2.0** - *2020-04-08* +* authenticate with Google; +* authentication forms appearance adjusted; + ## **2.1.1** - *2020-04-08* * `Dashboard` - browsers list adjusted (IE support dropped); * `Facade`: diff --git a/api/.env.defaults b/api/.env.defaults index 41e22f2..959c761 100644 --- a/api/.env.defaults +++ b/api/.env.defaults @@ -17,6 +17,11 @@ REGISTRATION_MODE=open # password hashing algorithm (md5 or bcrypt; for bcrypt install https://www.npmjs.com/package/bcrypt) PASSWORD_HASH_ALGORITHM=md5 +# Google Auth +AUTH_GOOGLE_CLIENT_ID=CHANGE_ME +AUTH_GOOGLE_CLIENT_SECRET=CHANGE_ME +AUTH_GOOGLE_CALLBACK_URL=CHANGE_ME + # session settings. Store may be `memory`, `redis` or `mongo` # for Redis you should set additional configuration below (REDIS_HOST and REDIS_PORT) and install `redis` and `connect-redis` packages # for MongoDB you should set additional configuration below (MONGODB_URL) and install `connect-mongo` package diff --git a/api/config.js b/api/config.js index 56bc61e..d4ed8f7 100644 --- a/api/config.js +++ b/api/config.js @@ -40,6 +40,14 @@ const config = { //password hashing algorithm (md5 or bcrypt; for bcrypt install https://www.npmjs.com/package/bcrypt) passwordHashAlgorithm: process.env.PASSWORD_HASH_ALGORITHM, + auth: { + google: { + clientID: process.env.AUTH_GOOGLE_CLIENT_ID, + clientSecret: process.env.AUTH_GOOGLE_CLIENT_SECRET, + callbackURL: process.env.AUTH_GOOGLE_CALLBACK_URL + } + }, + // not used? can be removed? logger: { logEnabled: process.env.LOG_ENABLED.toLowerCase() === "true", diff --git a/api/lib/auth.js b/api/lib/auth.js index b428ccd..5d11070 100644 --- a/api/lib/auth.js +++ b/api/lib/auth.js @@ -11,7 +11,7 @@ var security = require("./security"), LocalStrategy = require("passport-local").Strategy, //FacebookStrategy = require('passport-facebook').Strategy, //TwitterStrategy = require('passport-twitter').Strategy, - //GoogleStrategy = require('passport-google-oauth').OAuth2Strategy, + GoogleStrategy = require("passport-google-oauth").OAuth2Strategy, //User = require('./../models/user'), config = require("@config"), logger = require("@logger"), @@ -105,157 +105,81 @@ var security = require("./security"), }); } ), - /* - required configuration: - "configAuth": { - "facebookAuth": { - "clientID": "your-secret-clientID-here", - "clientSecret": "your-client-secret-here", - "callbackURL": "http://localhost:8080/auth/facebook/callback" - }, - "twitterAuth": { - "consumerKey": "your-consumer-key-here", - "consumerSecret": "your-client-secret-here", - "callbackURL": "http://localhost:8080/auth/twitter/callback" - }, - "googleAuth": { - "clientID": "your-secret-clientID-here", - "clientSecret": "your-client-secret-here", - "callbackURL": "http://localhost:8080/auth/google/callback" - } - } - - facebook: new FacebookStrategy({ - // pull in our app id and secret from our auth.js file - clientID: configAuth.facebookAuth.clientID, - clientSecret: configAuth.facebookAuth.clientSecret, - callbackURL: configAuth.facebookAuth.callbackURL - }, - // facebook will send back the token and profile + google: new GoogleStrategy(config.auth.google, function(token, refreshToken, profile, done) { - + logger.info("GoogleStrategy"); + logger.dir(profile); // asynchronous - process.nextTick(function() { - - // find the user in the database based on their facebook id - store.getUser({ 'facebook.id': profile.id }, function(err, user) { - - // if there is an error, stop everything and return that - // ie an error connecting to the database - if (err) - return done(err); - - // if the user is found, then log them in - if (user) { - return done(null, user); // user found, return that user - } else { - // if there is no user found with that facebook id, create them - var newUser = new User(); - - // set all of the facebook information in our user model - newUser.facebook.id = profile.id; // set the users facebook id - newUser.facebook.token = token; // we will save the token that facebook provides to the user - newUser.facebook.name = profile.name.givenName + ' ' + profile.name.familyName; // look at the passport user profile to see how names are returned - newUser.facebook.email = profile.emails[0].value; // facebook can return multiple emails so we'll take the first - - // save our user to the database - newUser.save(function(err) { - if (err) - throw err; + process.nextTick(async function() { + // find a user + let user; + try { + user = await store.users.findOne({ "google.id" : profile.id }); - // if successful, return the new user - return done(null, newUser); - }); + // if not found by google.id + if (!user) { + // try to find by google email + user = await store.users.getByEmail(profile.emails[0].value); } - }); - }); - - }), - twitter: new TwitterStrategy({ - consumerKey: configAuth.twitterAuth.consumerKey, - consumerSecret: configAuth.twitterAuth.consumerSecret, - callbackURL: configAuth.twitterAuth.callbackURL - }, - function(token, tokenSecret, profile, done) { - - // make the code asynchronous - // store.getUser won't fire until we have all our data back from Twitter - process.nextTick(function() { - - store.getUser({ 'twitter.id': profile.id }, function(err, user) { - - // if there is an error, stop everything and return that - // ie an error connecting to the database - if (err) - return done(err); - - // if the user is found then log them in - if (user) { - return done(null, user); // user found, return that user - } else { - // if there is no user, create them - var newUser = new User(); + logger.dir(user); + } catch (err) { + logger.error(err); + return done(null, false, { message: "Unknown error" }); + } - // set all of the user data that we need - newUser.twitter.id = profile.id; - newUser.twitter.token = token; - newUser.twitter.username = profile.username; - newUser.twitter.displayName = profile.displayName; + // if a user is found, log in + if (user) { - // save our user into the database - newUser.save(function(err) { - if (err) - throw err; - return done(null, newUser); - }); + try { + // update user info + user.name = user.name || profile.displayName; + user.email = user.email || profile.emails[0].value; + if (!user.photo && profile.photos && profile.photos.length) { + user.photo = profile.photos[0].value; + } + user.google = user.google || {}; + user.google.id = profile.id; + user.google.token = token; + user.google.name = profile.displayName; + user.google.email = profile.emails[0].value; + + await store.users.save(user); + } catch (err) { + // silent } - }); - - }); - - }), - google: new GoogleStrategy({ - clientID: configAuth.googleAuth.clientID, - clientSecret: configAuth.googleAuth.clientSecret, - callbackURL: configAuth.googleAuth.callbackURL, - }, - function(token, refreshToken, profile, done) { - - // make the code asynchronous - // store.getUser won't fire until we have all our data back from Google - process.nextTick(function() { - - // try to find the user based on their google id - store.getUser({ 'google.id': profile.id }, function(err, user) { - if (err) - return done(err); - - if (user) { + logger.info("Google signin successful"); + return done(null, user); + } - // if a user is found, log them in - return done(null, user); - } else { - // if the user isnt in our database, create a new user - var newUser = new User(); + // if the user isn't in database - create a new user + user = { + name: profile.displayName, + email: profile.emails[0].value, + photo: profile.photos && profile.photos.length > 0 ? profile.photos[0].value : undefined, + google: { + id: profile.id, + token, + name: profile.displayName, + email: profile.emails[0].value + } + }; - // set all of the relevant information - newUser.google.id = profile.id; - newUser.google.token = token; - newUser.google.name = profile.displayName; - newUser.google.email = profile.emails[0].value; // pull the first email + let usersCount = await store.users.count(); + if (usersCount === 0) { + user.roles = ["owner"]; + } - // save the user - newUser.save(function(err) { - if (err) - throw err; - return done(null, newUser); - }); - } - }); + try { + await store.users.save(user); + logger.info("New user registered with google"); + return done(null, user); + } catch (err) { + logger.error(err); + return done(null, false, { message: "Unknown error" }); + } }); - - })*/ + }) }; module.exports = function (express) { @@ -285,20 +209,13 @@ module.exports = function (express) { // used to deserialize the user passport.deserializeUser(async function (sessionUser, done) { logger.debug("deserializeUser " + sessionUser.email); - /* - let user = await store.getUserById(id); - - if (!user) { - return done(null, false); - }*/ - done(null, sessionUser); }); passport.use("local", strategies.local); + passport.use(strategies.google); //passport.use(strategies.facebook); //passport.use(strategies.twitter); - //passport.use(strategies.google); express.use(passport.initialize()); express.use(passport.session()); diff --git a/api/lib/filters.js b/api/lib/filters.js new file mode 100644 index 0000000..0bb2fa8 --- /dev/null +++ b/api/lib/filters.js @@ -0,0 +1,44 @@ +const config = require("@config"), + logger = require("@logger"); + +function signinRequired(req, res, next) { + if (!req.isAuthenticated()) { + logger.info("Signin is required"); + if (req.xhr) { + return res.status(401).json({}).end(); + } else { + var url = require("url"), + querystring = require("querystring"), + redirectUrl = "/signin", + path = url.parse(req.originalUrl).path; + + if (path) { + redirectUrl += "?return=" + querystring.escape(path); + } + console.log("redirectUrl", path); + return res.redirect(redirectUrl); + } + } + next(); +} + +function xhrOnly(req, res, next) { + if (!req.xhr) { + return res.error(404); + } + next(); +} + +function facadeOnly(req, res, next) { + if (req.headers.authorization !== "Bearer " + config.facadeToken) { + return res.sendStatus(401); + } + next(); +} + +module.exports = { + signinRequired, + xhrOnly, + facadeOnly +}; + diff --git a/api/package.json b/api/package.json index 0d8ac89..e4f0519 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "express-template-api", - "version": "2.1.1", + "version": "2.2.0", "description": "Website template (skeleton) based on Express.js 4, Vue.js and Vuetify 2", "author": "NordicSoft", "license": "MIT", diff --git a/api/router.js b/api/router.js index bb1200f..e92d91c 100644 --- a/api/router.js +++ b/api/router.js @@ -1,49 +1,11 @@ -const config = require("@config"), - logger = require("@logger"); +const logger = require("@logger"), + { signinRequired, xhrOnly, facadeOnly } = require("@lib/filters"); module.exports = function (express) { logger.info("Init Router"); - // eslint-disable-next-line no-unused-vars - var signinRequired = function (req, res, next) { - if (!req.isAuthenticated()) { - logger.info("Signin is required"); - if (req.xhr) { - return res.status(401).json({}).end(); - } else { - var url = require("url"), - querystring = require("querystring"), - redirectUrl = "/signin", - path = url.parse(req.originalUrl).path; - - if (path) { - redirectUrl += "?return=" + querystring.escape(path); - } - console.log("redirectUrl", path); - return res.redirect(redirectUrl); - } - } - next(); - }; - - var xhrOnly = function (req, res, next) { - if (!req.xhr) { - return res.error(404); - } - next(); - }; - - var facadeOnly = function (req, res, next) { - if (req.headers.authorization !== "Bearer " + config.facadeToken) { - return res.sendStatus(401); - } - next(); - }; - - express.use(xhrOnly); - - express.use("/profile", signinRequired, require("./routes/profile")); - express.use("/gallery", signinRequired, require("./routes/gallery")); + express.use("/profile", xhrOnly, signinRequired, require("./routes/profile")); + express.use("/gallery", xhrOnly, signinRequired, require("./routes/gallery")); express.use("/facade", facadeOnly, require("./routes/facade")); express.use("/auth", require("./routes/auth")); express.use("/", signinRequired, require("./routes")); diff --git a/api/routes/auth.js b/api/routes/auth.js index 9cc632b..1a83b75 100644 --- a/api/routes/auth.js +++ b/api/routes/auth.js @@ -1,8 +1,10 @@ const express = require("express"), router = express.Router(), + passport = require("passport"), logger = require("@logger"), config = require("@config"), - store = require("@store"); + store = require("@store"), + { xhrOnly } = require("@lib/filters"); function signin(req, res) { req.signin(function (err, user, info) { @@ -14,34 +16,35 @@ function signin(req, res) { } async function refreshRegistrationEnabled(app) { - let registrationEnabled = config.registrationMode === "open" || await store.users.count() === 0; + let registrationEnabled = + config.registrationMode === "open" || (await store.users.count()) === 0; app.set("registration-enabled", registrationEnabled); } -router.get("/", function (req, res) { +router.get("/", xhrOnly, function (req, res) { return res.json({ isAuthenticated: req.isAuthenticated(), - registrationEnabled: req.app.get("registration-enabled") + registrationEnabled: req.app.get("registration-enabled"), }); }); -router.get("/check-email/:email", async function (req, res) { +router.get("/check-email/:email", xhrOnly, async function (req, res) { var email = req.params.email, user = await store.users.getByEmail(email); return res.json(!user); }); -router.get("/check-username/:username", async function (req, res) { +router.get("/check-username/:username", xhrOnly, async function (req, res) { var username = req.params.username, user = await store.users.getByUsername(username); return res.json(!user); }); -router.post("/signin", signin); +router.post("/signin", xhrOnly, signin); -router.post("/register", async function (req, res) { +router.post("/register", xhrOnly, async function (req, res) { if (req.isAuthenticated()) { - return res.status(400).json({ message: "User is authenticated"}); + return res.status(400).json({ message: "User is authenticated" }); } if (!req.app.get("registration-enabled")) { @@ -64,7 +67,9 @@ router.post("/register", async function (req, res) { // check username if (await store.users.getByUsername(username)) { - return res.status(400).json({ message: "This username is already taken" }); + return res + .status(400) + .json({ message: "This username is already taken" }); } // check email @@ -76,7 +81,7 @@ router.post("/register", async function (req, res) { let user = { name, email, - username + username, }; let usersCount = await store.users.count(); @@ -100,15 +105,37 @@ router.post("/register", async function (req, res) { }); break; default: - logger.error("Incorrect passwordHashAlgorithm specified in config.json"); + logger.error( + "Incorrect passwordHashAlgorithm specified in config.json" + ); break; } }); +// send to google to do the authentication +// profile gets us their basic information including their name +// email gets their emails +router.get( + "/google", + passport.authenticate("google", { scope: ["profile", "email"] }) +); + +// the callback after google has authenticated the user +router.get( + "/google/callback", + passport.authenticate("google", { + successRedirect: "/dashboard", + failureRedirect: "/dashboard/auth", + }) +); + router.get("/signout", function (req, res) { req.signout(); req.session.destroy(); - return res.json(true); + if (req.xhr) { + return res.json(true); + } + return res.redirect("/"); }); module.exports = router; diff --git a/dashboard/package.json b/dashboard/package.json index 74213d3..ffb8829 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -1,6 +1,6 @@ { "name": "express-template-dashboard", - "version": "2.1.1", + "version": "2.2.0", "private": true, "main": "server.js", "scripts": { diff --git a/dashboard/src/auth/App.vue b/dashboard/src/auth/App.vue index bae72ac..66d13fd 100644 --- a/dashboard/src/auth/App.vue +++ b/dashboard/src/auth/App.vue @@ -5,10 +5,22 @@ - + -
- + +
+ mdi-feature-search-outline @@ -31,6 +43,11 @@ export default { return { facadeUrl: process.env.VUE_APP_FACADE_URL }; + }, + methods: { + async google() { + window.location = process.env.VUE_APP_API_BASE_URL + "/auth/google"; + } } }; @@ -47,5 +64,14 @@ export default { .v-card { max-width: 500px !important; margin: 0 auto; + background: linear-gradient( + 35deg, + rgba($color: #1cd8d2, $alpha: 0.2), + rgba($color: #93edc7, $alpha: 0.1) + ); +} + +.social { + background: rgba($color: #fff, $alpha: 0.5); } diff --git a/dashboard/src/auth/Register.vue b/dashboard/src/auth/Register.vue index 1d1f767..78912a6 100644 --- a/dashboard/src/auth/Register.vue +++ b/dashboard/src/auth/Register.vue @@ -63,9 +63,9 @@ maxlength="254" outlined required - class="mb-3" + class="mb-1" /> -
+
mdi-account-plus-outline @@ -73,7 +73,7 @@ Register
-
+
Already have an account? Sign In
diff --git a/dashboard/src/auth/SignIn.vue b/dashboard/src/auth/SignIn.vue index 0102b59..c3838d5 100644 --- a/dashboard/src/auth/SignIn.vue +++ b/dashboard/src/auth/SignIn.vue @@ -23,9 +23,9 @@ maxlength="254" outlined required - class="mb-3" + class="mb-1" /> -
+
mdi-account-check-outline @@ -33,7 +33,7 @@ Sign In
-
+
Don't have an account? Register
diff --git a/facade/package.json b/facade/package.json index 79534c3..36519cd 100644 --- a/facade/package.json +++ b/facade/package.json @@ -1,6 +1,6 @@ { "name": "express-template-facade", - "version": "2.1.1", + "version": "2.2.0", "description": "Website template (skeleton) based on Express.js 4, Vue.js and Vuetify 2", "author": "NordicSoft", "license": "MIT", diff --git a/package.json b/package.json index 019cd56..1f0bbd8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "express-template", - "version": "2.1.1", + "version": "2.2.0", "description": "Website template (skeleton) based on Express.js 4, Vue.js and Vuetify 2", "author": "NordicSoft", "license": "MIT",