app.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  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(async (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. const userModel = await db.runJob("GET_MODEL", {
  28. modelName: "user",
  29. });
  30. let corsOptions = Object.assign({}, config.get("cors"));
  31. app.use(cors(corsOptions));
  32. app.options("*", cors(corsOptions));
  33. let oauth2 = new OAuth2(
  34. config.get("apis.github.client"),
  35. config.get("apis.github.secret"),
  36. "https://github.com/",
  37. "login/oauth/authorize",
  38. "login/oauth/access_token",
  39. null
  40. );
  41. let redirect_uri =
  42. config.get("serverDomain") + "/auth/github/authorize/callback";
  43. app.get("/auth/github/authorize", async (req, res) => {
  44. if (this.getStatus() !== "READY") {
  45. this.log(
  46. "INFO",
  47. "APP_REJECTED_GITHUB_AUTHORIZE",
  48. `A user tried to use github authorize, but the APP module is currently not ready.`
  49. );
  50. return redirectOnErr(
  51. res,
  52. "Something went wrong on our end. Please try again later."
  53. );
  54. }
  55. let params = [
  56. `client_id=${config.get("apis.github.client")}`,
  57. `redirect_uri=${config.get(
  58. "serverDomain"
  59. )}/auth/github/authorize/callback`,
  60. `scope=user:email`,
  61. ].join("&");
  62. res.redirect(
  63. `https://github.com/login/oauth/authorize?${params}`
  64. );
  65. });
  66. app.get("/auth/github/link", async (req, res) => {
  67. if (this.getStatus() !== "READY") {
  68. this.log(
  69. "INFO",
  70. "APP_REJECTED_GITHUB_AUTHORIZE",
  71. `A user tried to use github authorize, but the APP module is currently not ready.`
  72. );
  73. return redirectOnErr(
  74. res,
  75. "Something went wrong on our end. Please try again later."
  76. );
  77. }
  78. let params = [
  79. `client_id=${config.get("apis.github.client")}`,
  80. `redirect_uri=${config.get(
  81. "serverDomain"
  82. )}/auth/github/authorize/callback`,
  83. `scope=user:email`,
  84. `state=${req.cookies[SIDname]}`,
  85. ].join("&");
  86. res.redirect(
  87. `https://github.com/login/oauth/authorize?${params}`
  88. );
  89. });
  90. function redirectOnErr(res, err) {
  91. return res.redirect(
  92. `${config.get("domain")}/?err=${encodeURIComponent(err)}`
  93. );
  94. }
  95. app.get("/auth/github/authorize/callback", async (req, res) => {
  96. if (this.getStatus() !== "READY") {
  97. this.log(
  98. "INFO",
  99. "APP_REJECTED_GITHUB_AUTHORIZE",
  100. `A user tried to use github authorize, but the APP module is currently not ready.`
  101. );
  102. return redirectOnErr(
  103. res,
  104. "Something went wrong on our end. Please try again later."
  105. );
  106. }
  107. let code = req.query.code;
  108. let access_token;
  109. let body;
  110. let address;
  111. const state = req.query.state;
  112. const verificationToken = await this.utils.runJob(
  113. "GENERATE_RANDOM_STRING",
  114. { length: 64 }
  115. );
  116. async.waterfall(
  117. [
  118. (next) => {
  119. if (req.query.error)
  120. return next(req.query.error_description);
  121. next();
  122. },
  123. (next) => {
  124. oauth2.getOAuthAccessToken(
  125. code,
  126. { redirect_uri },
  127. next
  128. );
  129. },
  130. (_access_token, refresh_token, results, next) => {
  131. if (results.error)
  132. return next(results.error_description);
  133. access_token = _access_token;
  134. request.get(
  135. {
  136. url: `https://api.github.com/user`,
  137. headers: {
  138. "User-Agent": "request",
  139. Authorization: `token ${access_token}`,
  140. },
  141. },
  142. next
  143. );
  144. },
  145. (httpResponse, _body, next) => {
  146. body = _body = JSON.parse(_body);
  147. if (httpResponse.statusCode !== 200)
  148. return next(body.message);
  149. if (state) {
  150. return async.waterfall(
  151. [
  152. (next) => {
  153. cache
  154. .runJob("HGET", {
  155. table: "sessions",
  156. key: state,
  157. })
  158. .then((session) =>
  159. next(null, session)
  160. )
  161. .catch(next);
  162. },
  163. (session, next) => {
  164. if (!session)
  165. return next("Invalid session.");
  166. userModel.findOne(
  167. { _id: session.userId },
  168. next
  169. );
  170. },
  171. (user, next) => {
  172. if (!user)
  173. return next("User not found.");
  174. if (
  175. user.services.github &&
  176. user.services.github.id
  177. )
  178. return next(
  179. "Account already has GitHub linked."
  180. );
  181. userModel.updateOne(
  182. { _id: user._id },
  183. {
  184. $set: {
  185. "services.github": {
  186. id: body.id,
  187. access_token,
  188. },
  189. },
  190. },
  191. { runValidators: true },
  192. (err) => {
  193. if (err) return next(err);
  194. next(null, user, body);
  195. }
  196. );
  197. },
  198. (user) => {
  199. cache.runJob("PUB", {
  200. channel: "user.linkGithub",
  201. value: user._id,
  202. });
  203. res.redirect(
  204. `${config.get(
  205. "domain"
  206. )}/settings`
  207. );
  208. },
  209. ],
  210. next
  211. );
  212. }
  213. if (!body.id)
  214. return next("Something went wrong, no id.");
  215. userModel.findOne(
  216. { "services.github.id": body.id },
  217. (err, user) => {
  218. next(err, user, body);
  219. }
  220. );
  221. },
  222. (user, body, next) => {
  223. if (user) {
  224. user.services.github.access_token = access_token;
  225. return user.save(() => {
  226. next(true, user._id);
  227. });
  228. }
  229. userModel.findOne(
  230. {
  231. username: new RegExp(
  232. `^${body.login}$`,
  233. "i"
  234. ),
  235. },
  236. (err, user) => {
  237. next(err, user);
  238. }
  239. );
  240. },
  241. (user, next) => {
  242. if (user)
  243. return next(
  244. `An account with that username already exists.`
  245. );
  246. request.get(
  247. {
  248. url: `https://api.github.com/user/emails`,
  249. headers: {
  250. "User-Agent": "request",
  251. Authorization: `token ${access_token}`,
  252. },
  253. },
  254. next
  255. );
  256. },
  257. (httpResponse, body2, next) => {
  258. body2 = JSON.parse(body2);
  259. if (!Array.isArray(body2))
  260. return next(body2.message);
  261. body2.forEach((email) => {
  262. if (email.primary)
  263. address = email.email.toLowerCase();
  264. });
  265. userModel.findOne(
  266. { "email.address": address },
  267. next
  268. );
  269. },
  270. (user, next) => {
  271. this.utils
  272. .runJob("GENERATE_RANDOM_STRING", {
  273. length: 12,
  274. })
  275. .then((_id) => {
  276. next(null, user, _id);
  277. });
  278. },
  279. (user, _id, next) => {
  280. if (user) {
  281. if (Object.keys(JSON.parse(user.services.github)).length === 0)
  282. return next(`An account with that email address exists, but is not linked to GitHub.`)
  283. else
  284. return next(`An account with that email address already exists.`);
  285. }
  286. next(null, {
  287. _id, //TODO Check if exists
  288. username: body.login,
  289. name: body.name,
  290. location: body.location,
  291. bio: body.bio,
  292. email: {
  293. address,
  294. verificationToken,
  295. },
  296. services: {
  297. github: { id: body.id, access_token },
  298. },
  299. });
  300. },
  301. // generate the url for gravatar avatar
  302. (user, next) => {
  303. this.utils
  304. .runJob("CREATE_GRAVATAR", {
  305. email: user.email.address,
  306. })
  307. .then((url) => {
  308. user.avatar = { type: "gravatar", url };
  309. next(null, user);
  310. });
  311. },
  312. // save the new user to the database
  313. (user, next) => {
  314. userModel.create(user, next);
  315. },
  316. // add the activity of account creation
  317. (user, next) => {
  318. activities.runJob("ADD_ACTIVITY", {
  319. userId: user._id,
  320. activityType: "created_account",
  321. });
  322. next(null, user);
  323. },
  324. (user, next) => {
  325. mail.runJob("GET_SCHEMA", {
  326. schemaName: "verifyEmail",
  327. }).then((verifyEmailSchema) => {
  328. verifyEmailSchema(
  329. address,
  330. body.login,
  331. user.email.verificationToken
  332. );
  333. next(null, user._id);
  334. });
  335. },
  336. ],
  337. async (err, userId) => {
  338. if (err && err !== true) {
  339. err = await this.utils.runJob("GET_ERROR", {
  340. error: err,
  341. });
  342. this.log(
  343. "ERROR",
  344. "AUTH_GITHUB_AUTHORIZE_CALLBACK",
  345. `Failed to authorize with GitHub. "${err}"`
  346. );
  347. return redirectOnErr(res, err);
  348. }
  349. const sessionId = await this.utils.runJob("GUID", {});
  350. const sessionSchema = await cache.runJob("GET_SCHEMA", {
  351. schemaName: "session",
  352. });
  353. cache
  354. .runJob("HSET", {
  355. table: "sessions",
  356. key: sessionId,
  357. value: sessionSchema(sessionId, userId),
  358. })
  359. .then(() => {
  360. let date = new Date();
  361. date.setTime(
  362. new Date().getTime() +
  363. 2 * 365 * 24 * 60 * 60 * 1000
  364. );
  365. res.cookie(SIDname, sessionId, {
  366. expires: date,
  367. secure: config.get("cookie.secure"),
  368. path: "/",
  369. domain: config.get("cookie.domain"),
  370. });
  371. this.log(
  372. "INFO",
  373. "AUTH_GITHUB_AUTHORIZE_CALLBACK",
  374. `User "${userId}" successfully authorized with GitHub.`
  375. );
  376. res.redirect(`${config.get("domain")}/`);
  377. })
  378. .catch((err) => {
  379. return redirectOnErr(res, err.message);
  380. });
  381. }
  382. );
  383. });
  384. app.get("/auth/verify_email", async (req, res) => {
  385. if (this.getStatus() !== "READY") {
  386. this.log(
  387. "INFO",
  388. "APP_REJECTED_GITHUB_AUTHORIZE",
  389. `A user tried to use github authorize, but the APP module is currently not ready.`
  390. );
  391. return redirectOnErr(
  392. res,
  393. "Something went wrong on our end. Please try again later."
  394. );
  395. }
  396. let code = req.query.code;
  397. async.waterfall(
  398. [
  399. (next) => {
  400. if (!code) return next("Invalid code.");
  401. next();
  402. },
  403. (next) => {
  404. userModel.findOne(
  405. { "email.verificationToken": code },
  406. next
  407. );
  408. },
  409. (user, next) => {
  410. if (!user) return next("User not found.");
  411. if (user.email.verified)
  412. return next("This email is already verified.");
  413. userModel.updateOne(
  414. { "email.verificationToken": code },
  415. {
  416. $set: { "email.verified": true },
  417. $unset: { "email.verificationToken": "" },
  418. },
  419. { runValidators: true },
  420. next
  421. );
  422. },
  423. ],
  424. (err) => {
  425. if (err) {
  426. let error = "An error occurred.";
  427. if (typeof err === "string") error = err;
  428. else if (err.message) error = err.message;
  429. this.log(
  430. "ERROR",
  431. "VERIFY_EMAIL",
  432. `Verifying email failed. "${error}"`
  433. );
  434. return res.json({
  435. status: "failure",
  436. message: error,
  437. });
  438. }
  439. this.log(
  440. "INFO",
  441. "VERIFY_EMAIL",
  442. `Successfully verified email.`
  443. );
  444. res.redirect(
  445. `${config.get(
  446. "domain"
  447. )}?msg=Thank you for verifying your email`
  448. );
  449. }
  450. );
  451. });
  452. resolve();
  453. });
  454. }
  455. SERVER(payload) {
  456. return new Promise((resolve, reject) => {
  457. resolve(this.server);
  458. });
  459. }
  460. GET_APP(payload) {
  461. return new Promise((resolve, reject) => {
  462. resolve({ app: this.app });
  463. });
  464. }
  465. EXAMPLE_JOB(payload) {
  466. return new Promise((resolve, reject) => {
  467. if (true) {
  468. resolve({});
  469. } else {
  470. reject(new Error("Nothing changed."));
  471. }
  472. });
  473. }
  474. }
  475. module.exports = new AppModule();