mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 14:47:53 +01:00
v3: port static pages, make routes lib more flexible, share middlewares between v2 and v3, port v1, simplify server.js
This commit is contained in:
12
test/api/v3/integration/status/GET-status.test.js
Normal file
12
test/api/v3/integration/status/GET-status.test.js
Normal file
@@ -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',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
40
test/api/v3/unit/middlewares/cors.test.js
Normal file
40
test/api/v3/unit/middlewares/cors.test.js
Normal file
@@ -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;
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -119,6 +119,9 @@ describe('getUserLanguage', () => {
|
|||||||
context('request with session', () => {
|
context('request with session', () => {
|
||||||
it('uses the user preferred language if avalaible', (done) => {
|
it('uses the user preferred language if avalaible', (done) => {
|
||||||
sandbox.stub(User, 'findOne').returns({
|
sandbox.stub(User, 'findOne').returns({
|
||||||
|
lean () {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
exec () {
|
exec () {
|
||||||
return Q.resolve({
|
return Q.resolve({
|
||||||
preferences: {
|
preferences: {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export function generateRes (options = {}) {
|
|||||||
user: generateUser(options.localsUser),
|
user: generateUser(options.localsUser),
|
||||||
group: generateGroup(options.localsGroup),
|
group: generateGroup(options.localsGroup),
|
||||||
},
|
},
|
||||||
|
set: sandbox.stub(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return defaults(options, defaultRes);
|
return defaults(options, defaultRes);
|
||||||
@@ -41,6 +42,7 @@ export function generateReq (options = {}) {
|
|||||||
body: {},
|
body: {},
|
||||||
query: {},
|
query: {},
|
||||||
headers: {},
|
headers: {},
|
||||||
|
header: sandbox.stub().returns(null),
|
||||||
};
|
};
|
||||||
|
|
||||||
return defaults(options, defaultReq);
|
return defaults(options, defaultReq);
|
||||||
|
|||||||
21
website/src/controllers/api-v3/status.js
Normal file
21
website/src/controllers/api-v3/status.js
Normal file
@@ -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;
|
||||||
73
website/src/controllers/pages.js
Normal file
73
website/src/controllers/pages.js
Normal file
@@ -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;
|
||||||
@@ -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
|
* Load nconf and define default configuration values if config.json or ENV vars are not found
|
||||||
*/
|
*/
|
||||||
module.exports.setupConfig = function(){
|
module.exports.setupConfig = function(){
|
||||||
IS_PROD = nconf.get('NODE_ENV') === 'production';
|
|
||||||
BASE_URL = nconf.get('BASE_URL');
|
|
||||||
|
|
||||||
if (nconf.get('IS_DEV'))
|
if (nconf.get('IS_DEV'))
|
||||||
Error.stackTraceLimit = Infinity;
|
Error.stackTraceLimit = Infinity;
|
||||||
if (IS_PROD && nconf.get('NEW_RELIC_ENABLED') === 'true')
|
if (IS_PROD && nconf.get('NEW_RELIC_ENABLED') === 'true')
|
||||||
|
|||||||
31
website/src/libs/api-v3/routes.js
Normal file
31
website/src/libs/api-v3/routes.js
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
21
website/src/libs/api-v3/setupMongoose.js
Normal file
21
website/src/libs/api-v3/setupMongoose.js
Normal file
@@ -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);
|
||||||
24
website/src/libs/api-v3/setupPassport.js
Normal file
24
website/src/libs/api-v3/setupPassport.js
Normal file
@@ -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)));
|
||||||
@@ -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;
|
|
||||||
9
website/src/middlewares/api-v3/cors.js
Normal file
9
website/src/middlewares/api-v3/cors.js
Normal file
@@ -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();
|
||||||
|
};
|
||||||
@@ -75,6 +75,7 @@ module.exports = function getUserLanguage (req, res, next) {
|
|||||||
User.findOne({
|
User.findOne({
|
||||||
_id: req.session.userId,
|
_id: req.session.userId,
|
||||||
}, 'preferences.language')
|
}, 'preferences.language')
|
||||||
|
.lean()
|
||||||
.exec()
|
.exec()
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
req.language = _getFromUser(user, req);
|
req.language = _getFromUser(user, req);
|
||||||
|
|||||||
@@ -1,48 +1,80 @@
|
|||||||
// This module is only used to attach middlewares to the express app
|
// 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 errorHandler from './errorHandler';
|
||||||
import bodyParser from 'body-parser';
|
import bodyParser from 'body-parser';
|
||||||
import routes from '../../libs/api-v3/setupRoutes';
|
|
||||||
import notFoundHandler from './notFound';
|
import notFoundHandler from './notFound';
|
||||||
import nconf from 'nconf';
|
import nconf from 'nconf';
|
||||||
import morgan from 'morgan';
|
import morgan from 'morgan';
|
||||||
import responseHandler from './response';
|
|
||||||
import setupBody from './setupBody';
|
|
||||||
import cookieSession from 'cookie-session';
|
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 IS_PROD = nconf.get('IS_PROD');
|
||||||
const DISABLE_LOGGING = nconf.get('DISABLE_REQUEST_LOGGING');
|
const DISABLE_LOGGING = nconf.get('DISABLE_REQUEST_LOGGING');
|
||||||
|
const PUBLIC_DIR = path.join(__dirname, '/../../../public');
|
||||||
|
|
||||||
const SESSION_SECRET = nconf.get('SESSION_SECRET');
|
const SESSION_SECRET = nconf.get('SESSION_SECRET');
|
||||||
const TWO_WEEKS = 1000 * 60 * 60 * 24 * 14;
|
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'));
|
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({
|
app.use(bodyParser.urlencoded({
|
||||||
extended: true, // Uses 'qs' library as old connect middleware
|
extended: true, // Uses 'qs' library as old connect middleware
|
||||||
}));
|
}));
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
|
app.use(methodOverride()); // TODO still needed in 2016?
|
||||||
|
|
||||||
app.use(cookieSession({
|
app.use(cookieSession({
|
||||||
name: 'connect:sess', // Used to keep backward compatibility with Express 3 cookies
|
name: 'connect:sess', // Used to keep backward compatibility with Express 3 cookies
|
||||||
secret: SESSION_SECRET,
|
secret: SESSION_SECRET,
|
||||||
httpOnly: false, // TODO this should be true for security, what about https only?
|
httpOnly: false, // TODO this should be true for security, what about https only?
|
||||||
maxAge: TWO_WEEKS,
|
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);
|
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);
|
app.use(errorHandler);
|
||||||
};
|
};
|
||||||
|
|||||||
43
website/src/middlewares/api-v3/redirects.js
Normal file
43
website/src/middlewares/api-v3/redirects.js
Normal file
@@ -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();
|
||||||
|
}
|
||||||
19
website/src/middlewares/api-v3/v1.js
Normal file
19
website/src/middlewares/api-v3/v1.js
Normal file
@@ -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;
|
||||||
26
website/src/middlewares/api-v3/v2.js
Normal file
26
website/src/middlewares/api-v3/v2.js
Normal file
@@ -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;
|
||||||
27
website/src/middlewares/api-v3/v3.js
Normal file
27
website/src/middlewares/api-v3/v3.js
Normal file
@@ -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;
|
||||||
@@ -3,6 +3,9 @@ var limiter = require('connect-ratelimit');
|
|||||||
|
|
||||||
var IS_PROD = nconf.get('NODE_ENV') === 'production';
|
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) {
|
module.exports = function(app) {
|
||||||
// TODO review later
|
// TODO review later
|
||||||
// disable the rate limiter middleware
|
// disable the rate limiter middleware
|
||||||
|
|||||||
@@ -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();
|
|
||||||
};
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// TODO do we need this module?
|
||||||
|
|
||||||
module.exports.siteVersion = 1;
|
module.exports.siteVersion = 1;
|
||||||
|
|
||||||
module.exports.middleware = function(req, res, next){
|
module.exports.middleware = function(req, res, next){
|
||||||
|
|||||||
@@ -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();
|
|
||||||
};
|
|
||||||
@@ -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;
|
|
||||||
@@ -1,170 +1,35 @@
|
|||||||
// TODO cleanup all comments when API v3 is finished
|
|
||||||
|
|
||||||
import nconf from 'nconf';
|
import nconf from 'nconf';
|
||||||
import logger from './libs/api-v3/logger';
|
import logger from './libs/api-v3/logger';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import http from 'http';
|
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 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
|
// Setup translations
|
||||||
// let i18n = require('./libs/api-v2/i18n');
|
import './libs/api-v3/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);
|
|
||||||
|
|
||||||
|
// Load config files
|
||||||
|
import './libs/api-v3/setupMongoose';
|
||||||
import './libs/api-v3/firebase';
|
import './libs/api-v3/firebase';
|
||||||
|
import './libs/api-v3/setupPassport';
|
||||||
|
|
||||||
// load schemas & models
|
// Load some schemas & models
|
||||||
import './models/challenge';
|
import './models/challenge';
|
||||||
import './models/group';
|
import './models/group';
|
||||||
import './models/user';
|
import './models/user';
|
||||||
|
|
||||||
// ------------ Passport Configuration ------------
|
app.set('view engine', 'jade');
|
||||||
// let util = require('util')
|
app.set('views', `${__dirname}/../views`);
|
||||||
let FacebookStrategy = passportFacebook.Strategy;
|
|
||||||
|
|
||||||
// Passport session setup.
|
attachMiddlewares(app, server);
|
||||||
// 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);
|
|
||||||
|
|
||||||
server.on('request', app);
|
server.on('request', app);
|
||||||
server.listen(app.get('port'), () => {
|
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;
|
module.exports = server;
|
||||||
|
|||||||
Reference in New Issue
Block a user