diff --git a/test/api/v3/integration/dataexport/GET-export_avatar-memberId.html.test.js b/test/api/v3/integration/dataexport/GET-export_avatar-memberId.html.test.js new file mode 100644 index 0000000000..e3bf32c1b6 --- /dev/null +++ b/test/api/v3/integration/dataexport/GET-export_avatar-memberId.html.test.js @@ -0,0 +1,35 @@ +import { + generateUser, + translate as t, +} from '../../../../helpers/api-v3-integration.helper'; +import { v4 as generateUUID } from 'uuid'; + +describe('GET /export/avatar-:memberId.html', () => { + let user; + + before(async () => { + user = await generateUser(); + }); + + it('validates req.params.memberId', async () => { + await expect(user.get(`/export/avatar-:memberId.html`)).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('invalidReqParams'), + }); + }); + + it('handles non-existing members', async () => { + let dummyId = generateUUID(); + await expect(user.get(`/export/avatar-${dummyId}.html`)).to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('userWithIDNotFound', {userId: dummyId}), + }); + }); + + it('returns an html page', async () => { + let res = await user.get(`/export/avatar-${user._id}.html`); + expect(res.substring(0, 100).indexOf('')).to.equal(0); + }); +}); diff --git a/test/api/v3/integration/dataexport/GET-export_avatar-memberId.png.test.js b/test/api/v3/integration/dataexport/GET-export_avatar-memberId.png.test.js new file mode 100644 index 0000000000..a46ed695c6 --- /dev/null +++ b/test/api/v3/integration/dataexport/GET-export_avatar-memberId.png.test.js @@ -0,0 +1,3 @@ +// TODO how to test this route since it points to a file on AWS s3? + +describe('GET /export/avatar-:memberId.png', () => {}); diff --git a/test/api/v3/integration/dataexport/GET-export_history.csv.test.js b/test/api/v3/integration/dataexport/GET-export_history.csv.test.js new file mode 100644 index 0000000000..93b1e09e11 --- /dev/null +++ b/test/api/v3/integration/dataexport/GET-export_history.csv.test.js @@ -0,0 +1,3 @@ +// TODO how to test this route since it uses session authentication? + +describe('GET /export/history.csv', () => {}); diff --git a/test/api/v3/integration/dataexport/GET-export_userdata.json.test.js b/test/api/v3/integration/dataexport/GET-export_userdata.json.test.js new file mode 100644 index 0000000000..8eb6466916 --- /dev/null +++ b/test/api/v3/integration/dataexport/GET-export_userdata.json.test.js @@ -0,0 +1,3 @@ +// TODO how to test this route since it uses session authentication? + +describe('GET /export/userdata.json', () => {}); diff --git a/test/api/v3/integration/dataexport/GET-export_userdata.xml.test.js b/test/api/v3/integration/dataexport/GET-export_userdata.xml.test.js new file mode 100644 index 0000000000..5534c6f625 --- /dev/null +++ b/test/api/v3/integration/dataexport/GET-export_userdata.xml.test.js @@ -0,0 +1,3 @@ +// TODO how to test this route since it uses session authentication? + +describe('GET /export/userdata.xml', () => {}); diff --git a/website/public/js/controllers/footerCtrl.js b/website/public/js/controllers/footerCtrl.js index cd0b9ac182..42fd279944 100644 --- a/website/public/js/controllers/footerCtrl.js +++ b/website/public/js/controllers/footerCtrl.js @@ -7,8 +7,8 @@ function($scope, $rootScope, User, $http, Notification, ApiUrl, Social) { $scope.loadWidgets = Social.loadWidgets; if(env.isStaticPage){ - $scope.languages = env.avalaibleLanguages; - $scope.selectedLanguage = _.find(env.avalaibleLanguages, {code: env.language.code}); + $scope.languages = env.availableLanguages; + $scope.selectedLanguage = _.find(env.availableLanguages, {code: env.language.code}); $rootScope.selectedLanguage = $scope.selectedLanguage; diff --git a/website/public/js/controllers/settingsCtrl.js b/website/public/js/controllers/settingsCtrl.js index 3672a4d703..24fb8a03dc 100644 --- a/website/public/js/controllers/settingsCtrl.js +++ b/website/public/js/controllers/settingsCtrl.js @@ -84,7 +84,7 @@ habitrpg.controller('SettingsCtrl', }; $scope.language = window.env.language; - $scope.avalaibleLanguages = window.env.avalaibleLanguages; + $scope.availableLanguages = window.env.availableLanguages; $scope.changeLanguage = function(){ $rootScope.$on('userSynced', function(){ diff --git a/website/src/controllers/api-v3/dataexport.js b/website/src/controllers/api-v3/dataexport.js index ab835e7c78..dca4e5eae3 100644 --- a/website/src/controllers/api-v3/dataexport.js +++ b/website/src/controllers/api-v3/dataexport.js @@ -14,6 +14,7 @@ import AWS from 'aws-sdk'; import nconf from 'nconf'; import got from 'got'; import Q from 'q'; +import locals from '../../middlewares/api-v3/locals'; let S3 = new AWS.S3({ accessKeyId: nconf.get('S3:accessKeyId'), @@ -25,6 +26,8 @@ const BASE_URL = nconf.get('BASE_URL'); let api = {}; +// TODO move these routes out of the /api/v3/export namespace to the top level /export + /** * @api {get} /export/history.csv Export user tasks history in CSV format. History is only available for habits and dailys so todos and rewards won't be included * @apiVersion 3.0.0 @@ -152,6 +155,7 @@ api.exportUserDataXml = { api.exportUserAvatarHtml = { method: 'GET', url: '/export/avatar-:memberId.html', + middlewares: [locals], async handler (req, res) { req.checkParams('memberId', res.t('memberIdRequired')).notEmpty().isUUID(); diff --git a/website/src/controllers/api-v3/members.js b/website/src/controllers/api-v3/members.js index de2213d3b6..85449ecc79 100644 --- a/website/src/controllers/api-v3/members.js +++ b/website/src/controllers/api-v3/members.js @@ -131,7 +131,7 @@ function _getMembersForItem (type) { * * @apiParam {UUID} groupId The group id * @apiParam {UUID} lastId Query parameter to specify the last member returned in a previous request to this route and get the next batch of results - * @apiParam {boolean} includeAllPublicFields Query parameter avalaible only when fetching a party. If === `true` then all public fields for members will be returned (liek when making a request for a single member) + * @apiParam {boolean} includeAllPublicFields Query parameter available only when fetching a party. If === `true` then all public fields for members will be returned (liek when making a request for a single member) * * @apiSuccess {array} members An array of members, sorted by _id */ diff --git a/website/src/libs/api-v3/i18n.js b/website/src/libs/api-v3/i18n.js index ed2134f7bf..acb9751082 100644 --- a/website/src/libs/api-v3/i18n.js +++ b/website/src/libs/api-v3/i18n.js @@ -49,7 +49,7 @@ shared.i18n.translations = translations; export let langCodes = Object.keys(translations); -export let avalaibleLanguages = langCodes.map((langCode) => { +export let availableLanguages = langCodes.map((langCode) => { return { code: langCode, name: translations[langCode].languageName, @@ -57,7 +57,7 @@ export let avalaibleLanguages = langCodes.map((langCode) => { }); langCodes.forEach((code) => { - let lang = _.find(avalaibleLanguages, {code}); + let lang = _.find(availableLanguages, {code}); lang.momentLangCode = momentLangsMapping[code] || code; @@ -110,7 +110,7 @@ export let multipleVersionsLanguages = { // TODO review if this can be removed since the old mobile app is no longer active // stringName and vars are the allowed parameters export function enTranslations (...args) { - let language = _.find(avalaibleLanguages, {code: 'en'}); + let language = _.find(availableLanguages, {code: 'en'}); // language.momentLang = ((!isStaticPage && i18n.momentLangs[language.code]) || undefined); args.push(language.code); diff --git a/website/src/middlewares/locals.js b/website/src/middlewares/api-v2/locals.js similarity index 100% rename from website/src/middlewares/locals.js rename to website/src/middlewares/api-v2/locals.js diff --git a/website/src/middlewares/api-v3/index.js b/website/src/middlewares/api-v3/index.js index 01da4d4004..0834ed17fa 100644 --- a/website/src/middlewares/api-v3/index.js +++ b/website/src/middlewares/api-v3/index.js @@ -27,6 +27,8 @@ export default function attachMiddlewares (app) { app.use(setupBody); app.use(responseHandler); app.use(getUserLanguage); + app.set('view engine', 'jade'); + app.set('views', `${__dirname}/../../../views`); app.use('/api/v3', routes); app.use(notFoundHandler); diff --git a/website/src/middlewares/api-v3/locals.js b/website/src/middlewares/api-v3/locals.js new file mode 100644 index 0000000000..ccb4fc003b --- /dev/null +++ b/website/src/middlewares/api-v3/locals.js @@ -0,0 +1,73 @@ +import nconf from 'nconf'; +import _ from 'lodash'; +import shared from '../../../../common'; +import * as i18n from '../../libs/api-v3/i18n'; +import { + getBuildUrl, + getManifestFiles, +} from '../../libs/api-v3/buildManifest'; +import forceRefresh from './../forceRefresh'; +import { tavernQuest } from '../../models/group'; +import { mods } from '../../models/user'; +import { decrypt } from '../../libs/api-v3/encryption'; + +// To avoid stringifying more data then we need, +// items from `env` used on the client will have to be specified in this array +// TODO where is this used? +const CLIENT_VARS = ['language', 'isStaticPage', 'availableLanguages', 'translations', + 'FACEBOOK_KEY', 'NODE_ENV', 'BASE_URL', 'GA_ID', + 'AMAZON_PAYMENTS', 'STRIPE_PUB_KEY', 'AMPLITUDE_KEY', + 'worldDmg', 'mods', 'IS_MOBILE']; + +let env = { + getManifestFiles, + getBuildUrl, + _, + clientVars: CLIENT_VARS, + mods, + Content: shared.content, + siteVersion: forceRefresh.siteVersion, + availableLanguages: i18n.available, + AMAZON_PAYMENTS: { + SELLER_ID: nconf.get('AMAZON_PAYMENTS:SELLER_ID'), + CLIENT_ID: nconf.get('AMAZON_PAYMENTS:CLIENT_ID'), + }, +}; + +'NODE_ENV BASE_URL GA_ID STRIPE_PUB_KEY FACEBOOK_KEY AMPLITUDE_KEY'.split(' ').forEach(key => { + env[key] = nconf.get(key); +}); + +export default function locals (req, res, next) { + let language = _.find(i18n.availableLanguages, {code: req.language}); + let isStaticPage = req.url.split('/')[1] === 'static'; // If url contains '/static/' + + // Load moment.js language file only when not on static pages + language.momentLang = !isStaticPage && i18n.momentLangs[language.code] || undefined; + + res.locals.habitrpg = _.assign(env, { + IS_MOBILE: /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(req.header('User-Agent')), + language, + isStaticPage, + translations: i18n.translations[language.code], + t (...args) { // stringName and vars are the allowed parameters + args.push(language.code); + return shared.i18n.t(...args); + }, + // Defined here and not outside of the middleware because tavernQuest might be an + // empty object until the query to fetch it finishes + worldDmg: tavernQuest && tavernQuest.extra && tavernQuest.extra.worldDmg || {}, + }); + + // Put query-string party (& guild but use partyInvite for backward compatibility) + // invitations into session to be handled later + if (req.query.partyInvite) { + try { + req.session.partyInvite = JSON.parse(decrypt(req.query.partyInvite)); + } catch (e) { + // TODO logs + } + } + + next(); +} diff --git a/website/src/models/group.js b/website/src/models/group.js index ab63e813ac..4327a23f4f 100644 --- a/website/src/models/group.js +++ b/website/src/models/group.js @@ -489,7 +489,7 @@ schema.statics.bossQuest = async function bossQuest (user, progress) { export let tavernQuest = {}; let tavernQ = {_id: 'habitrpg', 'quest.key': {$ne: null}}; -// we use process.nextTick because at this point the model is not yet avalaible +// we use process.nextTick because at this point the model is not yet available process.nextTick(() => { model // eslint-disable-line no-use-before-define .findOne(tavernQ).exec() diff --git a/website/src/routes/dataexport.js b/website/src/routes/dataexport.js index b267005d24..3586df9673 100644 --- a/website/src/routes/dataexport.js +++ b/website/src/routes/dataexport.js @@ -6,11 +6,24 @@ var nconf = require('nconf'); var i18n = require('../libs/api-v2/i18n'); var locals = require('../middlewares/locals'); -/* Data export */ -router.get('/history.csv',auth.authWithSession,i18n.getUserLanguage,dataexport.history); //[todo] encode data output options in the data controller and use these to build routes -router.get('/userdata.xml',auth.authWithSession,i18n.getUserLanguage,dataexport.leanuser,dataexport.userdata.xml); -router.get('/userdata.json',auth.authWithSession,i18n.getUserLanguage,dataexport.leanuser,dataexport.userdata.json); -router.get('/avatar-:uuid.html', i18n.getUserLanguage, locals, dataexport.avatarPage); -router.get('/avatar-:uuid.png', i18n.getUserLanguage, locals, dataexport.avatarImage); +const BASE_URL = nconf.get('BASE_URL'); + +/* Data export deprecated routes */ +// TODO remove once api v2 is taken down +router.get('/history.csv', (req, res) => { + res.redirect(`${BASE_URL}/api/v3/export/history.csv`); +}); +router.get('/userdata.xml', (req, res) => { + res.redirect(`${BASE_URL}/api/v3/export/userdata.xml`); +}); +router.get('/userdata.json', (req, res) => { + res.redirect(`${BASE_URL}/api/v3/export/userdata.json`); +}); +router.get('/avatar-:uuid.html', (req, res) => { + res.redirect(`${BASE_URL}/api/v3/export/avatar-${req.params.uuid}.html`); +}); +router.get('/avatar-:uuid.png', (req, res) => { + res.redirect(`${BASE_URL}/api/v3/export/avatar-${req.params.uuid}.png`); +}); module.exports = router; diff --git a/website/src/server.js b/website/src/server.js index d5b9176472..3f4de12bdf 100644 --- a/website/src/server.js +++ b/website/src/server.js @@ -61,7 +61,6 @@ let FacebookStrategy = passportFacebook.Strategy; // 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 @@ -91,7 +90,9 @@ app.all(/^(?!\/api\/v3).+/i, oldApp); // Matches all requests going to /api/v3 app.all('/api/*', newApp); -// Mount middlewares for the new app +// 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 diff --git a/website/views/avatar-static.jade b/website/views/avatar-static.jade index 9a3532f726..58e11ae31a 100644 --- a/website/views/avatar-static.jade +++ b/website/views/avatar-static.jade @@ -9,7 +9,7 @@ html(ng-app="habitrpg") meta(name='apple-mobile-web-app-capable', content='yes') // .slice(0).push('user') is to clone the array, - // to be surethat `user` is never avalaible to other requests' env + // to be surethat `user` is never available to other requests' env // TODO does it need only `user` in clientVars, not the others? - clientVars = env.clientVars.slice(0); diff --git a/website/views/options/settings.jade b/website/views/options/settings.jade index aad9b8b476..5c27572810 100644 --- a/website/views/options/settings.jade +++ b/website/views/options/settings.jade @@ -32,7 +32,7 @@ script(type='text/ng-template', id='partials/options.settings.settings.html') .form-horizontal h5=env.t('language') - select.form-control(ng-model='language.code', ng-options='lang.code as lang.name for lang in avalaibleLanguages', ng-change='changeLanguage()') + select.form-control(ng-model='language.code', ng-options='lang.code as lang.name for lang in availableLanguages', ng-change='changeLanguage()') small !=env.t('americanEnglishGovern') br