123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457 |
- import config from "config";
- import async from "async";
- import request from "request";
- import cors from "cors";
- import cookieParser from "cookie-parser";
- import bodyParser from "body-parser";
- import express from "express";
- import { OAuth2 } from "oauth";
- import CoreClass from "../core";
- class AppModule extends CoreClass {
- constructor() {
- super("app");
- }
- initialize() {
- return new Promise(resolve => {
- const { mail } = this.moduleManager.modules;
- const { cache } = this.moduleManager.modules;
- const { db } = this.moduleManager.modules;
- const { activities } = this.moduleManager.modules;
- this.utils = this.moduleManager.modules.utils;
- const app = (this.app = express());
- const SIDname = config.get("cookie.SIDname");
- this.server = app.listen(config.get("serverPort"));
- app.use(cookieParser());
- app.use(bodyParser.json());
- app.use(bodyParser.urlencoded({ extended: true }));
- let userModel;
- db.runJob("GET_MODEL", { modelName: "user" })
- .then(model => {
- userModel = model;
- })
- .catch(console.error);
- const corsOptions = { ...config.get("cors") };
- app.use(cors(corsOptions));
- app.options("*", cors(corsOptions));
- 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("serverDomain")}/auth/github/authorize/callback`;
- /**
- * @param {object} res - response object from Express
- * @param {string} err - custom error message
- */
- function redirectOnErr(res, err) {
- res.redirect(`${config.get("domain")}/?err=${encodeURIComponent(err)}`);
- }
- 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=${config.get("serverDomain")}/auth/github/authorize/callback`,
- `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=${config.get("serverDomain")}/auth/github/authorize/callback`,
- `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 this.utils.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;
- return request.get(
- {
- url: `https://api.github.com/user`,
- headers: {
- "User-Agent": "request",
- Authorization: `token ${accessToken}`
- }
- },
- next
- );
- },
- (httpResponse, _body, next) => {
- body = _body = JSON.parse(_body);
- if (httpResponse.statusCode !== 200) return next(body.message);
- if (state) {
- return async.waterfall(
- [
- next => {
- cache
- .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: body.id,
- accessToken
- }
- }
- },
- { runValidators: true },
- err => {
- if (err) return next(err);
- return next(null, user, body);
- }
- );
- },
- user => {
- cache.runJob("PUB", {
- channel: "user.linkGithub",
- value: user._id
- });
- res.redirect(`${config.get("domain")}/settings#security`);
- }
- ],
- next
- );
- }
- if (!body.id) return next("Something went wrong, no id.");
- return userModel.findOne({ "services.github.id": body.id }, (err, user) => {
- next(err, user, body);
- });
- },
- (user, body, next) => {
- 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 request.get(
- {
- url: `https://api.github.com/user/emails`,
- headers: {
- "User-Agent": "request",
- Authorization: `token ${accessToken}`
- }
- },
- next
- );
- },
- (httpResponse, body2, next) => {
- body2 = JSON.parse(body2);
- if (!Array.isArray(body2)) return next(body2.message);
- body2.forEach(email => {
- if (email.primary) address = email.email.toLowerCase();
- });
- return userModel.findOne({ "email.address": address }, next);
- },
- (user, next) => {
- this.utils
- .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, // TODO Check if exists
- username: body.login,
- name: body.name,
- location: body.location,
- bio: body.bio,
- email: {
- address,
- verificationToken
- },
- services: {
- github: { id: body.id, accessToken }
- }
- });
- },
- // generate the url for gravatar avatar
- (user, next) => {
- this.utils
- .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);
- },
- // add the activity of account creation
- (user, next) => {
- activities.runJob("ADD_ACTIVITY", {
- userId: user._id,
- activityType: "created_account"
- });
- next(null, user);
- },
- (user, next) => {
- mail.runJob("GET_SCHEMA", {
- schemaName: "verifyEmail"
- }).then(verifyEmailSchema => {
- verifyEmailSchema(address, body.login, user.email.verificationToken, err => {
- next(err, user._id);
- });
- });
- }
- ],
- async (err, userId) => {
- if (err && err !== true) {
- err = await this.utils.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 this.utils.runJob("GUID", {});
- const sessionSchema = await cache.runJob("GET_SCHEMA", {
- schemaName: "session"
- });
- return cache
- .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("cookie.secure"),
- path: "/",
- domain: config.get("cookie.domain")
- });
- this.log(
- "INFO",
- "AUTH_GITHUB_AUTHORIZE_CALLBACK",
- `User "${userId}" successfully authorized with GitHub.`
- );
- res.redirect(`${config.get("domain")}/`);
- })
- .catch(err => redirectOnErr(res, err.message));
- }
- );
- });
- app.get("/auth/verify_email", 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;
- 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: "failure",
- message: error
- });
- }
- this.log("INFO", "VERIFY_EMAIL", `Successfully verified email.`);
- return res.redirect(`${config.get("domain")}?msg=Thank you for verifying your email`);
- }
- );
- });
- return resolve();
- });
- }
- SERVER() {
- return new Promise(resolve => {
- resolve(this.server);
- });
- }
- GET_APP() {
- return new Promise(resolve => {
- resolve({ app: this.app });
- });
- }
- // EXAMPLE_JOB() {
- // return new Promise((resolve, reject) => {
- // if (true) resolve({});
- // else reject(new Error("Nothing changed."));
- // });
- // }
- }
- export default new AppModule();
|