app.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. const CoreClass = require("../core.js");
  2. const express = require("express");
  3. const bodyParser = require("body-parser");
  4. const cookieParser = require("cookie-parser");
  5. const cors = require("cors");
  6. const config = require("config");
  7. const async = require("async");
  8. const request = require("request");
  9. const OAuth2 = require("oauth").OAuth2;
  10. class AppModule extends CoreClass {
  11. constructor() {
  12. super("app");
  13. }
  14. initialize() {
  15. return new Promise((resolve, reject) => {
  16. const mail = this.moduleManager.modules["mail"],
  17. cache = this.moduleManager.modules["cache"],
  18. db = this.moduleManager.modules["db"],
  19. activities = this.moduleManager.modules["activities"];
  20. this.utils = this.moduleManager.modules["utils"];
  21. let app = (this.app = express());
  22. const SIDname = config.get("cookie.SIDname");
  23. this.server = app.listen(config.get("serverPort"));
  24. app.use(cookieParser());
  25. app.use(bodyParser.json());
  26. app.use(bodyParser.urlencoded({ extended: true }));
  27. let corsOptions = Object.assign({}, config.get("cors"));
  28. app.use(cors(corsOptions));
  29. app.options("*", cors(corsOptions));
  30. let oauth2 = new OAuth2(
  31. config.get("apis.github.client"),
  32. config.get("apis.github.secret"),
  33. "https://github.com/",
  34. "login/oauth/authorize",
  35. "login/oauth/access_token",
  36. null
  37. );
  38. let redirect_uri =
  39. config.get("serverDomain") + "/auth/github/authorize/callback";
  40. app.get("/auth/github/authorize", async (req, res) => {
  41. try {
  42. await this._validateHook();
  43. } catch {
  44. return;
  45. }
  46. let params = [
  47. `client_id=${config.get("apis.github.client")}`,
  48. `redirect_uri=${config.get(
  49. "serverDomain"
  50. )}/auth/github/authorize/callback`,
  51. `scope=user:email`,
  52. ].join("&");
  53. res.redirect(
  54. `https://github.com/login/oauth/authorize?${params}`
  55. );
  56. });
  57. app.get("/auth/github/link", async (req, res) => {
  58. try {
  59. await this._validateHook();
  60. } catch {
  61. return;
  62. }
  63. let params = [
  64. `client_id=${config.get("apis.github.client")}`,
  65. `redirect_uri=${config.get(
  66. "serverDomain"
  67. )}/auth/github/authorize/callback`,
  68. `scope=user:email`,
  69. `state=${req.cookies[SIDname]}`,
  70. ].join("&");
  71. res.redirect(
  72. `https://github.com/login/oauth/authorize?${params}`
  73. );
  74. });
  75. function redirectOnErr(res, err) {
  76. return res.redirect(
  77. `${config.get("domain")}/?err=${encodeURIComponent(err)}`
  78. );
  79. }
  80. app.get("/auth/github/authorize/callback", async (req, res) => {
  81. try {
  82. await this._validateHook();
  83. } catch {
  84. return;
  85. }
  86. let code = req.query.code;
  87. let access_token;
  88. let body;
  89. let address;
  90. const state = req.query.state;
  91. const verificationToken = await this.utils.generateRandomString(
  92. 64
  93. );
  94. async.waterfall(
  95. [
  96. (next) => {
  97. if (req.query.error)
  98. return next(req.query.error_description);
  99. next();
  100. },
  101. (next) => {
  102. oauth2.getOAuthAccessToken(
  103. code,
  104. { redirect_uri },
  105. next
  106. );
  107. },
  108. (_access_token, refresh_token, results, next) => {
  109. if (results.error)
  110. return next(results.error_description);
  111. access_token = _access_token;
  112. request.get(
  113. {
  114. url: `https://api.github.com/user?access_token=${access_token}`,
  115. headers: { "User-Agent": "request" },
  116. },
  117. next
  118. );
  119. },
  120. (httpResponse, _body, next) => {
  121. body = _body = JSON.parse(_body);
  122. if (httpResponse.statusCode !== 200)
  123. return next(body.message);
  124. if (state) {
  125. return async.waterfall(
  126. [
  127. (next) => {
  128. cache.hget("sessions", state, next);
  129. },
  130. (session, next) => {
  131. if (!session)
  132. return next("Invalid session.");
  133. db.models.user.findOne(
  134. { _id: session.userId },
  135. next
  136. );
  137. },
  138. (user, next) => {
  139. if (!user)
  140. return next("User not found.");
  141. if (
  142. user.services.github &&
  143. user.services.github.id
  144. )
  145. return next(
  146. "Account already has GitHub linked."
  147. );
  148. db.models.user.updateOne(
  149. { _id: user._id },
  150. {
  151. $set: {
  152. "services.github": {
  153. id: body.id,
  154. access_token,
  155. },
  156. },
  157. },
  158. { runValidators: true },
  159. (err) => {
  160. if (err) return next(err);
  161. next(null, user, body);
  162. }
  163. );
  164. },
  165. (user) => {
  166. cache.pub(
  167. "user.linkGitHub",
  168. user._id
  169. );
  170. res.redirect(
  171. `${config.get(
  172. "domain"
  173. )}/settings`
  174. );
  175. },
  176. ],
  177. next
  178. );
  179. }
  180. if (!body.id)
  181. return next("Something went wrong, no id.");
  182. db.models.user.findOne(
  183. { "services.github.id": body.id },
  184. (err, user) => {
  185. next(err, user, body);
  186. }
  187. );
  188. },
  189. (user, body, next) => {
  190. if (user) {
  191. user.services.github.access_token = access_token;
  192. return user.save(() => {
  193. next(true, user._id);
  194. });
  195. }
  196. db.models.user.findOne(
  197. {
  198. username: new RegExp(
  199. `^${body.login}$`,
  200. "i"
  201. ),
  202. },
  203. (err, user) => {
  204. next(err, user);
  205. }
  206. );
  207. },
  208. (user, next) => {
  209. if (user)
  210. return next(
  211. "An account with that username already exists."
  212. );
  213. request.get(
  214. {
  215. url: `https://api.github.com/user/emails?access_token=${access_token}`,
  216. headers: { "User-Agent": "request" },
  217. },
  218. next
  219. );
  220. },
  221. (httpResponse, body2, next) => {
  222. body2 = JSON.parse(body2);
  223. if (!Array.isArray(body2))
  224. return next(body2.message);
  225. body2.forEach((email) => {
  226. if (email.primary)
  227. address = email.email.toLowerCase();
  228. });
  229. db.models.user.findOne(
  230. { "email.address": address },
  231. next
  232. );
  233. },
  234. (user, next) => {
  235. this.utils.generateRandomString(12).then((_id) => {
  236. next(null, user, _id);
  237. });
  238. },
  239. (user, _id, next) => {
  240. if (user)
  241. return next(
  242. "An account with that email address already exists."
  243. );
  244. next(null, {
  245. _id, //TODO Check if exists
  246. username: body.login,
  247. name: body.name,
  248. location: body.location,
  249. bio: body.bio,
  250. email: {
  251. address,
  252. verificationToken,
  253. },
  254. services: {
  255. github: { id: body.id, access_token },
  256. },
  257. });
  258. },
  259. // generate the url for gravatar avatar
  260. (user, next) => {
  261. this.utils
  262. .createGravatar(user.email.address)
  263. .then((url) => {
  264. user.avatar = { type: "gravatar", url };
  265. next(null, user);
  266. });
  267. },
  268. // save the new user to the database
  269. (user, next) => {
  270. db.models.user.create(user, next);
  271. },
  272. // add the activity of account creation
  273. (user, next) => {
  274. activities.addActivity(user._id, "created_account");
  275. next(null, user);
  276. },
  277. (user, next) => {
  278. mail.schemas.verifyEmail(
  279. address,
  280. body.login,
  281. user.email.verificationToken
  282. );
  283. next(null, user._id);
  284. },
  285. ],
  286. async (err, userId) => {
  287. if (err && err !== true) {
  288. err = await this.utils.getError(err);
  289. logger.error(
  290. "AUTH_GITHUB_AUTHORIZE_CALLBACK",
  291. `Failed to authorize with GitHub. "${err}"`
  292. );
  293. return redirectOnErr(res, err);
  294. }
  295. const sessionId = await this.utils.guid();
  296. cache.hset(
  297. "sessions",
  298. sessionId,
  299. cache.schemas.session(sessionId, userId),
  300. (err) => {
  301. if (err) return redirectOnErr(res, err.message);
  302. let date = new Date();
  303. date.setTime(
  304. new Date().getTime() +
  305. 2 * 365 * 24 * 60 * 60 * 1000
  306. );
  307. res.cookie(SIDname, sessionId, {
  308. expires: date,
  309. secure: config.get("cookie.secure"),
  310. path: "/",
  311. domain: config.get("cookie.domain"),
  312. });
  313. logger.success(
  314. "AUTH_GITHUB_AUTHORIZE_CALLBACK",
  315. `User "${userId}" successfully authorized with GitHub.`
  316. );
  317. res.redirect(`${config.get("domain")}/`);
  318. }
  319. );
  320. }
  321. );
  322. });
  323. app.get("/auth/verify_email", async (req, res) => {
  324. try {
  325. await this._validateHook();
  326. } catch {
  327. return;
  328. }
  329. let code = req.query.code;
  330. async.waterfall(
  331. [
  332. (next) => {
  333. if (!code) return next("Invalid code.");
  334. next();
  335. },
  336. (next) => {
  337. db.models.user.findOne(
  338. { "email.verificationToken": code },
  339. next
  340. );
  341. },
  342. (user, next) => {
  343. if (!user) return next("User not found.");
  344. if (user.email.verified)
  345. return next("This email is already verified.");
  346. db.models.user.updateOne(
  347. { "email.verificationToken": code },
  348. {
  349. $set: { "email.verified": true },
  350. $unset: { "email.verificationToken": "" },
  351. },
  352. { runValidators: true },
  353. next
  354. );
  355. },
  356. ],
  357. (err) => {
  358. if (err) {
  359. let error = "An error occurred.";
  360. if (typeof err === "string") error = err;
  361. else if (err.message) error = err.message;
  362. logger.error(
  363. "VERIFY_EMAIL",
  364. `Verifying email failed. "${error}"`
  365. );
  366. return res.json({
  367. status: "failure",
  368. message: error,
  369. });
  370. }
  371. logger.success(
  372. "VERIFY_EMAIL",
  373. `Successfully verified email.`
  374. );
  375. res.redirect(
  376. `${config.get(
  377. "domain"
  378. )}?msg=Thank you for verifying your email`
  379. );
  380. }
  381. );
  382. });
  383. resolve();
  384. });
  385. }
  386. SERVER(payload) {
  387. return new Promise((resolve, reject) => {
  388. resolve(this.server);
  389. });
  390. }
  391. GET_APP(payload) {
  392. return new Promise((resolve, reject) => {
  393. resolve({ app: this.app });
  394. });
  395. }
  396. EXAMPLE_JOB(payload) {
  397. return new Promise((resolve, reject) => {
  398. if (true) {
  399. resolve({});
  400. } else {
  401. reject(new Error("Nothing changed."));
  402. }
  403. });
  404. }
  405. }
  406. module.exports = new AppModule();