app.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  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. return next(
  282. "An account with that email address already exists."
  283. );
  284. next(null, {
  285. _id, //TODO Check if exists
  286. username: body.login,
  287. name: body.name,
  288. location: body.location,
  289. bio: body.bio,
  290. email: {
  291. address,
  292. verificationToken,
  293. },
  294. services: {
  295. github: { id: body.id, access_token },
  296. },
  297. });
  298. },
  299. // generate the url for gravatar avatar
  300. (user, next) => {
  301. this.utils
  302. .runJob("CREATE_GRAVATAR", {
  303. email: user.email.address,
  304. })
  305. .then((url) => {
  306. user.avatar = { type: "gravatar", url };
  307. next(null, user);
  308. });
  309. },
  310. // save the new user to the database
  311. (user, next) => {
  312. userModel.create(user, next);
  313. },
  314. // add the activity of account creation
  315. (user, next) => {
  316. activities.runJob("ADD_ACTIVITY", {
  317. userId: user._id,
  318. activityType: "created_account",
  319. });
  320. next(null, user);
  321. },
  322. (user, next) => {
  323. mail.runJob("GET_SCHEMA", {
  324. schemaName: "verifyEmail",
  325. }).then((verifyEmailSchema) => {
  326. verifyEmailSchema(
  327. address,
  328. body.login,
  329. user.email.verificationToken
  330. );
  331. next(null, user._id);
  332. });
  333. },
  334. ],
  335. async (err, userId) => {
  336. if (err && err !== true) {
  337. err = await this.utils.runJob("GET_ERROR", {
  338. error: err,
  339. });
  340. this.log(
  341. "ERROR",
  342. "AUTH_GITHUB_AUTHORIZE_CALLBACK",
  343. `Failed to authorize with GitHub. "${err}"`
  344. );
  345. return redirectOnErr(res, err);
  346. }
  347. const sessionId = await this.utils.runJob("GUID", {});
  348. const sessionSchema = await cache.runJob("GET_SCHEMA", {
  349. schemaName: "session",
  350. });
  351. cache
  352. .runJob("HSET", {
  353. table: "sessions",
  354. key: sessionId,
  355. value: sessionSchema(sessionId, userId),
  356. })
  357. .then(() => {
  358. let date = new Date();
  359. date.setTime(
  360. new Date().getTime() +
  361. 2 * 365 * 24 * 60 * 60 * 1000
  362. );
  363. res.cookie(SIDname, sessionId, {
  364. expires: date,
  365. secure: config.get("cookie.secure"),
  366. path: "/",
  367. domain: config.get("cookie.domain"),
  368. });
  369. this.log(
  370. "INFO",
  371. "AUTH_GITHUB_AUTHORIZE_CALLBACK",
  372. `User "${userId}" successfully authorized with GitHub.`
  373. );
  374. res.redirect(`${config.get("domain")}/`);
  375. })
  376. .catch((err) => {
  377. return redirectOnErr(res, err.message);
  378. });
  379. }
  380. );
  381. });
  382. app.get("/auth/verify_email", async (req, res) => {
  383. if (this.getStatus() !== "READY") {
  384. this.log(
  385. "INFO",
  386. "APP_REJECTED_GITHUB_AUTHORIZE",
  387. `A user tried to use github authorize, but the APP module is currently not ready.`
  388. );
  389. return redirectOnErr(
  390. res,
  391. "Something went wrong on our end. Please try again later."
  392. );
  393. }
  394. let code = req.query.code;
  395. async.waterfall(
  396. [
  397. (next) => {
  398. if (!code) return next("Invalid code.");
  399. next();
  400. },
  401. (next) => {
  402. userModel.findOne(
  403. { "email.verificationToken": code },
  404. next
  405. );
  406. },
  407. (user, next) => {
  408. if (!user) return next("User not found.");
  409. if (user.email.verified)
  410. return next("This email is already verified.");
  411. userModel.updateOne(
  412. { "email.verificationToken": code },
  413. {
  414. $set: { "email.verified": true },
  415. $unset: { "email.verificationToken": "" },
  416. },
  417. { runValidators: true },
  418. next
  419. );
  420. },
  421. ],
  422. (err) => {
  423. if (err) {
  424. let error = "An error occurred.";
  425. if (typeof err === "string") error = err;
  426. else if (err.message) error = err.message;
  427. this.log(
  428. "ERROR",
  429. "VERIFY_EMAIL",
  430. `Verifying email failed. "${error}"`
  431. );
  432. return res.json({
  433. status: "failure",
  434. message: error,
  435. });
  436. }
  437. this.log(
  438. "INFO",
  439. "VERIFY_EMAIL",
  440. `Successfully verified email.`
  441. );
  442. res.redirect(
  443. `${config.get(
  444. "domain"
  445. )}?msg=Thank you for verifying your email`
  446. );
  447. }
  448. );
  449. });
  450. resolve();
  451. });
  452. }
  453. SERVER(payload) {
  454. return new Promise((resolve, reject) => {
  455. resolve(this.server);
  456. });
  457. }
  458. GET_APP(payload) {
  459. return new Promise((resolve, reject) => {
  460. resolve({ app: this.app });
  461. });
  462. }
  463. EXAMPLE_JOB(payload) {
  464. return new Promise((resolve, reject) => {
  465. if (true) {
  466. resolve({});
  467. } else {
  468. reject(new Error("Nothing changed."));
  469. }
  470. });
  471. }
  472. }
  473. module.exports = new AppModule();