app.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  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?access_token=${access_token}`,
  137. headers: { "User-Agent": "request" },
  138. },
  139. next
  140. );
  141. },
  142. (httpResponse, _body, next) => {
  143. body = _body = JSON.parse(_body);
  144. if (httpResponse.statusCode !== 200)
  145. return next(body.message);
  146. if (state) {
  147. return async.waterfall(
  148. [
  149. (next) => {
  150. cache
  151. .runJob("HGET", {
  152. table: "sessions",
  153. key: state,
  154. })
  155. .then((session) =>
  156. next(null, session)
  157. )
  158. .catch(next);
  159. },
  160. (session, next) => {
  161. if (!session)
  162. return next("Invalid session.");
  163. userModel.findOne(
  164. { _id: session.userId },
  165. next
  166. );
  167. },
  168. (user, next) => {
  169. if (!user)
  170. return next("User not found.");
  171. if (
  172. user.services.github &&
  173. user.services.github.id
  174. )
  175. return next(
  176. "Account already has GitHub linked."
  177. );
  178. userModel.updateOne(
  179. { _id: user._id },
  180. {
  181. $set: {
  182. "services.github": {
  183. id: body.id,
  184. access_token,
  185. },
  186. },
  187. },
  188. { runValidators: true },
  189. (err) => {
  190. if (err) return next(err);
  191. next(null, user, body);
  192. }
  193. );
  194. },
  195. (user) => {
  196. cache.runJob("PUB", {
  197. channel: "user.linkGithub",
  198. value: user._id,
  199. });
  200. res.redirect(
  201. `${config.get(
  202. "domain"
  203. )}/settings`
  204. );
  205. },
  206. ],
  207. next
  208. );
  209. }
  210. if (!body.id)
  211. return next("Something went wrong, no id.");
  212. userModel.findOne(
  213. { "services.github.id": body.id },
  214. (err, user) => {
  215. next(err, user, body);
  216. }
  217. );
  218. },
  219. (user, body, next) => {
  220. if (user) {
  221. user.services.github.access_token = access_token;
  222. return user.save(() => {
  223. next(true, user._id);
  224. });
  225. }
  226. userModel.findOne(
  227. {
  228. username: new RegExp(
  229. `^${body.login}$`,
  230. "i"
  231. ),
  232. },
  233. (err, user) => {
  234. next(err, user);
  235. }
  236. );
  237. },
  238. (user, next) => {
  239. if (user)
  240. return next(
  241. "An account with that username already exists."
  242. );
  243. request.get(
  244. {
  245. url: `https://api.github.com/user/emails?access_token=${access_token}`,
  246. headers: { "User-Agent": "request" },
  247. },
  248. next
  249. );
  250. },
  251. (httpResponse, body2, next) => {
  252. body2 = JSON.parse(body2);
  253. if (!Array.isArray(body2))
  254. return next(body2.message);
  255. body2.forEach((email) => {
  256. if (email.primary)
  257. address = email.email.toLowerCase();
  258. });
  259. userModel.findOne(
  260. { "email.address": address },
  261. next
  262. );
  263. },
  264. (user, next) => {
  265. this.utils
  266. .runJob("GENERATE_RANDOM_STRING", {
  267. length: 12,
  268. })
  269. .then((_id) => {
  270. next(null, user, _id);
  271. });
  272. },
  273. (user, _id, next) => {
  274. if (user)
  275. return next(
  276. "An account with that email address already exists."
  277. );
  278. next(null, {
  279. _id, //TODO Check if exists
  280. username: body.login,
  281. name: body.name,
  282. location: body.location,
  283. bio: body.bio,
  284. email: {
  285. address,
  286. verificationToken,
  287. },
  288. services: {
  289. github: { id: body.id, access_token },
  290. },
  291. });
  292. },
  293. // generate the url for gravatar avatar
  294. (user, next) => {
  295. this.utils
  296. .runJob("CREATE_GRAVATAR", {
  297. email: user.email.address,
  298. })
  299. .then((url) => {
  300. user.avatar = { type: "gravatar", url };
  301. next(null, user);
  302. });
  303. },
  304. // save the new user to the database
  305. (user, next) => {
  306. userModel.create(user, next);
  307. },
  308. // add the activity of account creation
  309. (user, next) => {
  310. activities.runJob("ADD_ACTIVITY", {
  311. userId: user._id,
  312. activityType: "created_account",
  313. });
  314. next(null, user);
  315. },
  316. (user, next) => {
  317. mail.runJob("GET_SCHEMA", {
  318. schemaName: "verifyEmail",
  319. }).then((verifyEmailSchema) => {
  320. verifyEmailSchema(
  321. address,
  322. body.login,
  323. user.email.verificationToken
  324. );
  325. next(null, user._id);
  326. });
  327. },
  328. ],
  329. async (err, userId) => {
  330. if (err && err !== true) {
  331. err = await this.utils.getError(err);
  332. logger.error(
  333. "AUTH_GITHUB_AUTHORIZE_CALLBACK",
  334. `Failed to authorize with GitHub. "${err}"`
  335. );
  336. return redirectOnErr(res, err);
  337. }
  338. const sessionId = await this.utils.runJob("GUID", {});
  339. const sessionSchema = await cache.runJob("GET_SCHEMA", {
  340. schemaName: "session",
  341. });
  342. cache
  343. .runJob("HSET", {
  344. table: "sessions",
  345. key: sessionId,
  346. value: sessionSchema(sessionId, userId),
  347. })
  348. .then(() => {
  349. let date = new Date();
  350. date.setTime(
  351. new Date().getTime() +
  352. 2 * 365 * 24 * 60 * 60 * 1000
  353. );
  354. res.cookie(SIDname, sessionId, {
  355. expires: date,
  356. secure: config.get("cookie.secure"),
  357. path: "/",
  358. domain: config.get("cookie.domain"),
  359. });
  360. logger.success(
  361. "AUTH_GITHUB_AUTHORIZE_CALLBACK",
  362. `User "${userId}" successfully authorized with GitHub.`
  363. );
  364. res.redirect(`${config.get("domain")}/`);
  365. })
  366. .catch((err) => {
  367. return redirectOnErr(res, err.message);
  368. });
  369. }
  370. );
  371. });
  372. app.get("/auth/verify_email", async (req, res) => {
  373. if (this.getStatus() !== "READY") {
  374. this.log(
  375. "INFO",
  376. "APP_REJECTED_GITHUB_AUTHORIZE",
  377. `A user tried to use github authorize, but the APP module is currently not ready.`
  378. );
  379. return redirectOnErr(
  380. res,
  381. "Something went wrong on our end. Please try again later."
  382. );
  383. }
  384. let code = req.query.code;
  385. async.waterfall(
  386. [
  387. (next) => {
  388. if (!code) return next("Invalid code.");
  389. next();
  390. },
  391. (next) => {
  392. userModel.findOne(
  393. { "email.verificationToken": code },
  394. next
  395. );
  396. },
  397. (user, next) => {
  398. if (!user) return next("User not found.");
  399. if (user.email.verified)
  400. return next("This email is already verified.");
  401. userModel.updateOne(
  402. { "email.verificationToken": code },
  403. {
  404. $set: { "email.verified": true },
  405. $unset: { "email.verificationToken": "" },
  406. },
  407. { runValidators: true },
  408. next
  409. );
  410. },
  411. ],
  412. (err) => {
  413. if (err) {
  414. let error = "An error occurred.";
  415. if (typeof err === "string") error = err;
  416. else if (err.message) error = err.message;
  417. logger.error(
  418. "VERIFY_EMAIL",
  419. `Verifying email failed. "${error}"`
  420. );
  421. return res.json({
  422. status: "failure",
  423. message: error,
  424. });
  425. }
  426. logger.success(
  427. "VERIFY_EMAIL",
  428. `Successfully verified email.`
  429. );
  430. res.redirect(
  431. `${config.get(
  432. "domain"
  433. )}?msg=Thank you for verifying your email`
  434. );
  435. }
  436. );
  437. });
  438. resolve();
  439. });
  440. }
  441. SERVER(payload) {
  442. return new Promise((resolve, reject) => {
  443. resolve(this.server);
  444. });
  445. }
  446. GET_APP(payload) {
  447. return new Promise((resolve, reject) => {
  448. resolve({ app: this.app });
  449. });
  450. }
  451. EXAMPLE_JOB(payload) {
  452. return new Promise((resolve, reject) => {
  453. if (true) {
  454. resolve({});
  455. } else {
  456. reject(new Error("Nothing changed."));
  457. }
  458. });
  459. }
  460. }
  461. module.exports = new AppModule();