From ed0e4c6d20dc9b8663af1655ab3c4a2077ec384e Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Fri, 30 Oct 2015 15:05:50 +0100 Subject: [PATCH] Reorganize files under /src, separate express app in two apps, one for api v1 and v2 and the other \ for the upcoming api v3 Reorganize files under /src, separate express app in two apps, one for api v1 and v2 and the other for the upcoming api v3 move api v2 routes in a separate folder, rename apiv1 file for better readability, remove auth routes for api v1 move api-v2 controllers in subdirectory move unorganized files to /libs fix gulp requires and separate server in old (api v1 and v2) and new (api v3) app fix require paths fix require paths fix require paths put api v1 back Reorganize files under /src and separate express app in one for api v1 and v2 and the other for v3 --- Gruntfile.js | 2 +- tasks/gulp-console.js | 4 +- test/api-legacy/pushNotifications.coffee | 6 +- test/common/algos.mocha.coffee | 2 +- test/common/dailies.coffee | 2 +- test/common/user.fns.ultimateGear.test.js | 2 +- test/helpers/api.helper.js | 2 +- test/helpers/content.helper.js | 2 +- test/server_side/analytics.test.js | 6 +- test/server_side/controllers/groups.test.js | 4 +- test/server_side/controllers/user.test.js | 4 +- test/server_side/webhooks.test.js | 2 +- website/src/controllers/{ => api-v2}/auth.js | 10 +- .../controllers/{ => api-v2}/challenges.js | 14 +- .../src/controllers/{ => api-v2}/coupon.js | 2 +- .../src/controllers/{ => api-v2}/groups.js | 16 +- website/src/controllers/{ => api-v2}/hall.js | 6 +- .../src/controllers/{ => api-v2}/members.js | 8 +- .../{ => api-v2}/unsubscription.js | 8 +- website/src/controllers/{ => api-v2}/user.js | 18 +- website/src/controllers/payments/index.js | 4 +- website/src/controllers/payments/paypal.js | 2 +- website/src/{ => libs}/analytics.js | 2 +- website/src/{ => libs}/i18n.js | 8 +- website/src/{ => libs}/logging.js | 0 website/src/{ => libs}/utils.js | 2 +- website/src/{ => libs}/webhook.js | 0 website/src/middlewares/errorHandler.js | 2 +- website/src/middlewares/locals.js | 4 +- website/src/models/group.js | 2 +- website/src/routes/{apiv1.js => api-v1.js} | 10 +- website/src/routes/{ => api-v2}/auth.js | 11 +- website/src/routes/{ => api-v2}/coupon.js | 6 +- website/src/routes/api-v2/swagger.js | 777 ++++++++++++++++ .../src/routes/{ => api-v2}/unsubscription.js | 4 +- website/src/routes/apiv2.coffee | 853 ------------------ website/src/routes/dataexport.js | 4 +- website/src/routes/pages.js | 4 +- website/src/routes/payments.js | 4 +- website/src/seed.js | 55 -- website/src/server.js | 92 +- 41 files changed, 923 insertions(+), 1043 deletions(-) rename website/src/controllers/{ => api-v2}/auth.js (98%) rename website/src/controllers/{ => api-v2}/challenges.js (97%) rename website/src/controllers/{ => api-v2}/coupon.js (95%) rename website/src/controllers/{ => api-v2}/groups.js (98%) rename website/src/controllers/{ => api-v2}/hall.js (95%) rename website/src/controllers/{ => api-v2}/members.js (95%) rename website/src/controllers/{ => api-v2}/unsubscription.js (84%) rename website/src/controllers/{ => api-v2}/user.js (97%) rename website/src/{ => libs}/analytics.js (98%) rename website/src/{ => libs}/i18n.js (94%) rename website/src/{ => libs}/logging.js (100%) rename website/src/{ => libs}/utils.js (98%) rename website/src/{ => libs}/webhook.js (100%) rename website/src/routes/{apiv1.js => api-v1.js} (96%) rename website/src/routes/{ => api-v2}/auth.js (89%) rename website/src/routes/{ => api-v2}/coupon.js (75%) create mode 100644 website/src/routes/api-v2/swagger.js rename website/src/routes/{ => api-v2}/unsubscription.js (60%) delete mode 100644 website/src/routes/apiv2.coffee delete mode 100644 website/src/seed.js diff --git a/Gruntfile.js b/Gruntfile.js index 5b03657165..be40b40816 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -140,7 +140,7 @@ module.exports = function(grunt) { grunt.registerTask('test:prepare:translations', function() { require('coffee-script'); - var i18n = require('./website/src/i18n'), + var i18n = require('./website/src/libs/i18n'), fs = require('fs'); fs.writeFileSync('test/spec/mocks/translations.js', "if(!window.env) window.env = {};\n" + diff --git a/tasks/gulp-console.js b/tasks/gulp-console.js index 5b98853845..95a1962fd4 100644 --- a/tasks/gulp-console.js +++ b/tasks/gulp-console.js @@ -2,9 +2,9 @@ import 'coffee-script'; import mongoose from 'mongoose'; import autoinc from 'mongoose-id-autoinc'; -import logging from '../website/src/logging'; +import logging from '../website/src/libs/logging'; import nconf from 'nconf'; -import utils from '../website/src/utils'; +import utils from '../website/src/libs/utils'; import repl from 'repl'; import gulp from 'gulp'; diff --git a/test/api-legacy/pushNotifications.coffee b/test/api-legacy/pushNotifications.coffee index 3cbac5ab3d..d5df5d51e9 100644 --- a/test/api-legacy/pushNotifications.coffee +++ b/test/api-legacy/pushNotifications.coffee @@ -19,7 +19,7 @@ describe "Push-Notifications", -> done() context "Challenges", -> - challenges = rewire("../../website/src/controllers/challenges") + challenges = rewire("../../website/src/controllers/api-v2/challenges") challenges.__set__('pushNotify', pushSpy) challengeMock = { findById: (arg, cb) -> @@ -65,7 +65,7 @@ describe "Push-Notifications", -> recipient = null - groups = rewire("../../website/src/controllers/groups") + groups = rewire("../../website/src/controllers/api-v2/groups") groups.__set__('pushNotify', pushSpy) before (done) -> @@ -226,7 +226,7 @@ describe "Push-Notifications", -> , false context "sending gems from balance", -> - members = rewire("../../website/src/controllers/members") + members = rewire("../../website/src/controllers/api-v2/members") members.sendMessage = -> true members.__set__('pushNotify', pushSpy) diff --git a/test/common/algos.mocha.coffee b/test/common/algos.mocha.coffee index 642156b752..b413c3e000 100644 --- a/test/common/algos.mocha.coffee +++ b/test/common/algos.mocha.coffee @@ -3,7 +3,7 @@ expect = require 'expect.js' sinon = require 'sinon' moment = require 'moment' shared = require '../../common/script/index.coffee' -shared.i18n.translations = require('../../website/src/i18n.js').translations +shared.i18n.translations = require('../../website/src/libs/i18n.js').translations test_helper = require './test_helper' test_helper.addCustomMatchers() $w = (s)->s.split(' ') diff --git a/test/common/dailies.coffee b/test/common/dailies.coffee index d06c291f27..e5e3fad81a 100644 --- a/test/common/dailies.coffee +++ b/test/common/dailies.coffee @@ -3,7 +3,7 @@ expect = require 'expect.js' sinon = require 'sinon' moment = require 'moment' shared = require '../../common/script/index.coffee' -shared.i18n.translations = require('../../website/src/i18n.js').translations +shared.i18n.translations = require('../../website/src/libs/i18n.js').translations repeatWithoutLastWeekday = ()-> repeat = {su:true,m:true,t:true,w:true,th:true,f:true,s:true} diff --git a/test/common/user.fns.ultimateGear.test.js b/test/common/user.fns.ultimateGear.test.js index b7c091e3f5..d2b82f43e8 100644 --- a/test/common/user.fns.ultimateGear.test.js +++ b/test/common/user.fns.ultimateGear.test.js @@ -1,7 +1,7 @@ 'use strict'; var shared = require('../../common/script/index.coffee'); -shared.i18n.translations = require('../../website/src/i18n.js').translations +shared.i18n.translations = require('../../website/src/libs/i18n.js').translations require('./test_helper'); diff --git a/test/helpers/api.helper.js b/test/helpers/api.helper.js index 4a2084e4ea..f34c0d1384 100644 --- a/test/helpers/api.helper.js +++ b/test/helpers/api.helper.js @@ -9,7 +9,7 @@ import {v4 as generateUUID} from 'uuid'; import superagent from 'superagent'; import i18n from '../../common/script/src/i18n'; require('coffee-script'); -i18n.translations = require('../../website/src/i18n.js').translations; +i18n.translations = require('../../website/src/libs/i18n.js').translations; const API_TEST_SERVER_PORT = 3003; diff --git a/test/helpers/content.helper.js b/test/helpers/content.helper.js index 83178c1a52..c54a62d508 100644 --- a/test/helpers/content.helper.js +++ b/test/helpers/content.helper.js @@ -3,7 +3,7 @@ import {each} from 'lodash'; import i18n from '../../common/script/src/i18n'; require('coffee-script'); -i18n.translations = require('../../website/src/i18n.js').translations; +i18n.translations = require('../../website/src/libs/i18n.js').translations; export const STRING_ERROR_MSG = 'Error processing the string. Please see Help > Report a Bug.'; export const STRING_DOES_NOT_EXIST_MSG = /^String '.*' not found.$/; diff --git a/test/server_side/analytics.test.js b/test/server_side/analytics.test.js index b88d24c364..7cde7aede8 100644 --- a/test/server_side/analytics.test.js +++ b/test/server_side/analytics.test.js @@ -30,7 +30,7 @@ describe('analytics', function() { }); describe('init', function() { - var analytics = rewire('../../website/src/analytics'); + var analytics = rewire('../../website/src/libs/analytics'); it('throws an error if no options are passed in', function() { expect(analytics).to.throw('No options provided'); @@ -62,7 +62,7 @@ describe('analytics', function() { describe('track', function() { var analyticsData, event_type; - var analytics = rewire('../../website/src/analytics'); + var analytics = rewire('../../website/src/libs/analytics'); var initializedAnalytics; beforeEach(function() { @@ -370,7 +370,7 @@ describe('analytics', function() { var purchaseData; - var analytics = rewire('../../website/src/analytics'); + var analytics = rewire('../../website/src/libs/analytics'); var initializedAnalytics; beforeEach(function() { diff --git a/test/server_side/controllers/groups.test.js b/test/server_side/controllers/groups.test.js index ad9196e13f..9e970c4afc 100644 --- a/test/server_side/controllers/groups.test.js +++ b/test/server_side/controllers/groups.test.js @@ -5,10 +5,10 @@ var expect = chai.expect; var Q = require('q'); var Group = require('../../../website/src/models/group').model; -var groupsController = require('../../../website/src/controllers/groups'); +var groupsController = require('../../../website/src/controllers/api-v2/groups'); describe('Groups Controller', function() { - var utils = require('../../../website/src/utils'); + var utils = require('../../../website/src/libs/utils'); describe('#invite', function() { var res, req, user, group; diff --git a/test/server_side/controllers/user.test.js b/test/server_side/controllers/user.test.js index aac7bf69b7..a000c0cde1 100644 --- a/test/server_side/controllers/user.test.js +++ b/test/server_side/controllers/user.test.js @@ -4,7 +4,7 @@ chai.use(require("sinon-chai")) var expect = chai.expect var rewire = require('rewire'); -var userController = rewire('../../../website/src/controllers/user'); +var userController = rewire('../../../website/src/controllers/api-v2/user'); describe('User Controller', function() { @@ -359,7 +359,7 @@ describe('User Controller', function() { }); it('sends webhooks', function() { - var webhook = require('../../../website/src/webhook'); + var webhook = require('../../../website/src/libs/webhook'); sinon.spy(webhook, 'sendTaskWebhook'); userController.score(req, res); diff --git a/test/server_side/webhooks.test.js b/test/server_side/webhooks.test.js index 596e441620..ef6636bf93 100644 --- a/test/server_side/webhooks.test.js +++ b/test/server_side/webhooks.test.js @@ -4,7 +4,7 @@ chai.use(require("sinon-chai")) var expect = chai.expect var rewire = require('rewire'); -var webhook = rewire('../../website/src/webhook'); +var webhook = rewire('../../website/src/libs/webhook'); describe('webhooks', function() { var postSpy; diff --git a/website/src/controllers/auth.js b/website/src/controllers/api-v2/auth.js similarity index 98% rename from website/src/controllers/auth.js rename to website/src/controllers/api-v2/auth.js index 7fa551689e..7f6ae19bd3 100644 --- a/website/src/controllers/auth.js +++ b/website/src/controllers/api-v2/auth.js @@ -1,16 +1,16 @@ var _ = require('lodash'); var validator = require('validator'); var passport = require('passport'); -var shared = require('../../../common'); +var shared = require('../../../../common'); var async = require('async'); -var utils = require('../utils'); +var utils = require('../../libs/utils'); var nconf = require('nconf'); var request = require('request'); var FirebaseTokenGenerator = require('firebase-token-generator'); -var User = require('../models/user').model; -var EmailUnsubscription = require('../models/emailUnsubscription').model; +var User = require('../../models/user').model; +var EmailUnsubscription = require('../../models/emailUnsubscription').model; var analytics = utils.analytics; -var i18n = require('./../i18n'); +var i18n = require('./../../libs/i18n'); var isProd = nconf.get('NODE_ENV') === 'production'; diff --git a/website/src/controllers/challenges.js b/website/src/controllers/api-v2/challenges.js similarity index 97% rename from website/src/controllers/challenges.js rename to website/src/controllers/api-v2/challenges.js index 6ac40b2571..72f28d9f3d 100644 --- a/website/src/controllers/challenges.js +++ b/website/src/controllers/api-v2/challenges.js @@ -3,15 +3,15 @@ var _ = require('lodash'); var nconf = require('nconf'); var async = require('async'); -var shared = require('../../../common'); -var User = require('./../models/user').model; -var Group = require('./../models/group').model; -var Challenge = require('./../models/challenge').model; -var logging = require('./../logging'); +var shared = require('../../../../common'); +var User = require('./../../models/user').model; +var Group = require('./../../models/group').model; +var Challenge = require('./../../models/challenge').model; +var logging = require('./../../libs/logging'); var csv = require('express-csv'); -var utils = require('../utils'); +var utils = require('../../libs/utils'); var api = module.exports; -var pushNotify = require('./pushNotifications'); +var pushNotify = require('./../pushNotifications'); /* ------------------------------------------------------------------------ diff --git a/website/src/controllers/coupon.js b/website/src/controllers/api-v2/coupon.js similarity index 95% rename from website/src/controllers/coupon.js rename to website/src/controllers/api-v2/coupon.js index b8450d34f3..c6811037e5 100644 --- a/website/src/controllers/coupon.js +++ b/website/src/controllers/api-v2/coupon.js @@ -1,5 +1,5 @@ var _ = require('lodash'); -var Coupon = require('./../models/coupon').model; +var Coupon = require('./../../models/coupon').model; var api = module.exports; var csv = require('express-csv'); var async = require('async'); diff --git a/website/src/controllers/groups.js b/website/src/controllers/api-v2/groups.js similarity index 98% rename from website/src/controllers/groups.js rename to website/src/controllers/api-v2/groups.js index 1e274ad565..88f968d219 100644 --- a/website/src/controllers/groups.js +++ b/website/src/controllers/api-v2/groups.js @@ -9,17 +9,17 @@ var _ = require('lodash'); var nconf = require('nconf'); var async = require('async'); var Q = require('q'); -var utils = require('./../utils'); -var shared = require('../../../common'); -var User = require('./../models/user').model; -var Group = require('./../models/group').model; -var Challenge = require('./../models/challenge').model; -var EmailUnsubscription = require('./../models/emailUnsubscription').model; +var utils = require('./../../libs/utils'); +var shared = require('../../../../common'); +var User = require('./../../models/user').model; +var Group = require('./../../models/group').model; +var Challenge = require('./../../models/challenge').model; +var EmailUnsubscription = require('./../../models/emailUnsubscription').model; var isProd = nconf.get('NODE_ENV') === 'production'; var api = module.exports; -var pushNotify = require('./pushNotifications'); +var pushNotify = require('./../pushNotifications'); var analytics = utils.analytics; -var firebase = require('../libs/firebase'); +var firebase = require('../../libs/firebase'); /* ------------------------------------------------------------------------ diff --git a/website/src/controllers/hall.js b/website/src/controllers/api-v2/hall.js similarity index 95% rename from website/src/controllers/hall.js rename to website/src/controllers/api-v2/hall.js index 8754bc2e94..f3a3ead240 100644 --- a/website/src/controllers/hall.js +++ b/website/src/controllers/api-v2/hall.js @@ -1,9 +1,9 @@ var _ = require('lodash'); var nconf = require('nconf'); var async = require('async'); -var shared = require('../../../common'); -var User = require('./../models/user').model; -var Group = require('./../models/group').model; +var shared = require('../../../../common'); +var User = require('./../../models/user').model; +var Group = require('./../../models/group').model; var api = module.exports; api.ensureAdmin = function(req, res, next) { diff --git a/website/src/controllers/members.js b/website/src/controllers/api-v2/members.js similarity index 95% rename from website/src/controllers/members.js rename to website/src/controllers/api-v2/members.js index c7fe341d3d..c528b92cd7 100644 --- a/website/src/controllers/members.js +++ b/website/src/controllers/api-v2/members.js @@ -1,13 +1,13 @@ var User = require('mongoose').model('User'); -var groups = require('../models/group'); +var groups = require('../../models/group'); var partyFields = require('./groups').partyFields var api = module.exports; var async = require('async'); var _ = require('lodash'); -var shared = require('../../../common'); -var utils = require('../utils'); +var shared = require('../../../../common'); +var utils = require('../../libs/utils'); var nconf = require('nconf'); -var pushNotify = require('./pushNotifications'); +var pushNotify = require('./../pushNotifications'); var fetchMember = function(uuid, restrict){ return function(cb){ diff --git a/website/src/controllers/unsubscription.js b/website/src/controllers/api-v2/unsubscription.js similarity index 84% rename from website/src/controllers/unsubscription.js rename to website/src/controllers/api-v2/unsubscription.js index 8c0edffd5f..f768236ee5 100644 --- a/website/src/controllers/unsubscription.js +++ b/website/src/controllers/api-v2/unsubscription.js @@ -1,7 +1,7 @@ -var User = require('../models/user').model; -var EmailUnsubscription = require('../models/emailUnsubscription').model; -var utils = require('../utils'); -var i18n = require('../../../common').i18n; +var User = require('../../models/user').model; +var EmailUnsubscription = require('../../models/emailUnsubscription').model; +var utils = require('../../libs/utils'); +var i18n = require('../../../../common').i18n; var api = module.exports = {}; diff --git a/website/src/controllers/user.js b/website/src/controllers/api-v2/user.js similarity index 97% rename from website/src/controllers/user.js rename to website/src/controllers/api-v2/user.js index b9559e23c5..caf3c092a4 100644 --- a/website/src/controllers/user.js +++ b/website/src/controllers/api-v2/user.js @@ -5,19 +5,19 @@ var ipn = require('paypal-ipn'); var _ = require('lodash'); var nconf = require('nconf'); var async = require('async'); -var shared = require('../../../common'); -var User = require('./../models/user').model; -var utils = require('./../utils'); +var shared = require('../../../../common'); +var User = require('./../../models/user').model; +var utils = require('./../../libs/utils'); var analytics = utils.analytics; -var Group = require('./../models/group').model; -var Challenge = require('./../models/challenge').model; +var Group = require('./../../models/group').model; +var Challenge = require('./../../models/challenge').model; var moment = require('moment'); -var logging = require('./../logging'); +var logging = require('./../../libs/logging'); var acceptablePUTPaths; var api = module.exports; var qs = require('qs'); -var firebase = require('../libs/firebase'); -var webhook = require('../webhook'); +var firebase = require('../../libs/firebase'); +var webhook = require('../../libs/webhook'); // api.purchase // Shared.ops @@ -290,7 +290,7 @@ api.getUserAnonymized = function(req, res, next) { * The trick here is to only accept leaf paths, not root/intermediate paths (see http://goo.gl/OEzkAs) * FIXME - one-by-one we want to widdle down this list, instead replacing each needed set path with API operations */ -acceptablePUTPaths = _.reduce(require('./../models/user').schema.paths, function(m,v,leaf){ +acceptablePUTPaths = _.reduce(require('./../../models/user').schema.paths, function(m,v,leaf){ var found= _.find('achievements filters flags invitations lastCron party preferences profile stats inbox'.split(' '), function(root){ return leaf.indexOf(root) == 0; }); diff --git a/website/src/controllers/payments/index.js b/website/src/controllers/payments/index.js index 1f8935645d..fd7f98a9c2 100644 --- a/website/src/controllers/payments/index.js +++ b/website/src/controllers/payments/index.js @@ -2,13 +2,13 @@ var _ = require('lodash'); var shared = require('../../../../common'); var nconf = require('nconf'); -var utils = require('./../../utils'); +var utils = require('./../../libs/utils'); var moment = require('moment'); var isProduction = nconf.get("NODE_ENV") === "production"; var stripe = require('./stripe'); var paypal = require('./paypal'); var amazon = require('./amazon'); -var members = require('../members') +var members = require('../api-v2/members') var async = require('async'); var iap = require('./iap'); var mongoose= require('mongoose'); diff --git a/website/src/controllers/payments/paypal.js b/website/src/controllers/payments/paypal.js index 094171e3af..30970f72cf 100644 --- a/website/src/controllers/payments/paypal.js +++ b/website/src/controllers/payments/paypal.js @@ -5,7 +5,7 @@ var _ = require('lodash'); var url = require('url'); var User = require('mongoose').model('User'); var payments = require('./index'); -var logger = require('../../logging'); +var logger = require('../../libs/logging'); var ipn = require('paypal-ipn'); var paypal = require('paypal-rest-sdk'); var shared = require('../../../../common'); diff --git a/website/src/analytics.js b/website/src/libs/analytics.js similarity index 98% rename from website/src/analytics.js rename to website/src/libs/analytics.js index 19a8a9b14c..769a5cf8e1 100644 --- a/website/src/analytics.js +++ b/website/src/libs/analytics.js @@ -2,7 +2,7 @@ require('coffee-script'); require('./i18n'); var _ = require('lodash'); -var Content = require('../../common').content; +var Content = require('../../../common').content; var Amplitude = require('amplitude'); var googleAnalytics = require('universal-analytics'); diff --git a/website/src/i18n.js b/website/src/libs/i18n.js similarity index 94% rename from website/src/i18n.js rename to website/src/libs/i18n.js index 463565ac3e..f2170b453c 100644 --- a/website/src/i18n.js +++ b/website/src/libs/i18n.js @@ -1,11 +1,11 @@ var fs = require('fs'), path = require('path'), _ = require('lodash'), - User = require('./models/user').model, - shared = require('../../common'), + User = require('../models/user').model, + shared = require('../../../common'), translations = {}; -var localePath = path.join(__dirname, "/../../common/locales/") +var localePath = path.join(__dirname, "/../../../common/locales/") var loadTranslations = function(locale){ var files = fs.readdirSync(path.join(localePath, locale)); @@ -54,7 +54,7 @@ _.each(langCodes, function(code){ lang.momentLangCode = (momentLangsMapping[code] || code); try{ // MomentJS lang files are JS files that has to be executed in the browser so we load them as plain text files - var f = fs.readFileSync(path.join(__dirname, '/../../node_modules/moment/locale/' + lang.momentLangCode + '.js'), 'utf8'); + var f = fs.readFileSync(path.join(__dirname, '/../../../node_modules/moment/locale/' + lang.momentLangCode + '.js'), 'utf8'); momentLangs[code] = f; }catch (e){} }); diff --git a/website/src/logging.js b/website/src/libs/logging.js similarity index 100% rename from website/src/logging.js rename to website/src/libs/logging.js diff --git a/website/src/utils.js b/website/src/libs/utils.js similarity index 98% rename from website/src/utils.js rename to website/src/libs/utils.js index d63c54b531..1b1231e102 100644 --- a/website/src/utils.js +++ b/website/src/libs/utils.js @@ -170,7 +170,7 @@ module.exports.setupConfig = function(){ nconf.argv() .env() //.file('defaults', path.join(path.resolve(__dirname, '../config.json.example'))) - .file('user', path.join(path.resolve(__dirname, './../../config.json'))); + .file('user', path.join(path.resolve(__dirname, './../../../config.json'))); if (nconf.get('NODE_ENV') === "development") Error.stackTraceLimit = Infinity; diff --git a/website/src/webhook.js b/website/src/libs/webhook.js similarity index 100% rename from website/src/webhook.js rename to website/src/libs/webhook.js diff --git a/website/src/middlewares/errorHandler.js b/website/src/middlewares/errorHandler.js index eebf25ac6b..9a82316a75 100644 --- a/website/src/middlewares/errorHandler.js +++ b/website/src/middlewares/errorHandler.js @@ -1,4 +1,4 @@ -var logging = require('../logging'); +var logging = require('../libs/logging'); module.exports = function(err, req, res, next) { //res.locals.domain.emit('error', err); diff --git a/website/src/middlewares/locals.js b/website/src/middlewares/locals.js index e9dbb31260..2f238a6a99 100644 --- a/website/src/middlewares/locals.js +++ b/website/src/middlewares/locals.js @@ -1,8 +1,8 @@ var nconf = require('nconf'); var _ = require('lodash'); -var utils = require('../utils'); +var utils = require('../libs/utils'); var shared = require('../../../common'); -var i18n = require('../i18n.js'); +var i18n = require('../libs/i18n'); var buildManifest = require('../libs/buildManifest'); var shared = require('../../../common'); var forceRefresh = require('./forceRefresh'); diff --git a/website/src/models/group.js b/website/src/models/group.js index 9ec1f28a21..17817994c1 100644 --- a/website/src/models/group.js +++ b/website/src/models/group.js @@ -4,7 +4,7 @@ var User = require('./user').model; var shared = require('../../../common'); var _ = require('lodash'); var async = require('async'); -var logging = require('../logging'); +var logging = require('../libs/logging'); var Challenge = require('./../models/challenge').model; var firebase = require('../libs/firebase'); diff --git a/website/src/routes/apiv1.js b/website/src/routes/api-v1.js similarity index 96% rename from website/src/routes/apiv1.js rename to website/src/routes/api-v1.js index f2876fedc4..1002391cae 100644 --- a/website/src/routes/apiv1.js +++ b/website/src/routes/api-v1.js @@ -3,10 +3,10 @@ var router = new express.Router(); var _ = require('lodash'); var async = require('async'); var icalendar = require('icalendar'); -var api = require('./../controllers/user'); -var auth = require('./../controllers/auth'); -var logging = require('./../logging'); -var i18n = require('./../i18n'); +var api = require('./../controllers/api-v2/user'); +var auth = require('./../controllers/api-v2/auth'); +var logging = require('./../libs/logging'); +var i18n = require('./../libs/i18n'); var forceRefresh = require('../middlewares/forceRefresh').middleware; /* ---------- Deprecated API ------------*/ @@ -170,4 +170,4 @@ router.get('*', i18n.getUserLanguage, deprecated); router.post('*', i18n.getUserLanguage, deprecated); router.put('*', i18n.getUserLanguage, deprecated); -module.exports = router; +module.exports = router; \ No newline at end of file diff --git a/website/src/routes/auth.js b/website/src/routes/api-v2/auth.js similarity index 89% rename from website/src/routes/auth.js rename to website/src/routes/api-v2/auth.js index 7d53253210..39f2f6e069 100644 --- a/website/src/routes/auth.js +++ b/website/src/routes/api-v2/auth.js @@ -1,6 +1,6 @@ -var auth = require('../controllers/auth'); +var auth = require('../../controllers/api-v2/auth'); var express = require('express'); -var i18n = require('../i18n'); +var i18n = require('../../libs/i18n'); var router = new express.Router(); /* auth.auth*/ @@ -15,8 +15,7 @@ router.post('/api/v2/user/change-username', i18n.getUserLanguage, auth.auth, aut router.post('/api/v2/user/change-email', i18n.getUserLanguage, auth.auth, auth.changeEmail); router.post('/api/v2/user/auth/firebase', i18n.getUserLanguage, auth.auth, auth.getFirebaseToken); -router.post('/api/v1/register', i18n.getUserLanguage, auth.registerUser); -router.post('/api/v1/user/auth/local', i18n.getUserLanguage, auth.loginLocal); -router.post('/api/v1/user/auth/social', i18n.getUserLanguage, auth.loginSocial); - +router.post('/api/v1/register', i18n.getUserLanguage, auth.registerUser); +router.post('/api/v1/user/auth/local', i18n.getUserLanguage, auth.loginLocal); +router.post('/api/v1/user/auth/social', i18n.getUserLanguage, auth.loginSocial); module.exports = router; \ No newline at end of file diff --git a/website/src/routes/coupon.js b/website/src/routes/api-v2/coupon.js similarity index 75% rename from website/src/routes/coupon.js rename to website/src/routes/api-v2/coupon.js index 57a33866c6..811d81a6f2 100644 --- a/website/src/routes/coupon.js +++ b/website/src/routes/api-v2/coupon.js @@ -1,9 +1,9 @@ var nconf = require('nconf'); var express = require('express'); var router = new express.Router(); -var auth = require('../controllers/auth'); -var coupon = require('../controllers/coupon'); -var i18n = require('../i18n'); +var auth = require('../../controllers/api-v2/auth'); +var coupon = require('../../controllers/api-v2/coupon'); +var i18n = require('../../libs/i18n'); router.get('/api/v2/coupons', auth.authWithUrl, i18n.getUserLanguage, coupon.ensureAdmin, coupon.getCoupons); router.post('/api/v2/coupons/generate/:event', auth.auth, i18n.getUserLanguage, coupon.ensureAdmin, coupon.generateCoupons); diff --git a/website/src/routes/api-v2/swagger.js b/website/src/routes/api-v2/swagger.js new file mode 100644 index 0000000000..d93cfe6271 --- /dev/null +++ b/website/src/routes/api-v2/swagger.js @@ -0,0 +1,777 @@ +/* +---------- /api/v2 API ------------ +see https://github.com/wordnik/swagger-node-express +Every url added to router is prefaced by /api/v2 +Note: Many user-route ops exist in ../../common/script/index.coffee#user.ops, so that they can (1) be called both +client and server. +v1 user. Requires x-api-user (user id) and x-api-key (api key) headers, Test with: +$ mocha test/user.mocha.coffee + */ + +require('coffee-script'); +var user = require("../../controllers/api-v2/user"); +var groups = require("../../controllers/api-v2/groups"); +var members = require("../../controllers/api-v2/members"); +var auth = require("../../controllers/api-v2/auth"); +var hall = require("../../controllers/api-v2/hall"); +var challenges = require("../../controllers/api-v2/challenges"); +var dataexport = require("../../controllers/dataexport"); +var nconf = require("nconf"); +var cron = user.cron; +var _ = require('lodash'); +var content = require('../../../../common').content; +var i18n = require('../../libs/i18n'); +var forceRefresh = require('../../middlewares/forceRefresh').middleware; + +module.exports = function(swagger, v2) { + var path = swagger.pathParam; + var body = swagger.bodyParam; + var query = swagger.queryParam; + + swagger.setAppHandler(v2); + swagger.setErrorHandler("next"); + swagger.setHeaders = function() {}; + swagger.configureSwaggerPaths("", "/api-docs", ""); + + var api = { + '/status': { + spec: { + description: "Returns the status of the server (up or down). Does not require authentication." + }, + action: function(req, res) { + return res.json({ + status: "up" + }); + } + }, + '/content': { + spec: { + description: "Get all available content objects. This is essential, since Habit often depends on item keys (eg, when purchasing a weapon). Does not require authentication.", + parameters: [query("language", "Optional language to use for content's strings. Default is english.", "string")] + }, + action: user.getContent + }, + '/content/paths': { + spec: { + description: "Show user model tree. Does not require authentication." + }, + action: user.getModelPaths + }, + "/export/history": { + spec: { + description: "Export user history", + method: 'GET' + }, + middleware: [auth.auth, i18n.getUserLanguage], + action: dataexport.history + }, + "/user/tasks/{id}/{direction}": { + spec: { + description: "Simple scoring of a task (Habit, Daily, To-Do, or Reward). This is most-likely the only API route you'll be using as a 3rd-party developer. The most common operation is for the user to gain or lose points based on some action (browsing Reddit, running a mile, 1 Pomodor, etc). Call this route, if the task you're trying to score doesn't exist, it will be created for you. When random events occur, the user._tmp variable will be filled. Critical hits can be accessed through user._tmp.crit. The Streakbonus can be accessed through user._tmp.streakBonus. Both will contain the multiplier value. When random drops occur, the following values are available: user._tmp.drop = {text,type,dialog,value,key,notes}", + parameters: [path("id", "ID of the task to score. If this task doesn't exist, a task will be created automatically", "string"), path("direction", "Either 'up' or 'down'", "string"), body('', "If you're creating a 3rd-party task, pass up any task attributes in the body (see TaskSchema).", 'object')], + method: 'POST' + }, + action: user.score + }, + "/user/tasks:GET": { + spec: { + path: '/user/tasks', + description: "Get all user's tasks" + }, + action: user.getTasks + }, + "/user/tasks:POST": { + spec: { + path: '/user/tasks', + description: "Create a task", + method: 'POST', + parameters: [body("", "Send up the whole task (see TaskSchema)", "object")] + }, + action: user.addTask + }, + "/user/tasks/{id}:GET": { + spec: { + path: '/user/tasks/{id}', + description: "Get an individual task", + parameters: [path("id", "Task ID", "string")] + }, + action: user.getTask + }, + "/user/tasks/{id}:PUT": { + spec: { + path: '/user/tasks/{id}', + description: "Update a user's task", + method: 'PUT', + parameters: [path("id", "Task ID", "string"), body("", "Send up the whole task (see TaskSchema)", "object")] + }, + action: user.updateTask + }, + "/user/tasks/{id}:DELETE": { + spec: { + path: '/user/tasks/{id}', + description: "Delete a task", + method: 'DELETE', + parameters: [path("id", "Task ID", "string")] + }, + action: user.deleteTask + }, + "/user/tasks/{id}/sort": { + spec: { + method: 'POST', + description: 'Sort tasks', + parameters: [path("id", "Task ID", "string"), query("from", "Index where you're sorting from (0-based)", "integer"), query("to", "Index where you're sorting to (0-based)", "integer")] + }, + action: user.sortTask + }, + "/user/tasks/clear-completed": { + spec: { + method: 'POST', + description: "Clears competed To-Dos (needed periodically for performance)." + }, + action: user.clearCompleted + }, + "/user/tasks/{id}/unlink": { + spec: { + method: 'POST', + description: 'Unlink a task from its challenge', + parameters: [path("id", "Task ID", "string"), query('keep', "When unlinking a challenge task, how to handle the orphans?", 'string', ['keep', 'keep-all', 'remove', 'remove-all'])] + }, + middleware: [auth.auth, i18n.getUserLanguage], + action: challenges.unlink + }, + "/user/inventory/buy": { + spec: { + description: "Get a list of buyable gear" + }, + action: user.getBuyList + }, + "/user/inventory/buy/{key}": { + spec: { + method: 'POST', + description: "Buy a gear piece and equip it automatically", + parameters: [path('key', "The key of the item to buy (call /content route for available keys)", 'string', _.keys(content.gear.flat))] + }, + action: user.buy + }, + "/user/inventory/sell/{type}/{key}": { + spec: { + method: 'POST', + description: "Sell inventory items back to Alexander", + parameters: [path('type', "The type of object you're selling back.", 'string', ['eggs', 'hatchingPotions', 'food']), path('key', "The object key you're selling back (call /content route for available keys)", 'string')] + }, + action: user.sell + }, + "/user/inventory/purchase/{type}/{key}": { + spec: { + method: 'POST', + description: "Purchase a Gem-purchasable item from Alexander", + parameters: [path('type', "The type of object you're purchasing.", 'string', ['eggs', 'hatchingPotions', 'food', 'quests', 'special']), path('key', "The object key you're purchasing (call /content route for available keys)", 'string')] + }, + action: user.purchase + }, + "/user/inventory/hourglass/{type}/{key}": { + spec: { + method: 'POST', + description: "Purchase a pet or mount using a Mystic Hourglass", + parameters: [path('type', "The type of object you're purchasing.", 'string', ['pets', 'mounts']), path('key', "The object key you're purchasing (call /content route for available keys)", 'string')] + }, + action: user.hourglassPurchase + }, + "/user/inventory/mystery/{key}": { + spec: { + method: 'POST', + description: "Purchase a Mystery Item Set using a Mystic Hourglass", + parameters: [path('key', "The key for the Mystery Set you're purchasing (call /content route for available keys)", 'string')] + }, + action: user.buyMysterySet + }, + "/user/inventory/feed/{pet}/{food}": { + spec: { + method: 'POST', + description: "Feed your pet some food", + parameters: [path('pet', "The key of the pet you're feeding", 'string', _.keys(content.pets)), path('food', "The key of the food to feed your pet", 'string', _.keys(content.food))] + }, + action: user.feed + }, + "/user/inventory/equip/{type}/{key}": { + spec: { + method: 'POST', + description: "Equip an item (either pet, mount, equipped or costume)", + parameters: [path('type', "Type to equip", 'string', ['pet', 'mount', 'equipped', 'costume']), path('key', "The object key you're equipping (call /content route for available keys)", 'string')] + }, + action: user.equip + }, + "/user/inventory/hatch/{egg}/{hatchingPotion}": { + spec: { + method: 'POST', + description: "Pour a hatching potion on an egg", + parameters: [path('egg', "The egg key to hatch", 'string', _.keys(content.eggs)), path('hatchingPotion', "The hatching potion to pour", 'string', _.keys(content.hatchingPotions))] + }, + action: user.hatch + }, + "/user:GET": { + spec: { + path: '/user', + description: "Get the full user object" + }, + action: user.getUser + }, + "/user/anonymized": { + spec: { + description: "Get the user object without any personal data" + }, + action: user.getUserAnonymized + }, + "/user:PUT": { + spec: { + path: '/user', + method: 'PUT', + description: "Update the user object (only certain attributes are supported)", + parameters: [body('', 'The user object (see UserSchema)', 'object')] + }, + action: user.update + }, + "/user:DELETE": { + spec: { + path: '/user', + method: 'DELETE', + description: "Delete a user object entirely, USE WITH CAUTION!" + }, + middleware: [auth.auth, i18n.getUserLanguage], + action: user["delete"] + }, + "/user/revive": { + spec: { + method: 'POST', + description: "Revive your dead user" + }, + action: user.revive + }, + "/user/reroll": { + spec: { + method: 'POST', + description: 'Drink the Fortify Potion (Note, it used to be called re-roll)' + }, + action: user.reroll + }, + "/user/reset": { + spec: { + method: 'POST', + description: "Completely reset your account" + }, + action: user.reset + }, + "/user/sleep": { + spec: { + method: 'POST', + description: "Toggle whether you're resting in the inn" + }, + action: user.sleep + }, + "/user/rebirth": { + spec: { + method: 'POST', + description: "Rebirth your avatar" + }, + action: user.rebirth + }, + "/user/class/change": { + spec: { + method: 'POST', + description: "Either remove your avatar's class, or change it to something new", + parameters: [query('class', "The key of the class to change to. If not provided, user's class is removed.", 'string', ['warrior', 'healer', 'rogue', 'wizard', ''])] + }, + action: user.changeClass + }, + "/user/class/allocate": { + spec: { + method: 'POST', + description: "Allocate one point towards an attribute", + parameters: [query('stat', 'The stat to allocate towards', 'string', ['str', 'per', 'int', 'con'])] + }, + action: user.allocate + }, + "/user/class/cast/{spell}": { + spec: { + method: 'POST', + description: "Casts a spell on a target.", + parameters: [path('spell', "The key of the spell to cast (see ../../common#content/index.coffee)", 'string'), query('targetType', "The type of object you're targeting", 'string', ['party', 'self', 'user', 'task']), query('targetId', "The ID of the object you're targeting", 'string')] + }, + action: user.cast + }, + "/user/unlock": { + spec: { + method: 'POST', + description: "Unlock a certain gem-purchaseable path (or multiple paths)", + parameters: [query('path', "The path to unlock, such as hair.green or shirts.red,shirts.blue", 'string')] + }, + action: user.unlock + }, + "/user/batch-update": { + spec: { + method: 'POST', + description: "This is an advanced route which is useful for apps which might for example need offline support. You can send a whole batch of user-based operations, which allows you to queue them up offline and send them all at once. The format is {op:'nameOfOperation',parameters:{},body:{},query:{}}", + parameters: [body('', 'The array of batch-operations to perform', 'object')] + }, + middleware: [forceRefresh, auth.auth, i18n.getUserLanguage, cron, user.sessionPartyInvite], + action: user.batchUpdate + }, + "/user/tags/{id}:GET": { + spec: { + path: '/user/tags/{id}', + method: 'GET', + description: "Get a tag", + parameters: [path('id', 'The id of the tag to get', 'string')] + }, + action: user.getTag + }, + "/user/tags:POST": { + spec: { + path: "/user/tags", + method: 'POST', + description: 'Create a new tag', + parameters: [body('', 'New tag (see UserSchema.tags)', 'object')] + }, + action: user.addTag + }, + "/user/tags:GET": { + spec: { + path: "/user/tags", + method: 'GET', + description: 'List all of a user\'s tags' + }, + action: user.getTags + }, + "/user/tags/sort": { + spec: { + method: 'POST', + description: 'Sort tags', + parameters: [query("from", "Index where you're sorting from (0-based)", "integer"), query("to", "Index where you're sorting to (0-based)", "integer")] + }, + action: user.sortTag + }, + "/user/tags/{id}:PUT": { + spec: { + path: '/user/tags/{id}', + method: 'PUT', + description: "Edit a tag", + parameters: [path('id', 'The id of the tag to edit', 'string'), body('', 'Tag edits (see UserSchema.tags)', 'object')] + }, + action: user.updateTag + }, + "/user/tags/{id}:DELETE": { + spec: { + path: '/user/tags/{id}', + method: 'DELETE', + description: 'Delete a tag', + parameters: [path('id', 'Id of tag to delete', 'string')] + }, + action: user.deleteTag + }, + "/user/webhooks": { + spec: { + method: 'POST', + description: 'Create a new webhook', + parameters: [body('', 'New Webhook {url:"webhook endpoint (required)", id:"id of webhook (shared.uuid(), optional)", enabled:"whether webhook is enabled (true by default, optional)"}', 'object')] + }, + action: user.addWebhook + }, + "/user/webhooks/{id}:PUT": { + spec: { + path: '/user/webhooks/{id}', + method: 'PUT', + description: "Edit a webhook", + parameters: [path('id', 'The id of the webhook to edit', 'string'), body('', 'New Webhook {url:"webhook endpoint (required)", id:"id of webhook (shared.uuid(), optional)", enabled:"whether webhook is enabled (true by default, optional)"}', 'object')] + }, + action: user.updateWebhook + }, + "/user/webhooks/{id}:DELETE": { + spec: { + path: '/user/webhooks/{id}', + method: 'DELETE', + description: 'Delete a webhook', + parameters: [path('id', 'Id of webhook to delete', 'string')] + }, + action: user.deleteWebhook + }, + "/user/pushDevice": { + spec: { + method: 'POST', + description: 'Add a new push devices registration ID', + parameters: [body('', 'New push registration { regId: "123123", type: "android"}', 'object')] + }, + action: user.addPushDevice + }, + "/groups:GET": { + spec: { + path: '/groups', + description: "Get a list of groups", + parameters: [query('type', "Comma-separated types of groups to return, eg 'party,guilds,public,tavern'", 'string')] + }, + middleware: [auth.auth, i18n.getUserLanguage], + action: groups.list + }, + "/groups:POST": { + spec: { + path: '/groups', + method: 'POST', + description: 'Create a group', + parameters: [body('', 'Group object (see GroupSchema)', 'object')] + }, + middleware: [auth.auth, i18n.getUserLanguage], + action: groups.create + }, + "/groups/{gid}:GET": { + spec: { + path: '/groups/{gid}', + description: "Get a group. The party the user currently is in can be accessed with the gid 'party'.", + parameters: [path('gid', 'Group ID', 'string')] + }, + middleware: [auth.auth, i18n.getUserLanguage], + action: groups.get + }, + "/groups/{gid}:POST": { + spec: { + path: '/groups/{gid}', + method: 'POST', + description: "Edit a group", + parameters: [body('', 'Group object (see GroupSchema)', 'object')] + }, + middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup], + action: groups.update + }, + "/groups/{gid}/join": { + spec: { + method: 'POST', + description: 'Join a group', + parameters: [path('gid', 'Id of the group to join', 'string')] + }, + middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup], + action: groups.join + }, + "/groups/{gid}/leave": { + spec: { + method: 'POST', + description: 'Leave a group', + parameters: [path('gid', 'ID of the group to leave', 'string')] + }, + middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup], + action: groups.leave + }, + "/groups/{gid}/invite": { + spec: { + method: 'POST', + description: "Invite a user to a group", + parameters: [path('gid', 'Group id', 'string'), body('', 'a payload of invites either under body.uuids or body.emails, only one of them!', 'object')] + }, + middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup], + action: groups.invite + }, + "/groups/{gid}/removeMember": { + spec: { + method: 'POST', + description: "Remove / boot a member from a group", + parameters: [path('gid', 'Group id', 'string'), query('uuid', 'User id to boot', 'string')] + }, + middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup], + action: groups.removeMember + }, + "/groups/{gid}/questAccept": { + spec: { + method: 'POST', + description: "Accept a quest invitation", + parameters: [path('gid', "Group id", 'string'), query('key', "optional. if provided, trigger new invite, if not, accept existing invite", 'string')] + }, + middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup], + action: groups.questAccept + }, + "/groups/{gid}/questReject": { + spec: { + method: 'POST', + description: 'Reject quest invitation', + parameters: [path('gid', 'Group id', 'string')] + }, + middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup], + action: groups.questReject + }, + "/groups/{gid}/questCancel": { + spec: { + method: 'POST', + description: 'Cancel quest before it starts (in invitation stage)', + parameters: [path('gid', 'Group to cancel quest in', 'string')] + }, + middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup], + action: groups.questCancel + }, + "/groups/{gid}/questAbort": { + spec: { + method: 'POST', + description: 'Abort quest after it has started (all progress will be lost)', + parameters: [path('gid', 'Group to abort quest in', 'string')] + }, + middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup], + action: groups.questAbort + }, + "/groups/{gid}/questLeave": { + spec: { + method: 'POST', + description: 'Leave an active quest (Quest leaders cannot leave active quests. They must abort the quest to leave)', + parameters: [path('gid', 'Group to leave quest in', 'string')] + }, + middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup], + action: groups.questLeave + }, + "/groups/{gid}/chat:GET": { + spec: { + path: "/groups/{gid}/chat", + description: "Get all chat messages", + parameters: [path('gid', 'Group to return the chat from ', 'string')] + }, + middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup], + action: groups.getChat + }, + "/groups/{gid}/chat:POST": { + spec: { + method: 'POST', + path: "/groups/{gid}/chat", + description: "Send a chat message", + parameters: [query('message', 'Chat message', 'string'), path('gid', 'Group id', 'string')] + }, + middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup], + action: groups.postChat + }, + "/groups/{gid}/chat/seen": { + spec: { + method: 'POST', + description: "Flag chat messages for a particular group as seen", + parameters: [path('gid', 'Group id', 'string')] + }, + action: groups.seenMessage + }, + "/groups/{gid}/chat/{messageId}": { + spec: { + method: 'DELETE', + description: 'Delete a chat message in a given group', + parameters: [path('gid', 'ID of the group containing the message to be deleted', 'string'), path('messageId', 'ID of message to be deleted', 'string')] + }, + middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup], + action: groups.deleteChatMessage + }, + "/groups/{gid}/chat/{mid}/like": { + spec: { + method: 'POST', + description: "Like a chat message", + parameters: [path('gid', 'Group id', 'string'), path('mid', 'Message id', 'string')] + }, + middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup], + action: groups.likeChatMessage + }, + "/groups/{gid}/chat/{mid}/flag": { + spec: { + method: 'POST', + description: "Flag a chat message", + parameters: [path('gid', 'Group id', 'string'), path('mid', 'Message id', 'string')] + }, + middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup], + action: groups.flagChatMessage + }, + "/groups/{gid}/chat/{mid}/clearflags": { + spec: { + method: 'POST', + description: "Clear flag count from message and unhide it", + parameters: [path('gid', 'Group id', 'string'), path('mid', 'Message id', 'string')] + }, + middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup], + action: groups.clearFlagCount + }, + "/members/{uuid}:GET": { + spec: { + path: '/members/{uuid}', + description: "Get a member.", + parameters: [path('uuid', 'Member ID', 'string')] + }, + middleware: [i18n.getUserLanguage], + action: members.getMember + }, + "/members/{uuid}/message": { + spec: { + method: 'POST', + description: 'Send a private message to a member', + parameters: [path('uuid', 'The UUID of the member to message', 'string'), body('', '{"message": "The private message to send"}', 'object')] + }, + middleware: [auth.auth], + action: members.sendPrivateMessage + }, + "/members/{uuid}/block": { + spec: { + method: 'POST', + description: 'Block a member from sending private messages', + parameters: [path('uuid', 'The UUID of the member to message', 'string')] + }, + middleware: [auth.auth], + action: user.blockUser + }, + "/members/{uuid}/gift": { + spec: { + method: 'POST', + description: 'Send a gift to a member', + parameters: [path('uuid', 'The UUID of the member', 'string'), body('', '{"type": "gems or subscription", "gems":{"amount":Number, "fromBalance":Boolean}, "subscription":{"months":Number}}', 'object')] + }, + middleware: [auth.auth], + action: members.sendGift + }, + "/hall/heroes": { + spec: {}, + middleware: [auth.auth, i18n.getUserLanguage], + action: hall.getHeroes + }, + "/hall/heroes/{uid}:GET": { + spec: { + path: "/hall/heroes/{uid}" + }, + middleware: [auth.auth, i18n.getUserLanguage, hall.ensureAdmin], + action: hall.getHero + }, + "/hall/heroes/{uid}:POST": { + spec: { + method: 'POST', + path: "/hall/heroes/{uid}" + }, + middleware: [auth.auth, i18n.getUserLanguage, hall.ensureAdmin], + action: hall.updateHero + }, + "/hall/patrons": { + spec: { + parameters: [query('page', 'Page number to fetch (this list is long)', 'string')] + }, + middleware: [auth.auth, i18n.getUserLanguage], + action: hall.getPatrons + }, + "/challenges:GET": { + spec: { + path: '/challenges', + description: "Get a list of challenges" + }, + middleware: [auth.auth, i18n.getUserLanguage], + action: challenges.list + }, + "/challenges:POST": { + spec: { + path: '/challenges', + method: 'POST', + description: "Create a challenge", + parameters: [body('', 'Challenge object (see ChallengeSchema)', 'object')] + }, + middleware: [auth.auth, i18n.getUserLanguage], + action: challenges.create + }, + "/challenges/{cid}:GET": { + spec: { + path: '/challenges/{cid}', + description: 'Get a challenge', + parameters: [path('cid', 'Challenge id', 'string')] + }, + middleware: [auth.auth, i18n.getUserLanguage], + action: challenges.get + }, + "/challenges/{cid}/csv": { + spec: { + description: 'Get a challenge (csv format)', + parameters: [path('cid', 'Challenge id', 'string')] + }, + action: challenges.csv + }, + "/challenges/{cid}:POST": { + spec: { + path: '/challenges/{cid}', + method: 'POST', + description: "Update a challenge", + parameters: [path('cid', 'Challenge id', 'string'), body('', 'Challenge object (see ChallengeSchema)', 'object')] + }, + middleware: [auth.auth, i18n.getUserLanguage], + action: challenges.update + }, + "/challenges/{cid}:DELETE": { + spec: { + path: '/challenges/{cid}', + method: 'DELETE', + description: "Delete a challenge", + parameters: [path('cid', 'Challenge id', 'string')] + }, + middleware: [auth.auth, i18n.getUserLanguage], + action: challenges["delete"] + }, + "/challenges/{cid}/close": { + spec: { + method: 'POST', + description: 'Close a challenge', + parameters: [path('cid', 'Challenge id', 'string'), query('uid', 'User ID of the winner', 'string', true)] + }, + middleware: [auth.auth, i18n.getUserLanguage], + action: challenges.selectWinner + }, + "/challenges/{cid}/join": { + spec: { + method: 'POST', + description: "Join a challenge", + parameters: [path('cid', 'Challenge id', 'string')] + }, + middleware: [auth.auth, i18n.getUserLanguage], + action: challenges.join + }, + "/challenges/{cid}/leave": { + spec: { + method: 'POST', + description: 'Leave a challenge', + parameters: [path('cid', 'Challenge id', 'string')] + }, + middleware: [auth.auth, i18n.getUserLanguage], + action: challenges.leave + }, + "/challenges/{cid}/member/{uid}": { + spec: { + description: "Get a member's progress in a particular challenge", + parameters: [path('cid', 'Challenge id', 'string'), path('uid', 'User id', 'string')] + }, + middleware: [auth.auth, i18n.getUserLanguage], + action: challenges.getMember + } + }; + if (nconf.get("NODE_ENV") === "development") { + api["/user/addTenGems"] = { + spec: { + method: 'POST' + }, + action: user.addTenGems + }; + api["/user/addHourglass"] = { + spec: { + method: 'POST' + }, + action: user.addHourglass + }; + }; + + _.each(api, function(route, path) { + var base; + if ((base = route.spec).description == null) { + base.description = ''; + } + _.defaults(route.spec, { + path: path, + nickname: path, + notes: route.spec.description, + summary: route.spec.description, + parameters: [], + errorResponses: [], + method: 'GET' + }); + if (route.middleware == null) { + route.middleware = path.indexOf('/user') === 0 ? [auth.auth, i18n.getUserLanguage, cron] : [i18n.getUserLanguage]; + } + swagger["add" + route.spec.method](route); + return true; + }); + + return swagger.configure((nconf.get('BASE_URL')) + "/api/v2", "2"); +}; diff --git a/website/src/routes/unsubscription.js b/website/src/routes/api-v2/unsubscription.js similarity index 60% rename from website/src/routes/unsubscription.js rename to website/src/routes/api-v2/unsubscription.js index efd06f9197..942a396eef 100644 --- a/website/src/routes/unsubscription.js +++ b/website/src/routes/api-v2/unsubscription.js @@ -1,7 +1,7 @@ var express = require('express'); var router = new express.Router(); -var i18n = require('../i18n'); -var unsubscription = require('../controllers/unsubscription'); +var i18n = require('../../libs/i18n'); +var unsubscription = require('../../controllers/api-v2/unsubscription'); router.get('/unsubscribe', i18n.getUserLanguage, unsubscription.unsubscribe); diff --git a/website/src/routes/apiv2.coffee b/website/src/routes/apiv2.coffee deleted file mode 100644 index 746423684f..0000000000 --- a/website/src/routes/apiv2.coffee +++ /dev/null @@ -1,853 +0,0 @@ -### ----------- /api/v2 API ------------ -see https://github.com/wordnik/swagger-node-express -Every url added to router is prefaced by /api/v2 -Note: Many user-route ops exist in ../../common/script/index.coffee#user.ops, so that they can (1) be called both -client and server. -v1 user. Requires x-api-user (user id) and x-api-key (api key) headers, Test with: -$ mocha test/user.mocha.coffee -### - -user = require("../controllers/user") -groups = require("../controllers/groups") -members = require("../controllers/members") -auth = require("../controllers/auth") -hall = require("../controllers/hall") -challenges = require("../controllers/challenges") -dataexport = require("../controllers/dataexport") -nconf = require("nconf") -cron = user.cron -_ = require('lodash') -content = require('../../../common').content -i18n = require('../i18n') -forceRefresh = require('../middlewares/forceRefresh').middleware - -module.exports = (swagger, v2) -> - [path,body,query] = [swagger.pathParam, swagger.bodyParam, swagger.queryParam] - - swagger.setAppHandler(v2) - swagger.setErrorHandler("next") - swagger.setHeaders = -> #disable setHeaders, since we have our own thing going on in middleware.js (and which requires `req`, which swagger doesn't pass in) - swagger.configureSwaggerPaths("", "/api-docs", "") - - api = - - '/status': - spec: - description: "Returns the status of the server (up or down). Does not require authentication." - action: (req, res) -> - res.json status: "up" - - '/content': - spec: - description: "Get all available content objects. This is essential, since Habit often depends on item keys (eg, when purchasing a weapon). Does not require authentication." - parameters: [ - query("language","Optional language to use for content's strings. Default is english.","string") - ] - action: user.getContent - - '/content/paths': - spec: - description: "Show user model tree. Does not require authentication." - action: user.getModelPaths - - "/export/history": - spec: - description: "Export user history" - method: 'GET' - middleware: [auth.auth, i18n.getUserLanguage] - action: dataexport.history #[todo] encode data output options in the data controller and use these to build routes - - # --------------------------------- - # User - # --------------------------------- - - # Scoring - - "/user/tasks/{id}/{direction}": - spec: - #notes: "Simple scoring of a task." - description: "Simple scoring of a task (Habit, Daily, To-Do, or Reward). This is most-likely the only API route you'll be using as a 3rd-party developer. The most common operation is for the user to gain or lose points based on some action (browsing Reddit, running a mile, 1 Pomodor, etc). Call this route, if the task you're trying to score doesn't exist, it will be created for you. When random events occur, the user._tmp variable will be filled. Critical hits can be accessed through user._tmp.crit. The Streakbonus can be accessed through user._tmp.streakBonus. Both will contain the multiplier value. When random drops occur, the following values are available: user._tmp.drop = {text,type,dialog,value,key,notes}" - parameters: [ - path("id", "ID of the task to score. If this task doesn't exist, a task will be created automatically", "string") - path("direction", "Either 'up' or 'down'", "string") - body '',"If you're creating a 3rd-party task, pass up any task attributes in the body (see TaskSchema).",'object' - ] - method: 'POST' - action: user.score - - # Tasks - "/user/tasks:GET": - spec: - path: '/user/tasks' - description: "Get all user's tasks" - action: user.getTasks - - "/user/tasks:POST": - spec: - path: '/user/tasks' - description: "Create a task" - method: 'POST' - parameters: [ body "","Send up the whole task (see TaskSchema)","object" ] - action: user.addTask - - "/user/tasks/{id}:GET": - spec: - path: '/user/tasks/{id}' - description: "Get an individual task" - parameters: [ - path("id", "Task ID", "string") - ] - action: user.getTask - - "/user/tasks/{id}:PUT": - spec: - path: '/user/tasks/{id}' - description: "Update a user's task" - method: 'PUT' - parameters: [ - path "id", "Task ID", "string" - body "","Send up the whole task (see TaskSchema)","object" - ] - action: user.updateTask - - "/user/tasks/{id}:DELETE": - spec: - path: '/user/tasks/{id}' - description: "Delete a task" - method: 'DELETE' - parameters: [ path("id", "Task ID", "string") ] - action: user.deleteTask - - - "/user/tasks/{id}/sort": - spec: - method: 'POST' - description: 'Sort tasks' - parameters: [ - path("id", "Task ID", "string") - query("from","Index where you're sorting from (0-based)","integer") - query("to","Index where you're sorting to (0-based)","integer") - ] - action: user.sortTask - - - "/user/tasks/clear-completed": - spec: - method: 'POST' - description: "Clears competed To-Dos (needed periodically for performance)." - action: user.clearCompleted - - - "/user/tasks/{id}/unlink": - spec: - method: 'POST' - description: 'Unlink a task from its challenge' - parameters: [ - path("id", "Task ID", "string") - query 'keep',"When unlinking a challenge task, how to handle the orphans?",'string',['keep','keep-all','remove','remove-all'] - ] - middleware: [auth.auth, i18n.getUserLanguage] ## removing cron since they may want to remove task first - action: challenges.unlink - - - # Inventory - "/user/inventory/buy": - spec: - description: "Get a list of buyable gear" - action: user.getBuyList - - "/user/inventory/buy/{key}": - spec: - method: 'POST' - description: "Buy a gear piece and equip it automatically" - parameters:[ - path 'key',"The key of the item to buy (call /content route for available keys)",'string', _.keys(content.gear.flat) - ] - action: user.buy - - "/user/inventory/sell/{type}/{key}": - spec: - method: 'POST' - description: "Sell inventory items back to Alexander" - parameters: [ - #TODO verify these are the correct types - path('type',"The type of object you're selling back.",'string',['eggs','hatchingPotions','food']) - path('key',"The object key you're selling back (call /content route for available keys)",'string') - ] - action: user.sell - - "/user/inventory/purchase/{type}/{key}": - spec: - method: 'POST' - description: "Purchase a Gem-purchasable item from Alexander" - parameters:[ - path('type',"The type of object you're purchasing.",'string',['eggs','hatchingPotions','food','quests','special']) - path('key',"The object key you're purchasing (call /content route for available keys)",'string') - ] - action: user.purchase - - "/user/inventory/hourglass/{type}/{key}": - spec: - method: 'POST' - description: "Purchase a pet or mount using a Mystic Hourglass" - parameters:[ - path('type',"The type of object you're purchasing.",'string',['pets','mounts']) - path('key',"The object key you're purchasing (call /content route for available keys)",'string') - ] - action: user.hourglassPurchase - - "/user/inventory/mystery/{key}": - spec: - method: 'POST' - description: "Purchase a Mystery Item Set using a Mystic Hourglass" - parameters:[ - path('key',"The key for the Mystery Set you're purchasing (call /content route for available keys)",'string') - ] - action: user.buyMysterySet - - "/user/inventory/feed/{pet}/{food}": - spec: - method: 'POST' - description: "Feed your pet some food" - parameters: [ - path 'pet',"The key of the pet you're feeding",'string',_.keys(content.pets) - path 'food',"The key of the food to feed your pet",'string',_.keys(content.food) - ] - action: user.feed - - "/user/inventory/equip/{type}/{key}": - spec: - method: 'POST' - description: "Equip an item (either pet, mount, equipped or costume)" - parameters: [ - path 'type',"Type to equip",'string',['pet','mount','equipped', 'costume'] - path 'key',"The object key you're equipping (call /content route for available keys)",'string' - ] - action: user.equip - - "/user/inventory/hatch/{egg}/{hatchingPotion}": - spec: - method: 'POST' - description: "Pour a hatching potion on an egg" - parameters: [ - path 'egg',"The egg key to hatch",'string',_.keys(content.eggs) - path 'hatchingPotion',"The hatching potion to pour",'string',_.keys(content.hatchingPotions) - ] - action: user.hatch - - - # User - "/user:GET": - spec: - path: '/user' - description: "Get the full user object" - action: user.getUser - - "/user/anonymized": - spec: - description: "Get the user object without any personal data" - action: user.getUserAnonymized - - "/user:PUT": - spec: - path: '/user' - method: 'PUT' - description: "Update the user object (only certain attributes are supported)" - parameters: [ - body '','The user object (see UserSchema)','object' - ] - action: user.update - - "/user:DELETE": - spec: - path: '/user' - method: 'DELETE' - description: "Delete a user object entirely, USE WITH CAUTION!" - middleware: [auth.auth, i18n.getUserLanguage] - action: user.delete - - "/user/revive": - spec: - method: 'POST' - description: "Revive your dead user" - action: user.revive - - "/user/reroll": - spec: - method: 'POST' - description: 'Drink the Fortify Potion (Note, it used to be called re-roll)' - action: user.reroll - - "/user/reset": - spec: - method: 'POST' - description: "Completely reset your account" - action: user.reset - - "/user/sleep": - spec: - method: 'POST' - description: "Toggle whether you're resting in the inn" - action: user.sleep - - "/user/rebirth": - spec: - method: 'POST' - description: "Rebirth your avatar" - action: user.rebirth - - "/user/class/change": - spec: - method: 'POST' - description: "Either remove your avatar's class, or change it to something new" - parameters: [ - query 'class',"The key of the class to change to. If not provided, user's class is removed.",'string',['warrior','healer','rogue','wizard',''] - ] - action: user.changeClass - - "/user/class/allocate": - spec: - method: 'POST' - description: "Allocate one point towards an attribute" - parameters: [ - query 'stat','The stat to allocate towards','string',['str','per','int','con'] - ] - action:user.allocate - - "/user/class/cast/{spell}": - spec: - method: 'POST' - description: "Casts a spell on a target." - parameters: [ - path 'spell',"The key of the spell to cast (see ../../common#content/index.coffee)",'string' - query 'targetType',"The type of object you're targeting",'string',['party','self','user','task'] - query 'targetId',"The ID of the object you're targeting",'string' - - ] - action: user.cast - - "/user/unlock": - spec: - method: 'POST' - description: "Unlock a certain gem-purchaseable path (or multiple paths)" - parameters: [ - query 'path',"The path to unlock, such as hair.green or shirts.red,shirts.blue",'string' - ] - action: user.unlock - - "/user/batch-update": - spec: - method: 'POST' - description: "This is an advanced route which is useful for apps which might for example need offline support. You can send a whole batch of user-based operations, which allows you to queue them up offline and send them all at once. The format is {op:'nameOfOperation',parameters:{},body:{},query:{}}" - parameters:[ - body '','The array of batch-operations to perform','object' - ] - middleware: [forceRefresh, auth.auth, i18n.getUserLanguage, cron, user.sessionPartyInvite] - action: user.batchUpdate - - # Tags - "/user/tags/{id}:GET": - spec: - path: '/user/tags/{id}' - method: 'GET' - description: "Get a tag" - parameters: [ - path 'id','The id of the tag to get','string' - ] - action: user.getTag - - "/user/tags:POST": - spec: - path: "/user/tags" - method: 'POST' - description: 'Create a new tag' - parameters: [ - body '','New tag (see UserSchema.tags)','object' - ] - action: user.addTag - - "/user/tags:GET": - spec: - path: "/user/tags" - method: 'GET' - description: 'List all of a user\'s tags' - action: user.getTags - - "/user/tags/sort": - spec: - method: 'POST' - description: 'Sort tags' - parameters: [ - query("from","Index where you're sorting from (0-based)","integer") - query("to","Index where you're sorting to (0-based)","integer") - ] - action: user.sortTag - - "/user/tags/{id}:PUT": - spec: - path: '/user/tags/{id}' - method: 'PUT' - description: "Edit a tag" - parameters: [ - path 'id','The id of the tag to edit','string' - body '','Tag edits (see UserSchema.tags)','object' - ] - action: user.updateTag - - "/user/tags/{id}:DELETE": - spec: - path: '/user/tags/{id}' - method: 'DELETE' - description: 'Delete a tag' - parameters: [ - path 'id','Id of tag to delete','string' - ] - action: user.deleteTag - - # Webhooks - "/user/webhooks": - spec: - method: 'POST' - description: 'Create a new webhook' - parameters: [ - body '','New Webhook {url:"webhook endpoint (required)", id:"id of webhook (shared.uuid(), optional)", enabled:"whether webhook is enabled (true by default, optional)"}','object' - ] - action: user.addWebhook - - "/user/webhooks/{id}:PUT": - spec: - path: '/user/webhooks/{id}' - method: 'PUT' - description: "Edit a webhook" - parameters: [ - path 'id','The id of the webhook to edit','string' - body '','New Webhook {url:"webhook endpoint (required)", id:"id of webhook (shared.uuid(), optional)", enabled:"whether webhook is enabled (true by default, optional)"}','object' - ] - action: user.updateWebhook - - "/user/webhooks/{id}:DELETE": - spec: - path: '/user/webhooks/{id}' - method: 'DELETE' - description: 'Delete a webhook' - parameters: [ - path 'id','Id of webhook to delete','string' - ] - action: user.deleteWebhook - - # Push Notifications - "/user/pushDevice": - spec: - method: 'POST' - description: 'Add a new push devices registration ID' - parameters: [ - body '','New push registration { regId: "123123", type: "android"}','object' - ] - action: user.addPushDevice - - # --------------------------------- - # Groups - # --------------------------------- - "/groups:GET": - spec: - path: '/groups' - description: "Get a list of groups" - parameters: [ - query 'type',"Comma-separated types of groups to return, eg 'party,guilds,public,tavern'",'string' - ] - middleware: [auth.auth, i18n.getUserLanguage] - action: groups.list - - - "/groups:POST": - spec: - path: '/groups' - method: 'POST' - description: 'Create a group' - parameters: [ - body '','Group object (see GroupSchema)','object' - ] - middleware: [auth.auth, i18n.getUserLanguage] - action: groups.create - - "/groups/{gid}:GET": - spec: - path: '/groups/{gid}' - description: "Get a group. The party the user currently is in can be accessed with the gid 'party'." - parameters: [path('gid','Group ID','string')] - middleware: [auth.auth, i18n.getUserLanguage] - action: groups.get - - "/groups/{gid}:POST": - spec: - path: '/groups/{gid}' - method: 'POST' - description: "Edit a group" - parameters: [body('','Group object (see GroupSchema)','object')] - middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] - action: groups.update - - "/groups/{gid}/join": - spec: - method: 'POST' - description: 'Join a group' - parameters: [path('gid','Id of the group to join','string')] - middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] - action: groups.join - - "/groups/{gid}/leave": - spec: - method: 'POST' - description: 'Leave a group' - parameters: [path('gid','ID of the group to leave','string')] - middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] - action: groups.leave - - "/groups/{gid}/invite": - spec: - method: 'POST' - description: "Invite a user to a group" - parameters: [ - path 'gid','Group id','string' - body '','a payload of invites either under body.uuids or body.emails, only one of them!','object' - ] - middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] - action:groups.invite - - "/groups/{gid}/removeMember": - spec: - method: 'POST' - description: "Remove / boot a member from a group" - parameters: [ - path 'gid','Group id','string' - query 'uuid','User id to boot','string' - ] - middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] - action:groups.removeMember - - "/groups/{gid}/questAccept": - spec: - method: 'POST' - description: "Accept a quest invitation" - parameters: [ - path 'gid',"Group id",'string' - query 'key',"optional. if provided, trigger new invite, if not, accept existing invite",'string' - ] - middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] - action:groups.questAccept - - "/groups/{gid}/questReject": - spec: - method: 'POST' - description: 'Reject quest invitation' - parameters: [ - path 'gid','Group id','string' - ] - middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] - action: groups.questReject - - "/groups/{gid}/questCancel": - spec: - method: 'POST' - description: 'Cancel quest before it starts (in invitation stage)' - parameters: [path('gid','Group to cancel quest in','string')] - middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] - action: groups.questCancel - - "/groups/{gid}/questAbort": - spec: - method: 'POST' - description: 'Abort quest after it has started (all progress will be lost)' - parameters: [path('gid','Group to abort quest in','string')] - middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] - action: groups.questAbort - - "/groups/{gid}/questLeave": - spec: - method: 'POST' - description: 'Leave an active quest (Quest leaders cannot leave active quests. They must abort the quest to leave)' - parameters: [path('gid','Group to leave quest in','string')] - middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] - action: groups.questLeave - - #TODO PUT /groups/:gid/chat/:messageId - - "/groups/{gid}/chat:GET": - spec: - path: "/groups/{gid}/chat" - description: "Get all chat messages" - parameters: [path('gid','Group to return the chat from ','string')] - middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] - action: groups.getChat - - - "/groups/{gid}/chat:POST": - spec: - method: 'POST' - path: "/groups/{gid}/chat" - description: "Send a chat message" - parameters: [ - query 'message', 'Chat message','string' - path 'gid','Group id','string' - ] - middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] - action: groups.postChat - - # placing before route below, so that if !=='seen' it goes to next() - "/groups/{gid}/chat/seen": - spec: - method: 'POST' - description: "Flag chat messages for a particular group as seen" - parameters: [ - path 'gid','Group id','string' - ] - action: groups.seenMessage - - "/groups/{gid}/chat/{messageId}": - spec: - method: 'DELETE' - description: 'Delete a chat message in a given group' - parameters: [ - path 'gid', 'ID of the group containing the message to be deleted', 'string' - path 'messageId', 'ID of message to be deleted', 'string' - ] - middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] - action: groups.deleteChatMessage - - "/groups/{gid}/chat/{mid}/like": - spec: - method: 'POST' - description: "Like a chat message" - parameters: [ - path 'gid','Group id','string' - path 'mid','Message id','string' - ] - middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] - action: groups.likeChatMessage - - "/groups/{gid}/chat/{mid}/flag": - spec: - method: 'POST' - description: "Flag a chat message" - parameters: [ - path 'gid','Group id','string' - path 'mid','Message id','string' - ] - middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] - action: groups.flagChatMessage - - "/groups/{gid}/chat/{mid}/clearflags": - spec: - method: 'POST' - description: "Clear flag count from message and unhide it" - parameters: [ - path 'gid','Group id','string' - path 'mid','Message id','string' - ] - middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] - action: groups.clearFlagCount - - # --------------------------------- - # Members - # --------------------------------- - "/members/{uuid}:GET": - spec: - path: '/members/{uuid}' - description: "Get a member." - parameters: [path('uuid','Member ID','string')] - middleware: [i18n.getUserLanguage] # removed auth.auth, so anon users can view shared avatars - action: members.getMember - "/members/{uuid}/message": - spec: - method: 'POST' - description: 'Send a private message to a member' - parameters: [ - path 'uuid', 'The UUID of the member to message', 'string' - body '', '{"message": "The private message to send"}', 'object' - ] - middleware: [auth.auth] - action: members.sendPrivateMessage - "/members/{uuid}/block": - spec: - method: 'POST' - description: 'Block a member from sending private messages' - parameters: [ - path 'uuid', 'The UUID of the member to message', 'string' - ] - middleware: [auth.auth] - action: user.blockUser - "/members/{uuid}/gift": - spec: - method: 'POST' - description: 'Send a gift to a member' - parameters: [ - path 'uuid', 'The UUID of the member', 'string' - body '', '{"type": "gems or subscription", "gems":{"amount":Number, "fromBalance":Boolean}, "subscription":{"months":Number}}', 'object' - ] - middleware: [auth.auth] - action: members.sendGift - - # --------------------------------- - # Hall of Heroes / Patrons - # --------------------------------- - "/hall/heroes": - spec: {} - middleware:[auth.auth, i18n.getUserLanguage] - action: hall.getHeroes - - "/hall/heroes/{uid}:GET": - spec: path: "/hall/heroes/{uid}" - middleware:[auth.auth, i18n.getUserLanguage, hall.ensureAdmin] - action: hall.getHero - - "/hall/heroes/{uid}:POST": - spec: - method: 'POST' - path: "/hall/heroes/{uid}" - middleware: [auth.auth, i18n.getUserLanguage, hall.ensureAdmin] - action: hall.updateHero - - "/hall/patrons": - spec: - parameters: [ - query 'page','Page number to fetch (this list is long)','string' - ] - middleware:[auth.auth, i18n.getUserLanguage] - action: hall.getPatrons - - - # --------------------------------- - # Challenges - # --------------------------------- - - # Note: while challenges belong to groups, and would therefore make sense as a nested resource - # (eg /groups/:gid/challenges/:cid), they will also be referenced by users from the "challenges" tab - # without knowing which group they belong to. So to prevent unecessary lookups, we have them as a top-level resource - "/challenges:GET": - spec: - path: '/challenges' - description: "Get a list of challenges" - middleware: [auth.auth, i18n.getUserLanguage] - action: challenges.list - - - "/challenges:POST": - spec: - path: '/challenges' - method: 'POST' - description: "Create a challenge" - parameters: [body('','Challenge object (see ChallengeSchema)','object')] - middleware: [auth.auth, i18n.getUserLanguage] - action: challenges.create - - "/challenges/{cid}:GET": - spec: - path: '/challenges/{cid}' - description: 'Get a challenge' - parameters: [path('cid','Challenge id','string')] - middleware: [auth.auth, i18n.getUserLanguage] - action: challenges.get - - "/challenges/{cid}/csv": - spec: - description: 'Get a challenge (csv format)' - parameters: [path('cid','Challenge id','string')] - action: challenges.csv - - "/challenges/{cid}:POST": - spec: - path: '/challenges/{cid}' - method: 'POST' - description: "Update a challenge" - parameters: [ - path 'cid','Challenge id','string' - body('','Challenge object (see ChallengeSchema)','object') - ] - middleware: [auth.auth, i18n.getUserLanguage] - action: challenges.update - - "/challenges/{cid}:DELETE": - spec: - path: '/challenges/{cid}' - method: 'DELETE' - description: "Delete a challenge" - parameters: [path('cid','Challenge id','string')] - middleware: [auth.auth, i18n.getUserLanguage] - action: challenges.delete - - "/challenges/{cid}/close": - spec: - method: 'POST' - description: 'Close a challenge' - parameters: [ - path 'cid','Challenge id','string' - query 'uid','User ID of the winner','string',true - ] - middleware: [auth.auth, i18n.getUserLanguage] - action: challenges.selectWinner - - "/challenges/{cid}/join": - spec: - method: 'POST' - description: "Join a challenge" - parameters: [path('cid','Challenge id','string')] - middleware: [auth.auth, i18n.getUserLanguage] - action: challenges.join - - "/challenges/{cid}/leave": - spec: - method: 'POST' - description: 'Leave a challenge' - parameters: [path('cid','Challenge id','string')] - middleware: [auth.auth, i18n.getUserLanguage] - action: challenges.leave - - "/challenges/{cid}/member/{uid}": - spec: - description: "Get a member's progress in a particular challenge" - parameters: [ - path 'cid','Challenge id','string' - path 'uid','User id','string' - ] - middleware: [auth.auth, i18n.getUserLanguage] - action: challenges.getMember - - - if nconf.get("NODE_ENV") is "development" - api["/user/addTenGems"] = - spec: method:'POST' - action: user.addTenGems - - api["/user/addHourglass"] = - spec: method:'POST' - action: user.addHourglass - - _.each api, (route, path) -> - ## Spec format is: - # spec: - # path: "/pet/{petId}" - # description: "Operations about pets" - # notes: "Returns a pet based on ID" - # summary: "Find pet by ID" - # method: "GET" - # parameters: [path("petId", "ID of pet that needs to be fetched", "string")] - # type: "Pet" - # errorResponses: [swagger.errors.invalid("id"), swagger.errors.notFound("pet")] - # nickname: "getPetById" - - route.spec.description ?= '' - _.defaults route.spec, - path: path - nickname: path - notes: route.spec.description - summary: route.spec.description - parameters: [] - #type: 'Pet' - errorResponses: [] - method: 'GET' - route.middleware ?= if path.indexOf('/user') is 0 then [auth.auth, i18n.getUserLanguage, cron] else [i18n.getUserLanguage] - swagger["add#{route.spec.method}"](route);true - - - swagger.configure("#{nconf.get('BASE_URL')}/api/v2", "2") diff --git a/website/src/routes/dataexport.js b/website/src/routes/dataexport.js index 9fc2ded323..5bf02a228c 100644 --- a/website/src/routes/dataexport.js +++ b/website/src/routes/dataexport.js @@ -1,9 +1,9 @@ var express = require('express'); var router = new express.Router(); var dataexport = require('../controllers/dataexport'); -var auth = require('../controllers/auth'); +var auth = require('../controllers/api-v2/auth'); var nconf = require('nconf'); -var i18n = require('../i18n'); +var i18n = require('../libs/i18n'); var locals = require('../middlewares/locals'); /* Data export */ diff --git a/website/src/routes/pages.js b/website/src/routes/pages.js index be2d429e59..b88393301b 100644 --- a/website/src/routes/pages.js +++ b/website/src/routes/pages.js @@ -3,9 +3,7 @@ var express = require('express'); var router = new express.Router(); var _ = require('lodash'); var locals = require('../middlewares/locals'); -var user = require('../controllers/user'); -var auth = require('../controllers/auth'); -var i18n = require('../i18n'); +var i18n = require('../libs/i18n'); // -------- App -------- router.get('/', i18n.getUserLanguage, locals, function(req, res) { diff --git a/website/src/routes/payments.js b/website/src/routes/payments.js index 01667fe747..41c03210be 100644 --- a/website/src/routes/payments.js +++ b/website/src/routes/payments.js @@ -1,9 +1,9 @@ var nconf = require('nconf'); var express = require('express'); var router = new express.Router(); -var auth = require('../controllers/auth'); +var auth = require('../controllers/api-v2/auth'); var payments = require('../controllers/payments'); -var i18n = require('../i18n'); +var i18n = require('../libs/i18n'); router.get('/paypal/checkout', auth.authWithUrl, i18n.getUserLanguage, payments.paypalCheckout); router.get('/paypal/checkout/success', i18n.getUserLanguage, payments.paypalCheckoutSuccess); diff --git a/website/src/seed.js b/website/src/seed.js deleted file mode 100644 index c54c715168..0000000000 --- a/website/src/seed.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * This script is no longer required due to this code in src/models/group.js: - * // initialize tavern if !exists (fresh installs) - * Group.count({_id:'habitrpg'},function(err,ct){ - * ... - * }) - * - * However we're keeping this script in case future seed updates are needed. - * - * Reference: https://github.com/HabitRPG/habitrpg/issues/3852#issuecomment-55334572 - */ - - -/* - -require('coffee-script') // for habitrpg-shared -var nconf = require('nconf'); -var utils = require('./utils'); -var logging = require('./logging'); -utils.setupConfig(); -var async = require('async'); -var mongoose = require('mongoose'); -User = require('./models/user').model; -Group = require('./models/group').model; - -async.waterfall([ - function(cb){ - mongoose.connect(nconf.get('NODE_DB_URI'), cb); - }, - function(cb){ - Group.findById('habitrpg', cb); - }, - function(tavern, cb){ - logging.info({tavern:tavern,cb:cb}); - if (!tavern) { - tavern = new Group({ - _id: 'habitrpg', - chat: [], - leader: '9', - name: 'HabitRPG', - type: 'guild', - privacy:'public' - }); - tavern.save(cb) - } else { - cb(); - } - } -],function(err){ - if (err) throw err; - logging.info("Done initializing database"); - mongoose.disconnect(); -}) - -*/ diff --git a/website/src/server.js b/website/src/server.js index 23ce277eba..94bffe592b 100644 --- a/website/src/server.js +++ b/website/src/server.js @@ -2,9 +2,9 @@ var cluster = require("cluster"); var _ = require('lodash'); var nconf = require('nconf'); -var utils = require('./utils'); +var utils = require('./libs/utils'); utils.setupConfig(); -var logging = require('./logging'); +var logging = require('./libs/logging'); var isProd = nconf.get('NODE_ENV') === 'production'; var isDev = nconf.get('NODE_ENV') === 'development'; var DISABLE_LOGGING = nconf.get('DISABLE_REQUEST_LOGGING'); @@ -29,7 +29,7 @@ if (cores!==0 && cluster.isMaster && (isDev || isProd)) { var shared = require('../../common'); // Setup translations - var i18n = require('./i18n'); + var i18n = require('./libs/i18n'); var TWO_WEEKS = 1000 * 60 * 60 * 24 * 14; var app = express(); @@ -90,54 +90,68 @@ if (cores!==0 && cluster.isMaster && (isDev || isProd)) { var publicDir = path.join(__dirname, "/../public"); app.set("port", nconf.get('PORT')); - require('./middlewares/apiThrottle')(app); - app.use(require('./middlewares/domain')(server,mongoose)); - if (!isProd && !DISABLE_LOGGING) app.use(express.logger("dev")); - app.use(express.compress()); - app.set("views", __dirname + "/../views"); - app.set("view engine", "jade"); - app.use(express.favicon(publicDir + '/favicon.ico')); - app.use(require('./middlewares/cors')); + + // 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 + var oldApp = express(); // api v1 and v2, and not scoped routes + var newApp = express(); // api v3 + + // Route requests to the right app + app.use(app.router); + // 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/v3', newApp); + + require('./middlewares/apiThrottle')(oldApp); + oldApp.use(require('./middlewares/domain')(server,mongoose)); + if (!isProd && !DISABLE_LOGGING) oldApp.use(express.logger("dev")); + oldApp.use(express.compress()); + oldApp.set("views", __dirname + "/../views"); + oldApp.set("view engine", "jade"); + oldApp.use(express.favicon(publicDir + '/favicon.ico')); + oldApp.use(require('./middlewares/cors')); var redirects = require('./middlewares/redirects'); - app.use(redirects.forceHabitica); - app.use(redirects.forceSSL); - app.use(express.urlencoded()); - app.use(express.json()); - app.use(require('method-override')()); - //app.use(express.cookieParser(nconf.get('SESSION_SECRET'))); - app.use(express.cookieParser()); - app.use(express.cookieSession({ secret: nconf.get('SESSION_SECRET'), httpOnly: false, cookie: { maxAge: TWO_WEEKS }})); - //app.use(express.session()); + oldApp.use(redirects.forceHabitica); + oldApp.use(redirects.forceSSL); + oldApp.use(express.urlencoded()); + oldApp.use(express.json()); + oldApp.use(require('method-override')()); + //oldApp.use(express.cookieParser(nconf.get('SESSION_SECRET'))); + oldApp.use(express.cookieParser()); + oldApp.use(express.cookieSession({ secret: nconf.get('SESSION_SECRET'), httpOnly: false, cookie: { maxAge: TWO_WEEKS }})); + //oldApp.use(express.session()); // Initialize Passport! Also use passport.session() middleware, to support // persistent login sessions (recommended). - app.use(passport.initialize()); - app.use(passport.session()); + oldApp.use(passport.initialize()); + oldApp.use(passport.session()); - app.use(app.router); + oldApp.use(oldApp.router); var maxAge = isProd ? 31536000000 : 0; // Cache emojis without copying them to build, they are too many - app.use(express['static'](path.join(__dirname, "/../build"), { maxAge: maxAge })); - app.use('/common/dist', express['static'](publicDir + "/../../common/dist", { maxAge: maxAge })); - app.use('/common/audio', express['static'](publicDir + "/../../common/audio", { maxAge: maxAge })); - app.use('/common/script/public', express['static'](publicDir + "/../../common/script/public", { maxAge: maxAge })); - app.use('/common/img', express['static'](publicDir + "/../../common/img", { maxAge: maxAge })); - app.use(express['static'](publicDir)); + oldApp.use(express['static'](path.join(__dirname, "/../build"), { maxAge: maxAge })); + oldApp.use('/common/dist', express['static'](publicDir + "/../../common/dist", { maxAge: maxAge })); + oldApp.use('/common/audio', express['static'](publicDir + "/../../common/audio", { maxAge: maxAge })); + oldApp.use('/common/script/public', express['static'](publicDir + "/../../common/script/public", { maxAge: maxAge })); + oldApp.use('/common/img', express['static'](publicDir + "/../../common/img", { maxAge: maxAge })); + oldApp.use(express['static'](publicDir)); // Custom Directives - app.use(require('./routes/pages').middleware); - app.use(require('./routes/payments').middleware); - app.use(require('./routes/auth').middleware); - app.use(require('./routes/coupon').middleware); - app.use(require('./routes/unsubscription').middleware); + oldApp.use(require('./routes/pages').middleware); + oldApp.use(require('./routes/payments').middleware); + oldApp.use(require('./routes/api-v2/auth').middleware); + oldApp.use(require('./routes/api-v2/coupon').middleware); + oldApp.use(require('./routes/api-v2/unsubscription').middleware); var v2 = express(); - app.use('/api/v2', v2); - app.use('/api/v1', require('./routes/apiv1').middleware); - app.use('/export', require('./routes/dataexport').middleware); - require('./routes/apiv2.coffee')(swagger, v2); - app.use(require('./middlewares/errorHandler')); + oldApp.use('/api/v2', v2); + oldApp.use('/api/v1', require('./routes/api-v1').middleware); + oldApp.use('/export', require('./routes/dataexport').middleware); + require('./routes/api-v2/swagger')(swagger, v2); + oldApp.use(require('./middlewares/errorHandler')); server.on('request', app); server.listen(app.get("port"), function() {