| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519 | 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("serverDomain")}/auth/github/authorize/callback`;			/**			 * @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("serverDomain")}/auth/github/authorize/callback`,					`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("serverDomain")}/auth/github/authorize/callback`,					`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											});											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`);					}				);			});			return 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();
 |