| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548 | 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");			this.server = http.createServer(app).listen(config.get("port"));			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 appUrl = `${config.get("url.secure") ? "https" : "http"}://${config.get("url.host")}`;			const corsOptions = JSON.parse(JSON.stringify(config.get("cors")));			corsOptions.origin.push(appUrl);			corsOptions.credentials = true;			app.use(cors(corsOptions));			app.options("*", cors(corsOptions));			/**			 * @param {object} res - response object from Express			 * @param {string} err - custom error message			 */			function redirectOnErr(res, err) {				res.redirect(`${appUrl}?err=${encodeURIComponent(err)}`);			}			if (config.get("apis.github.enabled")) {				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").length > 0						? config.get("apis.github.redirect_uri")						: `${appUrl}/backend/auth/github/authorize/callback`;				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=${redirectUri}`,						`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=${redirectUri}`,						`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(`${appUrl}/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("url.secure"),										path: "/",										domain: config.get("url.host")									});									this.log(										"INFO",										"AUTH_GITHUB_AUTHORIZE_CALLBACK",										`User "${userId}" successfully authorized with GitHub.`									);									res.redirect(appUrl);								})								.catch(err => redirectOnErr(res, err.message));						}					);				});			}			app.get("/auth/verify_email", async (req, res) => {				if (this.getStatus() !== "READY") {					this.log(						"INFO",						"APP_REJECTED_VERIFY_EMAIL",						`A user tried to use verify email, 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(`${appUrl}?toast=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();
 |