diff --git a/test/api/v3/integration/status/GET-status.test.js b/test/api/v3/integration/status/GET-status.test.js new file mode 100644 index 0000000000..1d4d33a7d7 --- /dev/null +++ b/test/api/v3/integration/status/GET-status.test.js @@ -0,0 +1,12 @@ +import { + requester, +} from '../../../../helpers/api-v3-integration.helper'; + +describe('GET /status', () => { + it('returns status: up', async () => { + let res = await requester().get('/status'); + expect(res).to.eql({ + status: 'up', + }); + }); +}); diff --git a/test/api/v3/unit/middlewares/cors.test.js b/test/api/v3/unit/middlewares/cors.test.js new file mode 100644 index 0000000000..3fd449c963 --- /dev/null +++ b/test/api/v3/unit/middlewares/cors.test.js @@ -0,0 +1,40 @@ +/* eslint-disable global-require */ +import { + generateRes, + generateReq, + generateNext, +} from '../../../../helpers/api-unit.helper'; +import cors from '../../../../../website/src/middlewares/api-v3/cors'; + +describe('cors middleware', () => { + let res, req, next; + + beforeEach(() => { + req = generateReq(); + res = generateRes(); + next = generateNext(); + }); + + it('sets the correct headers', () => { + cors(req, res, next); + expect(res.set).to.have.been.calledWith({ + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'OPTIONS,GET,POST,PUT,HEAD,DELETE', + 'Access-Control-Allow-Headers': 'Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key', + }); + expect(res.sendStatus).to.not.have.been.called; + expect(next).to.have.been.called.once; + }); + + it('responds immediately if method is OPTIONS', () => { + req.method = 'OPTIONS'; + cors(req, res, next); + expect(res.set).to.have.been.calledWith({ + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'OPTIONS,GET,POST,PUT,HEAD,DELETE', + 'Access-Control-Allow-Headers': 'Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key', + }); + expect(res.sendStatus).to.have.been.calledWith(200); + expect(next).to.not.have.been.called; + }); +}); diff --git a/test/api/v3/unit/middlewares/getUserLanguage.test.js b/test/api/v3/unit/middlewares/getUserLanguage.test.js index 43b0cb0ab8..bd7e6aa48c 100644 --- a/test/api/v3/unit/middlewares/getUserLanguage.test.js +++ b/test/api/v3/unit/middlewares/getUserLanguage.test.js @@ -119,6 +119,9 @@ describe('getUserLanguage', () => { context('request with session', () => { it('uses the user preferred language if avalaible', (done) => { sandbox.stub(User, 'findOne').returns({ + lean () { + return this; + }, exec () { return Q.resolve({ preferences: { diff --git a/test/helpers/api-unit.helper.js b/test/helpers/api-unit.helper.js index efcdf09bc3..eb7769256a 100644 --- a/test/helpers/api-unit.helper.js +++ b/test/helpers/api-unit.helper.js @@ -31,6 +31,7 @@ export function generateRes (options = {}) { user: generateUser(options.localsUser), group: generateGroup(options.localsGroup), }, + set: sandbox.stub(), }; return defaults(options, defaultRes); @@ -41,6 +42,7 @@ export function generateReq (options = {}) { body: {}, query: {}, headers: {}, + header: sandbox.stub().returns(null), }; return defaults(options, defaultReq); diff --git a/website/src/controllers/api-v3/status.js b/website/src/controllers/api-v3/status.js new file mode 100644 index 0000000000..9ed050a275 --- /dev/null +++ b/website/src/controllers/api-v3/status.js @@ -0,0 +1,21 @@ +let api = {}; + +/** + * @api {get} /status Get Habitica's status + * @apiVersion 3.0.0 + * @apiName GetStatus + * @apiGroup Status + * + * @apiSuccess {status} string 'up' if everything is ok + */ +api.getStatus = { + method: 'GET', + url: '/status', + async handler (req, res) { + res.respond(200, { + status: 'up', + }); + }, +}; + +module.exports = api; diff --git a/website/src/controllers/pages.js b/website/src/controllers/pages.js new file mode 100644 index 0000000000..2f6887b722 --- /dev/null +++ b/website/src/controllers/pages.js @@ -0,0 +1,73 @@ +import locals from '../middlewares/api-v3/locals'; +import getUserLanguage from '../middlewares/api-v3/getUserLanguage'; +import _ from 'lodash'; + +const marked = require('marked'); + +let api = {}; + +const TOTAL_USER_COUNT = '1,100,000'; + +api.getFrontPage = { + method: 'GET', + url: '/', + middlewares: [getUserLanguage, locals], + async handler (req, res) { + if (!req.header('x-api-user') && !req.header('x-api-key') && !(req.session && req.session.userId)) { + return res.redirect('/static/front'); + } + + res.render('index.jade', { + title: 'Habitica | Your Life The Role Playing Game', + env: res.locals.habitrpg, + }); + }, +}; + +let staticPages = ['front', 'privacy', 'terms', 'api', 'features', + 'videos', 'contact', 'plans', 'new-stuff', 'community-guidelines', + 'old-news', 'press-kit', 'faq', 'overview', 'apps', + 'clear-browser-data', 'merch']; + +_.each(staticPages, (name) => { + api[`get${name}Page`] = { + method: 'GET', + url: `/static/${name}`, + middlewares: [getUserLanguage, locals], + async handler (req, res) { + res.render(`static/${name}.jade`, { + env: res.locals.habitrpg, + marked: marked, + userCount: TOTAL_USER_COUNT + }); + }, + }; +}); + +let shareables = ['level-up', 'hatch-pet', 'raise-pet', 'unlock-quest', 'won-challenge', 'achievement']; + +_.each(shareables, (name) => { + api[`get${name}ShareablePage`] = { + method: 'GET', + url: `/social/${name}`, + middlewares: [getUserLanguage, locals], + async handler (req, res) { + res.render(`social/${name}`, { + env: res.locals.habitrpg, + marked: marked, + userCount: TOTAL_USER_COUNT + }); + }, + }; +}); + +api.redirectExtensionsPage = { + method: 'GET', + url: '/static/extensions', + async handler (req, res) { + res.redirect('http://habitica.wikia.com/wiki/App_and_Extension_Integrations'); + }, +}; + + +module.exports = api; diff --git a/website/src/libs/api-v2/utils.js b/website/src/libs/api-v2/utils.js index 6686bce112..62a69c4322 100644 --- a/website/src/libs/api-v2/utils.js +++ b/website/src/libs/api-v2/utils.js @@ -167,9 +167,6 @@ module.exports.analytics = { track: function() { }, trackPurchase: function() { * Load nconf and define default configuration values if config.json or ENV vars are not found */ module.exports.setupConfig = function(){ - IS_PROD = nconf.get('NODE_ENV') === 'production'; - BASE_URL = nconf.get('BASE_URL'); - if (nconf.get('IS_DEV')) Error.stackTraceLimit = Infinity; if (IS_PROD && nconf.get('NEW_RELIC_ENABLED') === 'true') diff --git a/website/src/libs/api-v3/routes.js b/website/src/libs/api-v3/routes.js new file mode 100644 index 0000000000..0a6f3170ab --- /dev/null +++ b/website/src/libs/api-v3/routes.js @@ -0,0 +1,31 @@ +import fs from 'fs'; +import _ from 'lodash'; + +// Wrapper function to handler `async` route handlers that return promises +// It takes the async function, execute it and pass any error to next (args[2]) +let _wrapAsyncFn = fn => (...args) => fn(...args).catch(args[2]); +let noop = (req, res, next) => next(); + +module.exports.readController = function readController (router, controller) { + _.each(controller, (action) => { + let {method, url, middlewares = [], handler} = action; + + method = method.toLowerCase(); + let fn = handler ? _wrapAsyncFn(handler) : noop; + + router[method](url, ...middlewares, fn); + }); +}; + +module.exports.walkControllers = function walkControllers (router, filePath) { + fs + .readdirSync(filePath) + .forEach(fileName => { + if (!fs.statSync(filePath + fileName).isFile()) { + walkControllers(router, `${filePath}${fileName}/`); + } else if (fileName.match(/\.js$/)) { + let controller = require(filePath + fileName); // eslint-disable-line global-require + module.exports.readController(router, controller); + } + }); +}; diff --git a/website/src/libs/api-v3/setupMongoose.js b/website/src/libs/api-v3/setupMongoose.js new file mode 100644 index 0000000000..303180679c --- /dev/null +++ b/website/src/libs/api-v3/setupMongoose.js @@ -0,0 +1,21 @@ +import nconf from 'nconf'; +import logger from './logger'; +import autoinc from 'mongoose-id-autoinc'; +import mongoose from 'mongoose'; +import Q from 'q'; + +const IS_PROD = nconf.get('IS_PROD'); + +// Use Q promises instead of mpromise in mongoose +mongoose.Promise = Q.Promise; + +let mongooseOptions = !IS_PROD ? {} : { + replset: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } }, + server: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } }, +}; +let db = mongoose.connect(nconf.get('NODE_DB_URI'), mongooseOptions, (err) => { + if (err) throw err; + logger.info('Connected with Mongoose.'); +}); + +autoinc.init(db); diff --git a/website/src/libs/api-v3/setupPassport.js b/website/src/libs/api-v3/setupPassport.js new file mode 100644 index 0000000000..ea5c55a4c2 --- /dev/null +++ b/website/src/libs/api-v3/setupPassport.js @@ -0,0 +1,24 @@ +import passport from 'passport'; +import nconf from 'nconf'; +import passportFacebook from 'passport-facebook'; + +const FacebookStrategy = passportFacebook.Strategy; + +// Passport session setup. +// To support persistent login sessions, Passport needs to be able to +// serialize users into and deserialize users out of the session. Typically, +// this will be as simple as storing the user ID when serializing, and finding +// the user by ID when deserializing. However, since this example does not +// have a database of user records, the complete Facebook profile is serialized +// and deserialized. +passport.serializeUser((user, done) => done(null, user)); +passport.deserializeUser((obj, done) => done(null, obj)); + +// TODO +// This auth strategy is no longer used. It's just kept around for auth.js#loginFacebook() (passport._strategies.facebook.userProfile) +// The proper fix would be to move to a general OAuth module simply to verify accessTokens +passport.use(new FacebookStrategy({ + clientID: nconf.get('FACEBOOK_KEY'), + clientSecret: nconf.get('FACEBOOK_SECRET'), + // callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback" +}, (accessToken, refreshToken, profile, done) => done(null, profile))); diff --git a/website/src/libs/api-v3/setupRoutes.js b/website/src/libs/api-v3/setupRoutes.js deleted file mode 100644 index 3b91b81f25..0000000000 --- a/website/src/libs/api-v3/setupRoutes.js +++ /dev/null @@ -1,37 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import express from 'express'; -import _ from 'lodash'; - -const CONTROLLERS_PATH = path.join(__dirname, '/../../controllers/api-v3/'); -let router = express.Router(); // eslint-disable-line babel/new-cap - -// Wrapper function to handler `async` route handlers that return promises -// It takes the async function, execute it and pass any error to next (args[2]) -let _wrapAsyncFn = fn => (...args) => fn(...args).catch(args[2]); -let noop = (req, res, next) => next(); - -function walkControllers (filePath) { - fs - .readdirSync(filePath) - .forEach(fileName => { - if (!fs.statSync(filePath + fileName).isFile()) { - walkControllers(`${filePath}${fileName}/`); - } else if (fileName.match(/\.js$/)) { - let controller = require(filePath + fileName); // eslint-disable-line global-require - - _.each(controller, (action) => { - let {method, url, middlewares = [], handler} = action; - - method = method.toLowerCase(); - let fn = handler ? _wrapAsyncFn(handler) : noop; - - router[method](url, ...middlewares, fn); - }); - } - }); -} - -walkControllers(CONTROLLERS_PATH); - -module.exports = router; diff --git a/website/src/middlewares/api-v3/cors.js b/website/src/middlewares/api-v3/cors.js new file mode 100644 index 0000000000..c249c183c6 --- /dev/null +++ b/website/src/middlewares/api-v3/cors.js @@ -0,0 +1,9 @@ +module.exports = function corsMiddleware (req, res, next) { + res.set({ + 'Access-Control-Allow-Origin': req.header('origin') || '*', + 'Access-Control-Allow-Methods': 'OPTIONS,GET,POST,PUT,HEAD,DELETE', + 'Access-Control-Allow-Headers': 'Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key', + }); + if (req.method === 'OPTIONS') return res.sendStatus(200); + return next(); +}; diff --git a/website/src/middlewares/api-v3/getUserLanguage.js b/website/src/middlewares/api-v3/getUserLanguage.js index 086121b26b..df372c02b4 100644 --- a/website/src/middlewares/api-v3/getUserLanguage.js +++ b/website/src/middlewares/api-v3/getUserLanguage.js @@ -75,6 +75,7 @@ module.exports = function getUserLanguage (req, res, next) { User.findOne({ _id: req.session.userId, }, 'preferences.language') + .lean() .exec() .then((user) => { req.language = _getFromUser(user, req); diff --git a/website/src/middlewares/api-v3/index.js b/website/src/middlewares/api-v3/index.js index 4f4349944d..7fd6c7a259 100644 --- a/website/src/middlewares/api-v3/index.js +++ b/website/src/middlewares/api-v3/index.js @@ -1,48 +1,80 @@ // This module is only used to attach middlewares to the express app -import expressValidator from 'express-validator'; -import getUserLanguage from './getUserLanguage'; -import analytics from './analytics'; import errorHandler from './errorHandler'; import bodyParser from 'body-parser'; -import routes from '../../libs/api-v3/setupRoutes'; import notFoundHandler from './notFound'; import nconf from 'nconf'; import morgan from 'morgan'; -import responseHandler from './response'; -import setupBody from './setupBody'; import cookieSession from 'cookie-session'; +import cors from './cors'; +import staticMiddleware from './static'; +import domainMiddleware from './domain'; +import mongoose from 'mongoose'; +import compression from 'compression'; +import favicon from 'serve-favicon'; +import methodOverride from 'method-override'; +import passport from 'passport'; +import path from 'path'; +import express from 'express'; +import routes from '../../libs/api-v3/routes'; +import { + forceSSL, + forceHabitica, +} from './redirects'; +import v1 from './v1'; +import v2 from './v2'; +import v3 from './v3'; +import staticPagesController from '../../controllers/pages'; const IS_PROD = nconf.get('IS_PROD'); const DISABLE_LOGGING = nconf.get('DISABLE_REQUEST_LOGGING'); +const PUBLIC_DIR = path.join(__dirname, '/../../../public'); const SESSION_SECRET = nconf.get('SESSION_SECRET'); const TWO_WEEKS = 1000 * 60 * 60 * 24 * 14; -module.exports = function attachMiddlewares (app) { +module.exports = function attachMiddlewares (app, server) { + app.use(domainMiddleware(server, mongoose)); + if (!IS_PROD && !DISABLE_LOGGING) app.use(morgan('dev')); - // TODO handle errors + app.use(compression()); + app.use(favicon(`${PUBLIC_DIR}/favicon.ico`)); + + app.use(cors); + app.use(forceSSL); + app.use(forceHabitica); + + // TODO if we don't manage to move the client off $resource the limit for bodyParser.json must be increased to 1mb from 100kb (default) app.use(bodyParser.urlencoded({ extended: true, // Uses 'qs' library as old connect middleware })); app.use(bodyParser.json()); + app.use(methodOverride()); // TODO still needed in 2016? + app.use(cookieSession({ name: 'connect:sess', // Used to keep backward compatibility with Express 3 cookies secret: SESSION_SECRET, httpOnly: false, // TODO this should be true for security, what about https only? maxAge: TWO_WEEKS, })); - app.use(expressValidator()); - app.use(analytics); - app.use(setupBody); - app.use(responseHandler); - app.use(getUserLanguage); - app.set('view engine', 'jade'); - app.set('views', `${__dirname}/../../../views`); - app.use('/api/v3', routes); + // Initialize Passport! Also use passport.session() middleware, to support + // persistent login sessions (recommended). + app.use(passport.initialize()); + app.use(passport.session()); + + const staticPagesRouter = express.Router(); // eslint-disable-line babel/new-cap + routes.readController(staticPagesRouter, staticPagesController); + app.use('/', staticPagesRouter); + + app.use('/api/v3', v3); + app.use('/api/v2', v2); + app.use('/api/v1', v1); + staticMiddleware(app); + app.use(notFoundHandler); - // Error handler middleware, define as the last one + // Error handler middleware, define as the last one. + // Used for v3 and v1, v2 will keep using its own error handler app.use(errorHandler); }; diff --git a/website/src/middlewares/api-v3/redirects.js b/website/src/middlewares/api-v3/redirects.js new file mode 100644 index 0000000000..a907a89b14 --- /dev/null +++ b/website/src/middlewares/api-v3/redirects.js @@ -0,0 +1,43 @@ +import nconf from 'nconf'; + +const IS_PROD = nconf.get('IS_PROD'); +const IGNORE_REDIRECT = nconf.get('IGNORE_REDIRECT'); +const BASE_URL = nconf.get('BASE_URL'); + +function isHTTP (req) { + return ( // eslint-disable-line no-extra-parens + req.header('x-forwarded-proto') && + req.header('x-forwarded-proto') !== 'https' && + IS_PROD && + BASE_URL.indexOf('https') === 0 + ); +} + +function isProxied (req) { + return ( // eslint-disable-line no-extra-parens + req.header('x-habitica-lb') && + req.header('x-habitica-lb') === 'Yes' + ); +} + +export function forceSSL (req, res, next) { + if (isHTTP(req) && !isProxied(req)) { + return res.redirect(BASE_URL + req.originalUrl); + } + + next(); +} + +// Redirect to habitica for non-api urls + +function nonApiUrl (req) { + return req.originalUrl.search(/\/api\//) === -1; +} + +export function forceHabitica (req, res, next) { + if (IS_PROD && !IGNORE_REDIRECT && !isProxied(req) && nonApiUrl(req)) { + return res.redirect(301, BASE_URL + req.url); + } + + next(); +} diff --git a/website/src/middlewares/api-v3/v1.js b/website/src/middlewares/api-v3/v1.js new file mode 100644 index 0000000000..abe26f42e6 --- /dev/null +++ b/website/src/middlewares/api-v3/v1.js @@ -0,0 +1,19 @@ +// API v1 middlewares and routes +// DEPRECATED AND INACTIVE + +import express from 'express'; +import nconf from 'nconf'; +import { + NotFound, +} from '../../libs/api-v3/errors'; + +const router = express.Router(); // eslint-disable-line babel/new-cap + +const BASE_URL = nconf.get('BASE_URL'); + +router.all('*', function deprecatedV1 (req, res, next) { + let error = new NotFound(`API v1 is no longer supported, please use API v3 instead (${BASE_URL}/static/api).`); + return next(error); +}); + +module.exports = router; diff --git a/website/src/middlewares/api-v3/v2.js b/website/src/middlewares/api-v3/v2.js new file mode 100644 index 0000000000..822927b119 --- /dev/null +++ b/website/src/middlewares/api-v3/v2.js @@ -0,0 +1,26 @@ +// DEPRECATED BUT STILL ACTIVE + +// import path from 'path'; +// import swagger from 'swagger-node-express'; +// import shared from '../../../../common'; +import express from 'express'; + +const v2app = express(); + +// re-set the view options because they are not inherited from the top level app +v2app.set('view engine', 'jade'); +v2app.set('views', `${__dirname}/../../../views`); + +// Custom Directives +// v2app.use('/', require('../../routes/api-v2/auth')); +// v2app.use('/', require('../../routes/api-v2/coupon')); +// v2app.use('/', require('../../routes/api-v2/unsubscription')); + +// const v2routes = express(); +// v2app.use('/api/v2', v2routes); +// v2app.use('/export', require('../../routes/dataexport')); +// require('../../routes/api-v2/swagger')(swagger, v2); + +// v2app.use(require('../api-v2/errorHandler')); + +module.exports = v2app; diff --git a/website/src/middlewares/api-v3/v3.js b/website/src/middlewares/api-v3/v3.js new file mode 100644 index 0000000000..a7c6e0d396 --- /dev/null +++ b/website/src/middlewares/api-v3/v3.js @@ -0,0 +1,27 @@ +import express from 'express'; +import expressValidator from 'express-validator'; +import getUserLanguage from './getUserLanguage'; +import responseHandler from './response'; +import analytics from './analytics'; +import setupBody from './setupBody'; +import routes from '../../libs/api-v3/routes'; +import path from 'path'; + +const v3app = express(); + +// re-set the view options because they are not inherited from the top level app +v3app.set('view engine', 'jade'); +v3app.set('views', `${__dirname}/../../../views`); + +v3app.use(expressValidator()); +v3app.use(analytics); +v3app.use(setupBody); +v3app.use(responseHandler); +v3app.use(getUserLanguage); // TODO move to after auth for authenticated routes + +const CONTROLLERS_PATH = path.join(__dirname, '/../../controllers/api-v3/'); +const router = express.Router(); // eslint-disable-line babel/new-cap +routes.walkControllers(router, CONTROLLERS_PATH); +v3app.use(router); + +module.exports = v3app; diff --git a/website/src/middlewares/apiThrottle.js b/website/src/middlewares/apiThrottle.js index 8de298106c..63995e410c 100644 --- a/website/src/middlewares/apiThrottle.js +++ b/website/src/middlewares/apiThrottle.js @@ -3,6 +3,9 @@ var limiter = require('connect-ratelimit'); var IS_PROD = nconf.get('NODE_ENV') === 'production'; +// TODO since Habitica runs on many different servers this module is pretty useless +// as it will only block requests that go to the same server + module.exports = function(app) { // TODO review later // disable the rate limiter middleware diff --git a/website/src/middlewares/cors.js b/website/src/middlewares/cors.js deleted file mode 100644 index e72db26981..0000000000 --- a/website/src/middlewares/cors.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = function(req, res, next) { - res.header("Access-Control-Allow-Origin", req.headers.origin || "*"); - res.header("Access-Control-Allow-Methods", "OPTIONS,GET,POST,PUT,HEAD,DELETE"); - res.header("Access-Control-Allow-Headers", "Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key"); - if (req.method === 'OPTIONS') return res.sendStatus(200); - return next(); -}; diff --git a/website/src/middlewares/forceRefresh.js b/website/src/middlewares/forceRefresh.js index 6d5b4fa8c9..577ad2f4c6 100644 --- a/website/src/middlewares/forceRefresh.js +++ b/website/src/middlewares/forceRefresh.js @@ -1,3 +1,5 @@ +// TODO do we need this module? + module.exports.siteVersion = 1; module.exports.middleware = function(req, res, next){ diff --git a/website/src/middlewares/redirects.js b/website/src/middlewares/redirects.js deleted file mode 100644 index ddc135beab..0000000000 --- a/website/src/middlewares/redirects.js +++ /dev/null @@ -1,41 +0,0 @@ -var nconf = require('nconf'); -var IS_PROD = nconf.get('NODE_ENV') === 'production'; -var ignoreRedirect = nconf.get('IGNORE_REDIRECT'); -var BASE_URL = nconf.get('BASE_URL'); - -function isHTTP(req) { - return ( - req.headers['x-forwarded-proto'] && - req.headers['x-forwarded-proto'] !== 'https' && - IS_PROD && - BASE_URL.indexOf('https') === 0 - ); -} - -function isProxied(req) { - return ( - req.headers['x-habitica-lb'] && - req.headers['x-habitica-lb'] === 'Yes' - ); -} - -module.exports.forceSSL = function(req, res, next){ - if(isHTTP(req) && !isProxied(req)) { - return res.redirect(BASE_URL + req.url); - } - - next(); -}; - -// Redirect to habitica for non-api urls - -function nonApiUrl(req) { - return req.url.search(/\/api\//) === -1; -} - -module.exports.forceHabitica = function(req, res, next) { - if (IS_PROD && !ignoreRedirect && !isProxied(req) && nonApiUrl(req)) { - return res.redirect(301, BASE_URL + req.url); - } - next(); -}; diff --git a/website/src/routes/api-v1.js b/website/src/routes/api-v1.js deleted file mode 100644 index 1178428603..0000000000 --- a/website/src/routes/api-v1.js +++ /dev/null @@ -1,13 +0,0 @@ -var express = require('express'); -var router = express.Router(); -var nconf = require('nconf'); - -/* ---------- Deprecated API ------------*/ - -router.all('*', function deprecated(req, res, next) { - res.json(404, { - err: 'API v1 is no longer supported, please use API v2 instead ' + nconf.get('BASE_URL') + '/static/api' - }); -}); - -module.exports = router; diff --git a/website/src/server.js b/website/src/server.js index 14dd392e68..95280ffaf6 100644 --- a/website/src/server.js +++ b/website/src/server.js @@ -1,170 +1,35 @@ -// TODO cleanup all comments when API v3 is finished - import nconf from 'nconf'; import logger from './libs/api-v3/logger'; import express from 'express'; import http from 'http'; -// import path from 'path'; -// let swagger = require('swagger-node-express'); -import autoinc from 'mongoose-id-autoinc'; -import passport from 'passport'; -// let shared = require('../../common'); -import passportFacebook from 'passport-facebook'; -import mongoose from 'mongoose'; -import Q from 'q'; -import domainMiddleware from './middlewares/api-v3/domain'; import attachMiddlewares from './middlewares/api-v3/index'; -import staticMiddleware from './middlewares/api-v3/static'; + +const server = http.createServer(); +const app = express(); + +app.set('port', nconf.get('PORT')); // Setup translations -// let i18n = require('./libs/api-v2/i18n'); - -const IS_PROD = nconf.get('IS_PROD'); -// const IS_DEV = nconf.get('IS_DEV'); -// const DISABLE_LOGGING = nconf.get('DISABLE_REQUEST_LOGGING'); -// const TWO_WEEKS = 1000 * 60 * 60 * 24 * 14; - -let server = http.createServer(); -let app = express(); - -// Mongoose configuration - -// Use Q promises instead of mpromise in mongoose -mongoose.Promise = Q.Promise; -let mongooseOptions = !IS_PROD ? {} : { - replset: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } }, - server: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } }, -}; -let db = mongoose.connect(nconf.get('NODE_DB_URI'), mongooseOptions, (err) => { - if (err) throw err; - logger.info('Connected with Mongoose'); -}); - -autoinc.init(db); +import './libs/api-v3/i18n'; +// Load config files +import './libs/api-v3/setupMongoose'; import './libs/api-v3/firebase'; +import './libs/api-v3/setupPassport'; -// load schemas & models +// Load some schemas & models import './models/challenge'; import './models/group'; import './models/user'; -// ------------ Passport Configuration ------------ -// let util = require('util') -let FacebookStrategy = passportFacebook.Strategy; +app.set('view engine', 'jade'); +app.set('views', `${__dirname}/../views`); -// Passport session setup. -// To support persistent login sessions, Passport needs to be able to -// serialize users into and deserialize users out of the session. Typically, -// this will be as simple as storing the user ID when serializing, and finding -// the user by ID when deserializing. However, since this example does not -// have a database of user records, the complete Facebook profile is serialized -// and deserialized. -passport.serializeUser((user, done) => done(null, user)); -passport.deserializeUser((obj, done) => done(null, obj)); - -// FIXME -// This auth strategy is no longer used. It's just kept around for auth.js#loginFacebook() (passport._strategies.facebook.userProfile) -// The proper fix would be to move to a general OAuth module simply to verify accessTokens -passport.use(new FacebookStrategy({ - clientID: nconf.get('FACEBOOK_KEY'), - clientSecret: nconf.get('FACEBOOK_SECRET'), - // callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback" -}, (accessToken, refreshToken, profile, done) => done(null, profile))); - -// ------------ Server Configuration ------------ -// let publicDir = path.join(__dirname, '/../public'); - -app.set('port', nconf.get('PORT')); - -// Setup two different Express apps, one that matches everything except '/api/v3' -// and the other for /api/v3 routes, so we can keep the old an new api versions completely separate -// not sharing a single middleware if we don't want to -let oldApp = express(); // api v1 and v2, and not scoped routes -let newApp = express(); // api v3 - -app.use(domainMiddleware(server, mongoose)); -// Route requests to the right app -// Matches all request except the ones going to /api/v3/** -app.all(/^(?!\/api\/v3).+/i, oldApp); -// Matches all requests going to /api/v3 -app.all('/api/*', newApp); - -// TODO change ^ so that all routes except those marked explictly with api/v2 goes to oldApp - -// Mount middlewares for the new app (api v3) -attachMiddlewares(newApp); - -/* OLD APP IS DISABLED UNTIL COMPATIBLE WITH NEW MODELS -//require('./middlewares/apiThrottle')(oldApp); -oldApp.use(require('./middlewares/api-v2/domain')(server,mongoose)); -if (!IS_PROD && !DISABLE_LOGGING) oldApp.use(require('morgan')("dev")); -oldApp.use(require('compression')()); -oldApp.set("views", __dirname + "/../views"); -oldApp.set("view engine", "jade"); -oldApp.use(require('serve-favicon')(publicDir + '/favicon.ico')); -oldApp.use(require('./middlewares/cors')); - -var redirects = require('./middlewares/redirects'); -oldApp.use(redirects.forceHabitica); -oldApp.use(redirects.forceSSL); -var bodyParser = require('body-parser'); -// Default limit is 100kb, need that because we actually send whole groups to the server -// FIXME as soon as possible (need to move on the client from $resource -> $http) -var BODY_PARSER_LIMIT = '1mb'; -oldApp.use(bodyParser.urlencoded({ - extended: true, - parameterLimit: 10000, // Upped for safety from 1k, FIXME as above - limit: BODY_PARSER_LIMIT, -})); -oldApp.use(bodyParser.json({ - limit: BODY_PARSER_LIMIT, -})); -oldApp.use(require('method-override')()); - -oldApp.use(require('cookie-session')({ - name: 'connect:sess', // Used to keep backward compatibility with Express 3 cookies - secret: nconf.get('SESSION_SECRET'), - httpOnly: false, - maxAge: TWO_WEEKS -})); - -// Initialize Passport! Also use passport.session() middleware, to support -// persistent login sessions (recommended). -oldApp.use(passport.initialize()); -oldApp.use(passport.session()); - -// Custom Directives -oldApp.use('/', require('./routes/pages')); -oldApp.use('/', require('./routes/payments')); -oldApp.use('/', require('./routes/api-v2/auth')); -oldApp.use('/', require('./routes/api-v2/coupon')); -oldApp.use('/', require('./routes/api-v2/unsubscription')); -var v2 = express(); -oldApp.use('/api/v2', v2); -oldApp.use('/api/', require('./routes/api-v1')); -oldApp.use('/export', require('./routes/dataexport')); -require('./routes/api-v2/swagger')(swagger, v2); - -// Cache emojis without copying them to build, they are too many - -oldApp.use(require('./middlewares/api-v2/errorHandler')); -* -let maxAge = IS_PROD ? 31536000000 : 0; - -oldApp.use(express.static(path.join(__dirname, '/../build'), { maxAge })); -oldApp.use('/common/dist', express.static(`${publicDir}/../../common/dist`, { maxAge })); -oldApp.use('/common/audio', express.static(`${publicDir}/../../common/audio`, { maxAge })); -oldApp.use('/common/script/public', express.static(`${publicDir}/../../common/script/public`, { maxAge })); -oldApp.use('/common/img', express.static(`${publicDir}/../../common/img`, { maxAge })); -oldApp.use(express.static(publicDir)); -*/ - -staticMiddleware(app); +attachMiddlewares(app, server); server.on('request', app); server.listen(app.get('port'), () => { - return logger.info(`Express server listening on port ${app.get('port')}`); + logger.info(`Express server listening on port ${app.get('port')}`); }); module.exports = server;