import config from "config"; import axios from "axios"; import async from "async"; import cors from "cors"; import cookieParser from "cookie-parser"; import bodyParser from "body-parser"; import express from "express"; import oauth from "oauth"; import http from "http"; import CoreClass from "../core"; const { OAuth2 } = oauth; let AppModule; let MailModule; let CacheModule; let DBModule; let ActivitiesModule; let PlaylistsModule; let UtilsModule; class _AppModule extends CoreClass { // eslint-disable-next-line require-jsdoc constructor() { super("app"); AppModule = this; } /** * Initialises the app module * * @returns {Promise} - returns promise (reject, resolve) */ initialize() { return new Promise(resolve => { MailModule = this.moduleManager.modules.mail; CacheModule = this.moduleManager.modules.cache; DBModule = this.moduleManager.modules.db; ActivitiesModule = this.moduleManager.modules.activities; PlaylistsModule = this.moduleManager.modules.playlists; UtilsModule = this.moduleManager.modules.utils; const app = (this.app = express()); const SIDname = config.get("cookie.SIDname"); this.server = http .createServer(app) .listen(config.get("serverPort")); app.use(cookieParser()); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); let userModel; DBModule.runJob("GET_MODEL", { modelName: "user" }) .then(model => { userModel = model; }) .catch(console.error); const corsOptions = { ...config.get("cors"), credentials: true }; app.use(cors(corsOptions)); app.options("*", cors(corsOptions)); const oauth2 = new OAuth2( config.get("apis.github.client"), config.get("apis.github.secret"), "https://github.com/", "login/oauth/authorize", "login/oauth/access_token", null ); const redirectUri = `${config.get("apis.github.redirect_uri")}`; /** * @param {object} res - response object from Express * @param {string} err - custom error message */ function redirectOnErr(res, err) { res.redirect( `${config.get("domain")}?err=${encodeURIComponent(err)}` ); } app.get("/auth/github/authorize", async (req, res) => { if (this.getStatus() !== "READY") { this.log( "INFO", "APP_REJECTED_GITHUB_AUTHORIZE", `A user tried to use github authorize, but the APP module is currently not ready.` ); return redirectOnErr( res, "Something went wrong on our end. Please try again later." ); } const params = [ `client_id=${config.get("apis.github.client")}`, `redirect_uri=${config.get("apis.github.redirect_uri")}`, `scope=user:email` ].join("&"); return res.redirect( `https://github.com/login/oauth/authorize?${params}` ); }); app.get("/auth/github/link", async (req, res) => { if (this.getStatus() !== "READY") { this.log( "INFO", "APP_REJECTED_GITHUB_AUTHORIZE", `A user tried to use github authorize, but the APP module is currently not ready.` ); return redirectOnErr( res, "Something went wrong on our end. Please try again later." ); } const params = [ `client_id=${config.get("apis.github.client")}`, `redirect_uri=${config.get("apis.github.redirect_uri")}`, `scope=user:email`, `state=${req.cookies[SIDname]}` ].join("&"); return res.redirect( `https://github.com/login/oauth/authorize?${params}` ); }); app.get("/auth/github/authorize/callback", async (req, res) => { if (this.getStatus() !== "READY") { this.log( "INFO", "APP_REJECTED_GITHUB_AUTHORIZE", `A user tried to use github authorize, but the APP module is currently not ready.` ); return redirectOnErr( res, "Something went wrong on our end. Please try again later." ); } const { code } = req.query; let accessToken; let body; let address; const { state } = req.query; const verificationToken = await UtilsModule.runJob( "GENERATE_RANDOM_STRING", { length: 64 } ); return async.waterfall( [ next => { if (req.query.error) return next(req.query.error_description); return next(); }, next => { oauth2.getOAuthAccessToken( code, { redirect_uri: redirectUri }, next ); }, (_accessToken, refreshToken, results, next) => { if (results.error) return next(results.error_description); accessToken = _accessToken; const options = { headers: { "User-Agent": "request", Authorization: `token ${accessToken}` } }; return axios .get("https://api.github.com/user", options) .then(github => next(null, github)) .catch(err => next(err)); }, (github, next) => { if (github.status !== 200) return next(github.data.message); if (state) { return async.waterfall( [ next => { CacheModule.runJob("HGET", { table: "sessions", key: state }) .then(session => next(null, session) ) .catch(next); }, (session, next) => { if (!session) return next("Invalid session."); return userModel.findOne( { _id: session.userId }, next ); }, (user, next) => { if (!user) return next("User not found."); if ( user.services.github && user.services.github.id ) return next( "Account already has GitHub linked." ); return userModel.updateOne( { _id: user._id }, { $set: { "services.github": { id: github.data.id, access_token: accessToken } } }, { runValidators: true }, err => { if (err) return next(err); return next( null, user, github.data ); } ); }, user => { CacheModule.runJob("PUB", { channel: "user.linkGithub", value: user._id }); CacheModule.runJob("PUB", { channel: "user.updated", value: { userId: user._id } }); res.redirect( `${config.get( "domain" )}/settings?tab=security` ); } ], next ); } if (!github.data.id) return next("Something went wrong, no id."); return userModel.findOne( { "services.github.id": github.data.id }, (err, user) => { next(err, user, github.data); } ); }, (user, _body, next) => { body = _body; if (user) { user.services.github.access_token = accessToken; return user.save(() => next(true, user._id)); } return userModel.findOne( { username: new RegExp(`^${body.login}$`, "i") }, (err, user) => next(err, user) ); }, (user, next) => { if (user) return next( `An account with that username already exists.` ); return axios .get("https://api.github.com/user/emails", { headers: { "User-Agent": "request", Authorization: `token ${accessToken}` } }) .then(res => next(null, res.data)) .catch(err => next(err)); }, (body, next) => { if (!Array.isArray(body)) return next(body.message); body.forEach(email => { if (email.primary) address = email.email.toLowerCase(); }); return userModel.findOne( { "email.address": address }, next ); }, (user, next) => { UtilsModule.runJob("GENERATE_RANDOM_STRING", { length: 12 }).then(_id => next(null, user, _id)); }, (user, _id, next) => { if (user) { if ( Object.keys( JSON.parse(user.services.github) ).length === 0 ) return next( `An account with that email address exists, but is not linked to GitHub.` ); return next( `An account with that email address already exists.` ); } return next(null, { _id, username: body.login, name: body.name, location: body.location, bio: body.bio, email: { address, verificationToken }, services: { github: { id: body.id, access_token: accessToken } } }); }, // generate the url for gravatar avatar (user, next) => { UtilsModule.runJob("CREATE_GRAVATAR", { email: user.email.address }).then(url => { user.avatar = { type: "gravatar", url }; next(null, user); }); }, // save the new user to the database (user, next) => { userModel.create(user, next); }, (user, next) => { MailModule.runJob("GET_SCHEMA", { schemaName: "verifyEmail" }).then(verifyEmailSchema => { verifyEmailSchema( address, body.login, user.email.verificationToken, err => { next(err, user._id); } ); }); }, // create a liked songs playlist for the new user (userId, next) => { PlaylistsModule.runJob("CREATE_USER_PLAYLIST", { userId, displayName: "Liked Songs", type: "user-liked" }) .then(likedSongsPlaylist => { next(null, likedSongsPlaylist, userId); }) .catch(err => next(err)); }, // create a disliked songs playlist for the new user (likedSongsPlaylist, userId, next) => { PlaylistsModule.runJob("CREATE_USER_PLAYLIST", { userId, displayName: "Disliked Songs", type: "user-disliked" }) .then(dislikedSongsPlaylist => { next( null, { likedSongsPlaylist, dislikedSongsPlaylist }, userId ); }) .catch(err => next(err)); }, // associate liked + disliked songs playlist to the user object ( { likedSongsPlaylist, dislikedSongsPlaylist }, userId, next ) => { userModel.updateOne( { _id: userId }, { $set: { likedSongsPlaylist, dislikedSongsPlaylist } }, { runValidators: true }, err => { if (err) return next(err); return next(null, userId); } ); }, // add the activity of account creation (userId, next) => { ActivitiesModule.runJob("ADD_ACTIVITY", { userId, type: "user__joined", payload: { message: "Welcome to Musare!" } }); next(null, userId); } ], async (err, userId) => { if (err && err !== true) { err = await UtilsModule.runJob("GET_ERROR", { error: err }); this.log( "ERROR", "AUTH_GITHUB_AUTHORIZE_CALLBACK", `Failed to authorize with GitHub. "${err}"` ); return redirectOnErr(res, err); } const sessionId = await UtilsModule.runJob("GUID", {}); const sessionSchema = await CacheModule.runJob( "GET_SCHEMA", { schemaName: "session" } ); return CacheModule.runJob("HSET", { table: "sessions", key: sessionId, value: sessionSchema(sessionId, userId) }) .then(() => { const date = new Date(); date.setTime( new Date().getTime() + 2 * 365 * 24 * 60 * 60 * 1000 ); res.cookie(SIDname, sessionId, { expires: date, secure: config.get("cookie.secure"), path: "/", domain: config.get("cookie.domain") }); this.log( "INFO", "AUTH_GITHUB_AUTHORIZE_CALLBACK", `User "${userId}" successfully authorized with GitHub.` ); res.redirect(`${config.get("domain")}/`); }) .catch(err => redirectOnErr(res, err.message)); } ); }); app.get("/auth/verify_email", async (req, res) => { if (this.getStatus() !== "READY") { this.log( "INFO", "APP_REJECTED_GITHUB_AUTHORIZE", `A user tried to use github authorize, but the APP module is currently not ready.` ); return redirectOnErr( res, "Something went wrong on our end. Please try again later." ); } const { code } = req.query; return async.waterfall( [ next => { if (!code) return next("Invalid code."); return next(); }, next => { userModel.findOne( { "email.verificationToken": code }, next ); }, (user, next) => { if (!user) return next("User not found."); if (user.email.verified) return next("This email is already verified."); return userModel.updateOne( { "email.verificationToken": code }, { $set: { "email.verified": true }, $unset: { "email.verificationToken": "" } }, { runValidators: true }, next ); } ], err => { if (err) { let error = "An error occurred."; if (typeof err === "string") error = err; else if (err.message) error = err.message; this.log( "ERROR", "VERIFY_EMAIL", `Verifying email failed. "${error}"` ); return res.json({ status: "error", message: error }); } this.log( "INFO", "VERIFY_EMAIL", `Successfully verified email.` ); return res.redirect( `${config.get( "domain" )}?msg=Thank you for verifying your email` ); } ); }); resolve(); }); } /** * Returns the express server * * @returns {Promise} - returns promise (reject, resolve) */ SERVER() { return new Promise(resolve => { resolve(AppModule.server); }); } /** * Returns the app object * * @returns {Promise} - returns promise (reject, resolve) */ GET_APP() { return new Promise(resolve => { resolve({ app: AppModule.app }); }); } // EXAMPLE_JOB() { // return new Promise((resolve, reject) => { // if (true) resolve({}); // else reject(new Error("Nothing changed.")); // }); // } } export default new _AppModule();