mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-19 15:48:04 +01:00
port locals middleware, add some tests and a lot of fixes
This commit is contained in:
@@ -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('<!DOCTYPE html>')).to.equal(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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', () => {});
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// TODO how to test this route since it uses session authentication?
|
||||||
|
|
||||||
|
describe('GET /export/history.csv', () => {});
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// TODO how to test this route since it uses session authentication?
|
||||||
|
|
||||||
|
describe('GET /export/userdata.json', () => {});
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// TODO how to test this route since it uses session authentication?
|
||||||
|
|
||||||
|
describe('GET /export/userdata.xml', () => {});
|
||||||
@@ -7,8 +7,8 @@ function($scope, $rootScope, User, $http, Notification, ApiUrl, Social) {
|
|||||||
$scope.loadWidgets = Social.loadWidgets;
|
$scope.loadWidgets = Social.loadWidgets;
|
||||||
|
|
||||||
if(env.isStaticPage){
|
if(env.isStaticPage){
|
||||||
$scope.languages = env.avalaibleLanguages;
|
$scope.languages = env.availableLanguages;
|
||||||
$scope.selectedLanguage = _.find(env.avalaibleLanguages, {code: env.language.code});
|
$scope.selectedLanguage = _.find(env.availableLanguages, {code: env.language.code});
|
||||||
|
|
||||||
$rootScope.selectedLanguage = $scope.selectedLanguage;
|
$rootScope.selectedLanguage = $scope.selectedLanguage;
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ habitrpg.controller('SettingsCtrl',
|
|||||||
};
|
};
|
||||||
|
|
||||||
$scope.language = window.env.language;
|
$scope.language = window.env.language;
|
||||||
$scope.avalaibleLanguages = window.env.avalaibleLanguages;
|
$scope.availableLanguages = window.env.availableLanguages;
|
||||||
|
|
||||||
$scope.changeLanguage = function(){
|
$scope.changeLanguage = function(){
|
||||||
$rootScope.$on('userSynced', function(){
|
$rootScope.$on('userSynced', function(){
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import AWS from 'aws-sdk';
|
|||||||
import nconf from 'nconf';
|
import nconf from 'nconf';
|
||||||
import got from 'got';
|
import got from 'got';
|
||||||
import Q from 'q';
|
import Q from 'q';
|
||||||
|
import locals from '../../middlewares/api-v3/locals';
|
||||||
|
|
||||||
let S3 = new AWS.S3({
|
let S3 = new AWS.S3({
|
||||||
accessKeyId: nconf.get('S3:accessKeyId'),
|
accessKeyId: nconf.get('S3:accessKeyId'),
|
||||||
@@ -25,6 +26,8 @@ const BASE_URL = nconf.get('BASE_URL');
|
|||||||
|
|
||||||
let api = {};
|
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
|
* @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
|
* @apiVersion 3.0.0
|
||||||
@@ -152,6 +155,7 @@ api.exportUserDataXml = {
|
|||||||
api.exportUserAvatarHtml = {
|
api.exportUserAvatarHtml = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/export/avatar-:memberId.html',
|
url: '/export/avatar-:memberId.html',
|
||||||
|
middlewares: [locals],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('memberId', res.t('memberIdRequired')).notEmpty().isUUID();
|
req.checkParams('memberId', res.t('memberIdRequired')).notEmpty().isUUID();
|
||||||
|
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ function _getMembersForItem (type) {
|
|||||||
*
|
*
|
||||||
* @apiParam {UUID} groupId The group id
|
* @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 {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
|
* @apiSuccess {array} members An array of members, sorted by _id
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ shared.i18n.translations = translations;
|
|||||||
|
|
||||||
export let langCodes = Object.keys(translations);
|
export let langCodes = Object.keys(translations);
|
||||||
|
|
||||||
export let avalaibleLanguages = langCodes.map((langCode) => {
|
export let availableLanguages = langCodes.map((langCode) => {
|
||||||
return {
|
return {
|
||||||
code: langCode,
|
code: langCode,
|
||||||
name: translations[langCode].languageName,
|
name: translations[langCode].languageName,
|
||||||
@@ -57,7 +57,7 @@ export let avalaibleLanguages = langCodes.map((langCode) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
langCodes.forEach((code) => {
|
langCodes.forEach((code) => {
|
||||||
let lang = _.find(avalaibleLanguages, {code});
|
let lang = _.find(availableLanguages, {code});
|
||||||
|
|
||||||
lang.momentLangCode = momentLangsMapping[code] || 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
|
// TODO review if this can be removed since the old mobile app is no longer active
|
||||||
// stringName and vars are the allowed parameters
|
// stringName and vars are the allowed parameters
|
||||||
export function enTranslations (...args) {
|
export function enTranslations (...args) {
|
||||||
let language = _.find(avalaibleLanguages, {code: 'en'});
|
let language = _.find(availableLanguages, {code: 'en'});
|
||||||
|
|
||||||
// language.momentLang = ((!isStaticPage && i18n.momentLangs[language.code]) || undefined);
|
// language.momentLang = ((!isStaticPage && i18n.momentLangs[language.code]) || undefined);
|
||||||
args.push(language.code);
|
args.push(language.code);
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export default function attachMiddlewares (app) {
|
|||||||
app.use(setupBody);
|
app.use(setupBody);
|
||||||
app.use(responseHandler);
|
app.use(responseHandler);
|
||||||
app.use(getUserLanguage);
|
app.use(getUserLanguage);
|
||||||
|
app.set('view engine', 'jade');
|
||||||
|
app.set('views', `${__dirname}/../../../views`);
|
||||||
|
|
||||||
app.use('/api/v3', routes);
|
app.use('/api/v3', routes);
|
||||||
app.use(notFoundHandler);
|
app.use(notFoundHandler);
|
||||||
|
|||||||
73
website/src/middlewares/api-v3/locals.js
Normal file
73
website/src/middlewares/api-v3/locals.js
Normal file
@@ -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();
|
||||||
|
}
|
||||||
@@ -489,7 +489,7 @@ schema.statics.bossQuest = async function bossQuest (user, progress) {
|
|||||||
export let tavernQuest = {};
|
export let tavernQuest = {};
|
||||||
let tavernQ = {_id: 'habitrpg', 'quest.key': {$ne: null}};
|
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(() => {
|
process.nextTick(() => {
|
||||||
model // eslint-disable-line no-use-before-define
|
model // eslint-disable-line no-use-before-define
|
||||||
.findOne(tavernQ).exec()
|
.findOne(tavernQ).exec()
|
||||||
|
|||||||
@@ -6,11 +6,24 @@ var nconf = require('nconf');
|
|||||||
var i18n = require('../libs/api-v2/i18n');
|
var i18n = require('../libs/api-v2/i18n');
|
||||||
var locals = require('../middlewares/locals');
|
var locals = require('../middlewares/locals');
|
||||||
|
|
||||||
/* Data export */
|
const BASE_URL = nconf.get('BASE_URL');
|
||||||
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);
|
/* Data export deprecated routes */
|
||||||
router.get('/userdata.json',auth.authWithSession,i18n.getUserLanguage,dataexport.leanuser,dataexport.userdata.json);
|
// TODO remove once api v2 is taken down
|
||||||
router.get('/avatar-:uuid.html', i18n.getUserLanguage, locals, dataexport.avatarPage);
|
router.get('/history.csv', (req, res) => {
|
||||||
router.get('/avatar-:uuid.png', i18n.getUserLanguage, locals, dataexport.avatarImage);
|
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;
|
module.exports = router;
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ let FacebookStrategy = passportFacebook.Strategy;
|
|||||||
// have a database of user records, the complete Facebook profile is serialized
|
// have a database of user records, the complete Facebook profile is serialized
|
||||||
// and deserialized.
|
// and deserialized.
|
||||||
passport.serializeUser((user, done) => done(null, user));
|
passport.serializeUser((user, done) => done(null, user));
|
||||||
|
|
||||||
passport.deserializeUser((obj, done) => done(null, obj));
|
passport.deserializeUser((obj, done) => done(null, obj));
|
||||||
|
|
||||||
// FIXME
|
// FIXME
|
||||||
@@ -91,7 +90,9 @@ app.all(/^(?!\/api\/v3).+/i, oldApp);
|
|||||||
// Matches all requests going to /api/v3
|
// Matches all requests going to /api/v3
|
||||||
app.all('/api/*', newApp);
|
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);
|
attachMiddlewares(newApp);
|
||||||
|
|
||||||
/* OLD APP IS DISABLED UNTIL COMPATIBLE WITH NEW MODELS
|
/* OLD APP IS DISABLED UNTIL COMPATIBLE WITH NEW MODELS
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ html(ng-app="habitrpg")
|
|||||||
meta(name='apple-mobile-web-app-capable', content='yes')
|
meta(name='apple-mobile-web-app-capable', content='yes')
|
||||||
|
|
||||||
// .slice(0).push('user') is to clone the array,
|
// .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?
|
// TODO does it need only `user` in clientVars, not the others?
|
||||||
-
|
-
|
||||||
clientVars = env.clientVars.slice(0);
|
clientVars = env.clientVars.slice(0);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ script(type='text/ng-template', id='partials/options.settings.settings.html')
|
|||||||
|
|
||||||
.form-horizontal
|
.form-horizontal
|
||||||
h5=env.t('language')
|
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
|
small
|
||||||
!=env.t('americanEnglishGovern')
|
!=env.t('americanEnglishGovern')
|
||||||
br
|
br
|
||||||
|
|||||||
Reference in New Issue
Block a user