mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 06:07:21 +01:00
Real-time Chat (#7664)
* feat(realtime-chat): add Pusher library to the server * feat(realtime-chat): only for private groups * feat(realtime-chat): add authentication endpoint for Pusher * feat(realtime-chat): client proof of concept * fix typo in apidoc * feat(realtime-chat): redo authentication and write integration tests * remove firebase code * fix client side tests * fix line ending in bower.json * feat(realtime chat): use presence channels for parties, send events & disconnect clients if user leaves or is removed from party, automatically update UI * pusher: enable all events in the background * fix pusher integration tests
This commit is contained in:
@@ -41,7 +41,9 @@
|
||||
"sticky": "1.0.3",
|
||||
"swagger-ui": "wordnik/swagger-ui#v2.0.24",
|
||||
"smart-app-banner": "78ef9c0679723b25be1a0ae04f7b4aef7cbced4f",
|
||||
"habitica-markdown": "1.2.2"
|
||||
"habitica-markdown": "1.2.2",
|
||||
"pusher-js-auth": "^2.0.0",
|
||||
"pusher-websocket-iso": "pusher#^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"angular-mocks": "1.3.9"
|
||||
|
||||
@@ -67,9 +67,10 @@
|
||||
"GCM_SERVER_API_KEY": "",
|
||||
"APN_ENABLED": "true"
|
||||
},
|
||||
"FIREBASE": {
|
||||
"APP": "app-name",
|
||||
"SECRET": "secret",
|
||||
"ENABLED": "false"
|
||||
"PUSHER": {
|
||||
"ENABLED": "false",
|
||||
"APP_ID": "appId",
|
||||
"KEY": "key",
|
||||
"SECRET": "secret"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,8 +31,6 @@
|
||||
"express": "~4.13.3",
|
||||
"express-csv": "~0.6.0",
|
||||
"express-validator": "^2.18.0",
|
||||
"firebase": "^2.2.9",
|
||||
"firebase-token-generator": "^2.0.0",
|
||||
"glob": "^4.3.5",
|
||||
"got": "^6.1.1",
|
||||
"grunt": "~0.4.1",
|
||||
@@ -80,6 +78,7 @@
|
||||
"pretty-data": "^0.40.0",
|
||||
"ps-tree": "^1.0.0",
|
||||
"push-notify": "habitrpg/push-notify#v1.2.0",
|
||||
"pusher": "^1.3.0",
|
||||
"request": "~2.72.0",
|
||||
"rimraf": "^2.4.3",
|
||||
"run-sequence": "^1.1.4",
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import moment from 'moment';
|
||||
|
||||
describe('POST /user/auth/firebase', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('returns a Firebase token', async () => {
|
||||
let {token, expires} = await user.post('/user/auth/firebase');
|
||||
expect(moment(expires).isValid()).to.be.true;
|
||||
expect(token).to.be.a('string');
|
||||
});
|
||||
});
|
||||
102
test/api/v3/integration/user/auth/POST-user_auth_pusher.test.js
Normal file
102
test/api/v3/integration/user/auth/POST-user_auth_pusher.test.js
Normal file
@@ -0,0 +1,102 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import {
|
||||
generateUser,
|
||||
requester,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /user/auth/pusher', () => {
|
||||
let user;
|
||||
let endpoint = '/user/auth/pusher';
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('requires authentication', async () => {
|
||||
let api = requester();
|
||||
|
||||
await expect(api.post(endpoint)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('missingAuthHeaders'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if req.body.socket_id is missing', async () => {
|
||||
await expect(user.post(endpoint, {
|
||||
channel_name: '123',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if req.body.channel_name is missing', async () => {
|
||||
await expect(user.post(endpoint, {
|
||||
socket_id: '123',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if req.body.channel_name is badly formatted', async () => {
|
||||
await expect(user.post(endpoint, {
|
||||
channel_name: '123',
|
||||
socket_id: '123',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'Invalid Pusher channel type.',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if an invalid channel type is passed', async () => {
|
||||
await expect(user.post(endpoint, {
|
||||
channel_name: 'invalid-group-123',
|
||||
socket_id: '123',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'Invalid Pusher channel type.',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if an invalid resource type is passed', async () => {
|
||||
await expect(user.post(endpoint, {
|
||||
channel_name: 'presence-user-123',
|
||||
socket_id: '123',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'Invalid Pusher resource type.',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if an invalid resource id is passed', async () => {
|
||||
await expect(user.post(endpoint, {
|
||||
channel_name: 'presence-group-123',
|
||||
socket_id: '123',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'Invalid Pusher resource id, must be a UUID.',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if the passed resource id doesn\'t match the user\'s party', async () => {
|
||||
await expect(user.post(endpoint, {
|
||||
channel_name: `presence-group-${generateUUID()}`,
|
||||
socket_id: '123',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: 'Resource id must be the user\'s party.',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -29,13 +29,14 @@ habitrpg.controller('ChatCtrl', ['$scope', 'Groups', 'Chat', 'User', '$http', 'A
|
||||
$scope.postChat = function(group, message){
|
||||
if (_.isEmpty(message) || $scope._sending) return;
|
||||
$scope._sending = true;
|
||||
var previousMsg = (group.chat && group.chat[0]) ? group.chat[0].id : false;
|
||||
Chat.postChat(group._id, message, previousMsg)
|
||||
// var previousMsg = (group.chat && group.chat[0]) ? group.chat[0].id : false;
|
||||
Chat.postChat(group._id, message) //, previousMsg) not sending the previousMsg as we have real time updates
|
||||
.then(function(response) {
|
||||
var message = response.data.data.message;
|
||||
|
||||
if (message) {
|
||||
group.chat.unshift(message);
|
||||
group.chat.splice(200);
|
||||
} else {
|
||||
group.chat = response.data.data.chat;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
habitrpg.controller("GuildsCtrl", ['$scope', 'Groups', 'User', 'Challenges', '$rootScope', '$state', '$location', '$compile', 'Analytics',
|
||||
function($scope, Groups, User, Challenges, $rootScope, $state, $location, $compile, Analytics) {
|
||||
habitrpg.controller("GuildsCtrl", ['$scope', 'Groups', 'User', 'Challenges', '$rootScope', '$state', '$location', '$compile', 'Analytics', 'Pusher',
|
||||
function($scope, Groups, User, Challenges, $rootScope, $state, $location, $compile, Analytics, Pusher) {
|
||||
$scope.groups = {
|
||||
guilds: [],
|
||||
public: [],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
habitrpg.controller("PartyCtrl", ['$rootScope','$scope','Groups','Chat','User','Challenges','$state','$compile','Analytics','Quests','Social',
|
||||
function($rootScope, $scope, Groups, Chat, User, Challenges, $state, $compile, Analytics, Quests, Social) {
|
||||
habitrpg.controller("PartyCtrl", ['$rootScope','$scope','Groups','Chat','User','Challenges','$state','$compile','Analytics','Quests','Social', 'Pusher',
|
||||
function($rootScope, $scope, Groups, Chat, User, Challenges, $state, $compile, Analytics, Quests, Social, Pusher) {
|
||||
|
||||
var user = User.user;
|
||||
|
||||
|
||||
@@ -7,14 +7,19 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
|
||||
function($scope, $rootScope, $location, User, $http, $state, $stateParams, Notification, Groups, Shared, Content, $modal, $timeout, ApiUrl, Payments, $sce, $window, Analytics, TAVERN_ID) {
|
||||
var user = User.user;
|
||||
|
||||
var initSticky = _.once(function(){
|
||||
$timeout(function () {
|
||||
if (window.env.IS_MOBILE || User.user.preferences.stickyHeader === false) return;
|
||||
$('.header-wrap').sticky({topSpacing:0});
|
||||
});
|
||||
});
|
||||
// Setup page once user is synced
|
||||
var clearAppLoadedListener = $rootScope.$watch('appLoaded', function (after) {
|
||||
if (after === true) {
|
||||
// Initialize sticky header
|
||||
$timeout(function () {
|
||||
if (window.env.IS_MOBILE || User.user.preferences.stickyHeader === false) return;
|
||||
$('.header-wrap').sticky({topSpacing:0});
|
||||
});
|
||||
|
||||
$rootScope.$on('userUpdated',initSticky);
|
||||
// Remove listener
|
||||
clearAppLoadedListener();
|
||||
}
|
||||
});
|
||||
|
||||
$rootScope.$on('$stateChangeSuccess',
|
||||
function(event, toState, toParams, fromState, fromParams){
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('habitrpg')
|
||||
.factory('Chat', ['$http', 'ApiUrl', 'User',
|
||||
function($http, ApiUrl, User) {
|
||||
.factory('Chat', ['$http', 'ApiUrl', 'User', 'Pusher',
|
||||
function($http, ApiUrl, User, Pusher) {
|
||||
var apiV3Prefix = '/api/v3';
|
||||
|
||||
function getChat (groupId) {
|
||||
@@ -24,6 +24,7 @@ angular.module('habitrpg')
|
||||
url: url,
|
||||
data: {
|
||||
message: message,
|
||||
pusherSocketId: Pusher.socketId, // to make sure the send doesn't get notified of it's own message
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
94
website/client/js/services/pusherService.js
Normal file
94
website/client/js/services/pusherService.js
Normal file
@@ -0,0 +1,94 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('habitrpg')
|
||||
.factory('Pusher', ['$rootScope', 'STORAGE_SETTINGS_ID', 'Groups',
|
||||
function($rootScope, STORAGE_SETTINGS_ID, Groups) {
|
||||
var settings = JSON.parse(localStorage.getItem(STORAGE_SETTINGS_ID));
|
||||
var IS_PUSHER_ENABLED = window.env['PUSHER:ENABLED'] === 'true';
|
||||
|
||||
var api = {
|
||||
pusher: undefined,
|
||||
socketId: undefined, // when defined the user is connected
|
||||
};
|
||||
|
||||
// Setup chat channels once app is ready, only for parties for now
|
||||
var clearAppLoadedListener = $rootScope.$watch('appLoaded', function (after) {
|
||||
if (!after) return;
|
||||
clearAppLoadedListener(); // clean the event listerner
|
||||
|
||||
if (!IS_PUSHER_ENABLED) return;
|
||||
|
||||
var user = $rootScope.user;
|
||||
|
||||
// Connect the user to Pusher and to the party's chat channel
|
||||
var partyId = user && $rootScope.user.party && $rootScope.user.party._id;
|
||||
if (!partyId) return;
|
||||
|
||||
api.pusher = new Pusher(window.env['PUSHER:KEY'], {
|
||||
encrypted: true,
|
||||
authEndpoint: '/api/v3/user/auth/pusher',
|
||||
auth: {
|
||||
headers: {
|
||||
'x-api-user': settings && settings.auth && settings.auth.apiId,
|
||||
'x-api-key': settings && settings.auth && settings.auth.apiToken,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
api.pusher.connection.bind('error', function(err) {
|
||||
console.error(err);
|
||||
// TODO if( err.data.code === 4004 ) detected connection limit
|
||||
});
|
||||
|
||||
api.pusher.connection.bind('connected', function () {
|
||||
api.socketId = api.pusher.connection.socket_id;
|
||||
});
|
||||
|
||||
var partyChannelName = 'presence-group-' + partyId;
|
||||
var partyChannel = api.pusher.subscribe(partyChannelName);
|
||||
|
||||
// When an error occurs while joining the channel
|
||||
partyChannel.bind('pusher:subscription_error', function(status) {
|
||||
console.error('Impossible to join the Pusher channel for your party, status: ', status);
|
||||
});
|
||||
|
||||
// When the user correctly enters the party channel
|
||||
partyChannel.bind('pusher:subscription_succeeded', function(members) {
|
||||
// TODO members = [{id, info}]
|
||||
});
|
||||
|
||||
// When a member enters the party channel
|
||||
partyChannel.bind('pusher:member_added', function(member) {
|
||||
// TODO member = {id, info}
|
||||
});
|
||||
|
||||
// When a member leaves the party channel
|
||||
partyChannel.bind('pusher:member_removed', function(member) {
|
||||
// TODO member = {id, info}
|
||||
});
|
||||
|
||||
// When the user is booted from the party, they get disconnected from Pusher
|
||||
partyChannel.bind('user-removed', function (data) {
|
||||
if (data.userId === user._id) {
|
||||
api.pusher.unsubscribe(partyChannelName);
|
||||
}
|
||||
});
|
||||
|
||||
// Same when the user leaves the party
|
||||
partyChannel.bind('user-left', function (data) {
|
||||
if (data.userId === user._id) {
|
||||
api.pusher.unsubscribe(partyChannelName);
|
||||
}
|
||||
});
|
||||
|
||||
// When a new chat message is posted
|
||||
partyChannel.bind('new-chat', function (data) {
|
||||
Groups.party().then(function () {
|
||||
// Groups.data.party.chat.unshift(data);
|
||||
// Groups.data.party.chat.splice(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return api;
|
||||
}]);
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"app": {
|
||||
"js": [
|
||||
"bower_components/pusher-websocket-iso/dist/web/pusher.js",
|
||||
"bower_components/jquery/dist/jquery.min.js",
|
||||
"bower_components/jquery.cookie/jquery.cookie.js",
|
||||
"bower_components/pnotify/jquery.pnotify.min.js",
|
||||
@@ -58,6 +59,7 @@
|
||||
"js/services/userNotificationsService.js",
|
||||
"js/services/userServices.js",
|
||||
"js/services/hallServices.js",
|
||||
"js/services/pusherService.js",
|
||||
|
||||
"js/filters/money.js",
|
||||
"js/filters/roundLargeNumbers.js",
|
||||
|
||||
@@ -6,7 +6,6 @@ var async = require('async');
|
||||
var utils = require('../../libs/api-v2/utils');
|
||||
var nconf = require('nconf');
|
||||
var request = require('request');
|
||||
var FirebaseTokenGenerator = require('firebase-token-generator');
|
||||
import {
|
||||
model as User,
|
||||
} from '../../models/user';
|
||||
@@ -351,28 +350,6 @@ api.changePassword = function(req, res, next) {
|
||||
})
|
||||
};
|
||||
|
||||
// DISABLED FOR API v2
|
||||
/*var firebaseTokenGeneratorInstance = new FirebaseTokenGenerator(nconf.get('FIREBASE:SECRET'));
|
||||
api.getFirebaseToken = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
// Expires 24 hours after now (60*60*24*1000) (in milliseconds)
|
||||
var expires = new Date();
|
||||
expires.setTime(expires.getTime() + 86400000);
|
||||
|
||||
var token = firebaseTokenGeneratorInstance
|
||||
.createToken({
|
||||
uid: user._id,
|
||||
isHabiticaUser: true
|
||||
}, {
|
||||
expires: expires
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
token: token,
|
||||
expires: expires
|
||||
});
|
||||
};*/
|
||||
|
||||
// DISABLED FOR API v2
|
||||
/*api.setupPassport = function(router) {
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ var isProd = nconf.get('NODE_ENV') === 'production';
|
||||
var api = module.exports;
|
||||
var pushNotify = require('./pushNotifications');
|
||||
var analytics = utils.analytics;
|
||||
var firebase = require('../../libs/api-v2/firebase');
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------
|
||||
@@ -233,8 +232,6 @@ api.create = function(req, res, next) {
|
||||
function(cb){user.save(cb)},
|
||||
function(saved,ct,cb){group.save(cb)},
|
||||
function(saved,ct,cb){
|
||||
firebase.updateGroupData(saved);
|
||||
firebase.addUserToGroup(saved._id, user._id);
|
||||
saved.getTransformedData({
|
||||
populateMembers: nameFields,
|
||||
cb,
|
||||
@@ -278,7 +275,6 @@ api.update = function(req, res, next) {
|
||||
group.save(function(err, saved){
|
||||
if (err) return next(err);
|
||||
|
||||
firebase.updateGroupData(saved);
|
||||
res.sendStatus(204);
|
||||
});
|
||||
}
|
||||
@@ -548,7 +544,6 @@ api.join = function(req, res, next) {
|
||||
group.save(cb);
|
||||
},
|
||||
function(cb){
|
||||
firebase.addUserToGroup(group._id, user._id);
|
||||
group.getTransformedData({
|
||||
cb,
|
||||
populateMembers: group.type === 'party' ? partyFields : nameFields,
|
||||
|
||||
@@ -33,7 +33,6 @@ import v3UserController from '../api-v3/user';
|
||||
let i18n = shared.i18n;
|
||||
|
||||
var api = module.exports;
|
||||
var firebase = require('../../libs/api-v2/firebase');
|
||||
var webhook = require('../../libs/api-v2/webhook');
|
||||
|
||||
const partyMembersFields = 'profile.name stats achievements items.special';
|
||||
@@ -468,7 +467,6 @@ api.delete = function(req, res, next) {
|
||||
return user.remove();
|
||||
})
|
||||
.then(() => {
|
||||
firebase.deleteUser(user._id);
|
||||
res.sendStatus(200);
|
||||
})
|
||||
.catch(next);
|
||||
|
||||
@@ -18,8 +18,8 @@ import { model as Group } from '../../models/group';
|
||||
import { model as EmailUnsubscription } from '../../models/emailUnsubscription';
|
||||
import { sendTxn as sendTxnEmail } from '../../libs/api-v3/email';
|
||||
import { decrypt } from '../../libs/api-v3/encryption';
|
||||
import FirebaseTokenGenerator from 'firebase-token-generator';
|
||||
import { send as sendEmail } from '../../libs/api-v3/email';
|
||||
import pusher from '../../libs/api-v3/pusher';
|
||||
|
||||
let api = {};
|
||||
|
||||
@@ -282,6 +282,76 @@ api.loginSocial = {
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
* @apiIgnore Private route
|
||||
* @api {post} /api/v3/user/auth/pusher Pusher.com authentication
|
||||
* @apiDescription Authentication for Pusher.com private and presence channels
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName UserAuthPusher
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiParam {String} socket_id Body parameter
|
||||
* @apiParam {String} channel_name Body parameter
|
||||
*
|
||||
* @apiSuccess {String} auth The authentication token
|
||||
*/
|
||||
api.pusherAuth = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/auth/pusher',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
req.checkBody('socket_id').notEmpty();
|
||||
req.checkBody('channel_name').notEmpty();
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let socketId = req.body.socket_id;
|
||||
let channelName = req.body.channel_name;
|
||||
|
||||
// Channel names are in the form of {presence|private}-{group|...}-{resourceId}
|
||||
let [channelType, resourceType, ...resourceId] = channelName.split('-');
|
||||
|
||||
if (['presence'].indexOf(channelType) === -1) { // presence is used only for parties, private for guilds too
|
||||
throw new BadRequest('Invalid Pusher channel type.');
|
||||
}
|
||||
|
||||
if (resourceType !== 'group') { // only groups are supported
|
||||
throw new BadRequest('Invalid Pusher resource type.');
|
||||
}
|
||||
|
||||
resourceId = resourceId.join('-'); // the split at the beginning had split resourceId too
|
||||
if (!validator.isUUID(resourceId)) {
|
||||
throw new BadRequest('Invalid Pusher resource id, must be a UUID.');
|
||||
}
|
||||
|
||||
// Only the user's party is supported for now
|
||||
if (user.party._id !== resourceId) {
|
||||
throw new NotFound('Resource id must be the user\'s party.');
|
||||
}
|
||||
|
||||
let authResult;
|
||||
|
||||
// Max 100 members for presence channel - parties only
|
||||
if (channelType === 'presence') {
|
||||
let presenceData = {
|
||||
user_id: user._id, // eslint-disable-line camelcase
|
||||
// Max 1KB
|
||||
user_info: {}, // eslint-disable-line camelcase
|
||||
};
|
||||
|
||||
authResult = pusher.authenticate(socketId, channelName, presenceData);
|
||||
} else {
|
||||
authResult = pusher.authenticate(socketId, channelName);
|
||||
}
|
||||
|
||||
// Not using res.respond because Pusher requires a different response format
|
||||
res.status(200).json(authResult);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {put} /api/v3/user/auth/update-username Update username
|
||||
* @apiDescription Update the username of a local user
|
||||
@@ -472,28 +542,6 @@ api.updateEmail = {
|
||||
},
|
||||
};
|
||||
|
||||
const firebaseTokenGenerator = new FirebaseTokenGenerator(nconf.get('FIREBASE:SECRET'));
|
||||
|
||||
// Internal route
|
||||
api.getFirebaseToken = {
|
||||
method: 'POST',
|
||||
url: '/user/auth/firebase',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
// Expires 24 hours from now (60*60*24*1000) (in milliseconds)
|
||||
let expires = new Date();
|
||||
expires.setTime(expires.getTime() + 86400000);
|
||||
|
||||
let token = firebaseTokenGenerator.createToken({
|
||||
uid: user._id,
|
||||
isHabiticaUser: true,
|
||||
}, { expires });
|
||||
|
||||
res.respond(200, {token, expires});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {delete} /api/v3/user/auth/social/:network Delete social authentication method
|
||||
* @apiDescription Remove a social authentication method (only facebook supported) from a user profile. The user must have local authentication enabled
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import _ from 'lodash';
|
||||
import { removeFromArray } from '../../libs/api-v3/collectionManipulators';
|
||||
import { getUserInfo, getGroupUrl, sendTxn } from '../../libs/api-v3/email';
|
||||
import pusher from '../../libs/api-v3/pusher';
|
||||
import nconf from 'nconf';
|
||||
import Bluebird from 'bluebird';
|
||||
|
||||
@@ -53,8 +54,8 @@ api.getChat = {
|
||||
* @apiGroup Chat
|
||||
*
|
||||
* @apiParam {UUID} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
||||
* @apiParam {message} Body parameter - message The message to post
|
||||
* @apiParam {previousMsg} previousMsg Query parameter - The previous chat message which will force a return of the full group chat
|
||||
* @apiParam {string} message Body parameter - message The message to post
|
||||
* @apiParam {UUID} previousMsg Query parameter - The previous chat message which will force a return of the full group chat
|
||||
*
|
||||
* @apiSuccess data An array of chat messages if a new message was posted after previousMsg, otherwise the posted message
|
||||
*/
|
||||
@@ -83,7 +84,7 @@ api.postChat = {
|
||||
let lastClientMsg = req.query.previousMsg;
|
||||
chatUpdated = lastClientMsg && group.chat && group.chat[0] && group.chat[0].id !== lastClientMsg ? true : false;
|
||||
|
||||
group.sendChat(req.body.message, user);
|
||||
let newChatMessage = group.sendChat(req.body.message, user);
|
||||
|
||||
let toSave = [group.save()];
|
||||
|
||||
@@ -93,6 +94,14 @@ api.postChat = {
|
||||
}
|
||||
|
||||
let [savedGroup] = await Bluebird.all(toSave);
|
||||
|
||||
// real-time chat is only enabled for private groups (for now only for parties)
|
||||
if (savedGroup.privacy === 'private' && savedGroup.type === 'party') {
|
||||
// req.body.pusherSocketId is sent from official clients to identify the sender user's real time socket
|
||||
// see https://pusher.com/docs/server_api_guide/server_excluding_recipients
|
||||
pusher.trigger(`presence-group-${savedGroup._id}`, 'new-chat', newChatMessage, req.body.pusherSocketId);
|
||||
}
|
||||
|
||||
if (chatUpdated) {
|
||||
res.respond(200, {chat: Group.toJSONCleanChat(savedGroup, user).chat});
|
||||
} else {
|
||||
@@ -107,8 +116,8 @@ api.postChat = {
|
||||
* @apiName LikeChat
|
||||
* @apiGroup Chat
|
||||
*
|
||||
* @apiParam {groupId} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
||||
* @apiParam {chatId} chatId The chat message _id
|
||||
* @apiParam {UUID} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
||||
* @apiParam {UUID} chatId The chat message _id
|
||||
*
|
||||
* @apiSuccess {Object} data The liked chat message
|
||||
*/
|
||||
@@ -154,8 +163,8 @@ api.likeChat = {
|
||||
* @apiName FlagChat
|
||||
* @apiGroup Chat
|
||||
*
|
||||
* @apiParam {groupId} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
||||
* @apiParam {chatId} chatId The chat message id
|
||||
* @apiParam {UUID} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
||||
* @apiParam {UUID} chatId The chat message id
|
||||
*
|
||||
* @apiSuccess {object} data The flagged chat message
|
||||
*/
|
||||
@@ -243,8 +252,8 @@ api.flagChat = {
|
||||
* @apiName ClearFlags
|
||||
* @apiGroup Chat
|
||||
*
|
||||
* @apiParam {groupId} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
||||
* @apiParam {chatId} chatId The chat message id
|
||||
* @apiParam {UUID} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
||||
* @apiParam {UUID} chatId The chat message id
|
||||
*
|
||||
* @apiSuccess {Object} data An empty object
|
||||
*/
|
||||
@@ -318,7 +327,7 @@ api.clearChatFlags = {
|
||||
* @apiName SeenChat
|
||||
* @apiGroup Chat
|
||||
*
|
||||
* @apiParam {groupId} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
||||
* @apiParam {UUID} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
||||
*
|
||||
* @apiSuccess {Object} data An empty object
|
||||
*/
|
||||
@@ -354,8 +363,8 @@ api.seenChat = {
|
||||
* @apiGroup Chat
|
||||
*
|
||||
* @apiParam {string} previousMsg Query parameter - The last message fetched by the client so that the whole chat will be returned only if new messages have been posted in the meantime
|
||||
* @apiParam {string} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
||||
* @apiParam {string} chatId The chat message id
|
||||
* @apiParam {UUID} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
||||
* @apiParam {UUID} chatId The chat message id
|
||||
*
|
||||
* @apiSuccess data The updated chat array or an empty object if no message was posted after previousMsg
|
||||
* @apiSuccess {Object} data An empty object when the previous message was deleted
|
||||
|
||||
@@ -17,10 +17,11 @@ import {
|
||||
NotAuthorized,
|
||||
} from '../../libs/api-v3/errors';
|
||||
import { removeFromArray } from '../../libs/api-v3/collectionManipulators';
|
||||
import * as firebase from '../../libs/api-v3/firebase';
|
||||
import { sendTxn as sendTxnEmail } from '../../libs/api-v3/email';
|
||||
import { encrypt } from '../../libs/api-v3/encryption';
|
||||
import sendPushNotification from '../../libs/api-v3/pushNotifications';
|
||||
import pusher from '../../libs/api-v3/pusher';
|
||||
|
||||
let api = {};
|
||||
|
||||
/**
|
||||
@@ -66,9 +67,6 @@ api.createGroup = {
|
||||
profile: {name: user.profile.name},
|
||||
};
|
||||
res.respond(201, response); // do not remove chat flags data as we've just created the group
|
||||
|
||||
firebase.updateGroupData(savedGroup);
|
||||
firebase.addUserToGroup(savedGroup._id, user._id);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -179,8 +177,6 @@ api.updateGroup = {
|
||||
};
|
||||
}
|
||||
res.respond(200, response);
|
||||
|
||||
firebase.updateGroupData(savedGroup);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -278,8 +274,6 @@ api.joinGroup = {
|
||||
response.leader = leader.toJSON({minimize: true});
|
||||
}
|
||||
res.respond(200, response);
|
||||
|
||||
firebase.addUserToGroup(group._id, user._id);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -449,7 +443,15 @@ api.removeGroupMember = {
|
||||
if (isInGroup === 'guild') {
|
||||
removeFromArray(member.guilds, group._id);
|
||||
}
|
||||
if (isInGroup === 'party') member.party._id = undefined; // TODO remove quest information too? Use group.leave()?
|
||||
if (isInGroup === 'party') {
|
||||
// Tell the realtime clients that a user is being removed
|
||||
// If the user that is being removed is still connected, they'll get disconnected automatically
|
||||
pusher.trigger(`presence-group-${group._id}`, 'user-removed', {
|
||||
userId: user._id,
|
||||
});
|
||||
|
||||
member.party._id = undefined; // TODO remove quest information too? Use group.leave()?
|
||||
}
|
||||
|
||||
if (member.newMessages[group._id]) {
|
||||
member.newMessages[group._id] = undefined;
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
import { model as User } from '../../models/user';
|
||||
import Bluebird from 'bluebird';
|
||||
import _ from 'lodash';
|
||||
import * as firebase from '../../libs/api-v3/firebase';
|
||||
import * as passwordUtils from '../../libs/api-v3/password';
|
||||
|
||||
let api = {};
|
||||
@@ -230,8 +229,6 @@ api.deleteUser = {
|
||||
await user.remove();
|
||||
|
||||
res.respond(200, {});
|
||||
|
||||
firebase.deleteUser(user._id);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
var Firebase = require('firebase');
|
||||
var nconf = require('nconf');
|
||||
var isProd = nconf.get('NODE_ENV') === 'production';
|
||||
var firebaseConfig = nconf.get('FIREBASE');
|
||||
|
||||
var firebaseRef;
|
||||
var isFirebaseEnabled = (nconf.get('NODE_ENV') === 'production') && (firebaseConfig.ENABLED === 'true');
|
||||
|
||||
import { TAVERN_ID } from '../../models/group';
|
||||
|
||||
// Setup
|
||||
if(isFirebaseEnabled){
|
||||
firebaseRef = new Firebase('https://' + firebaseConfig.APP + '.firebaseio.com');
|
||||
|
||||
// TODO what happens if an op is sent before client is authenticated?
|
||||
firebaseRef.authWithCustomToken(firebaseConfig.SECRET, function(err, authData){
|
||||
// TODO it's ok to kill the server here? what if FB is offline?
|
||||
if(err) throw new Error('Impossible to authenticate Firebase');
|
||||
});
|
||||
}
|
||||
|
||||
var api = module.exports = {};
|
||||
|
||||
api.updateGroupData = function(group){
|
||||
if(!isFirebaseEnabled) return;
|
||||
// TODO is throw ok? we don't have callbacks
|
||||
if(!group) throw new Error('group is required.');
|
||||
// Return in case of tavern (comparison working because we use string for _id)
|
||||
if(group._id === TAVERN_ID) return;
|
||||
|
||||
firebaseRef.child('rooms/' + group._id)
|
||||
.set({
|
||||
name: group.name
|
||||
});
|
||||
};
|
||||
|
||||
api.addUserToGroup = function(groupId, userId){
|
||||
if(!isFirebaseEnabled) return;
|
||||
if(!userId || !groupId) throw new Error('groupId, userId are required.');
|
||||
if(groupId === TAVERN_ID) return;
|
||||
|
||||
firebaseRef.child('members/' + groupId + '/' + userId)
|
||||
.set(true);
|
||||
|
||||
firebaseRef.child('users/' + userId + '/rooms/' + groupId)
|
||||
.set(true);
|
||||
};
|
||||
|
||||
api.removeUserFromGroup = function(groupId, userId){
|
||||
if(!isFirebaseEnabled) return;
|
||||
if(!userId || !groupId) throw new Error('groupId, userId are required.');
|
||||
if(groupId === TAVERN_ID) return;
|
||||
|
||||
firebaseRef.child('members/' + groupId + '/' + userId)
|
||||
.remove();
|
||||
|
||||
firebaseRef.child('users/' + userId + '/rooms/' + groupId)
|
||||
.remove();
|
||||
};
|
||||
|
||||
api.deleteGroup = function(groupId){
|
||||
if(!isFirebaseEnabled) return;
|
||||
if(!groupId) throw new Error('groupId is required.');
|
||||
if(groupId === TAVERN_ID) return;
|
||||
|
||||
firebaseRef.child('rooms/' + groupId)
|
||||
.remove();
|
||||
|
||||
// TODO not really necessary as long as we only store room data,
|
||||
// as empty objects are automatically deleted (/members/... in future...)
|
||||
firebaseRef.child('members/' + groupId)
|
||||
.remove();
|
||||
};
|
||||
|
||||
// TODO not really necessary as long as we only store room data,
|
||||
// as empty objects are automatically deleted
|
||||
api.deleteUser = function(userId){
|
||||
if(!isFirebaseEnabled) return;
|
||||
if(!userId) throw new Error('userId is required.');
|
||||
|
||||
firebaseRef.child('users/' + userId)
|
||||
.remove();
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
import Firebase from 'firebase';
|
||||
import nconf from 'nconf';
|
||||
import { TAVERN_ID } from '../../models/group';
|
||||
|
||||
const FIREBASE_CONFIG = nconf.get('FIREBASE');
|
||||
const FIREBASE_ENABLED = FIREBASE_CONFIG.ENABLED === 'true';
|
||||
|
||||
let firebaseRef;
|
||||
|
||||
if (FIREBASE_ENABLED) {
|
||||
firebaseRef = new Firebase(`https://${FIREBASE_CONFIG.APP}.firebaseio.com`);
|
||||
|
||||
// TODO what happens if an op is sent before client is authenticated?
|
||||
firebaseRef.authWithCustomToken(FIREBASE_CONFIG.SECRET, (err) => {
|
||||
// TODO it's ok to kill the server here? what if FB is offline?
|
||||
if (err) throw new Error('Impossible to authenticate Firebase');
|
||||
});
|
||||
}
|
||||
|
||||
export function updateGroupData (group) {
|
||||
if (!FIREBASE_ENABLED) return;
|
||||
// TODO is throw ok? we don't have callbacks
|
||||
if (!group) throw new Error('group obj is required.');
|
||||
// Return in case of tavern (comparison working because we use string for _id)
|
||||
if (group._id === TAVERN_ID) return;
|
||||
|
||||
firebaseRef.child(`rooms/${group._id}`)
|
||||
.set({
|
||||
name: group.name,
|
||||
});
|
||||
}
|
||||
|
||||
export function addUserToGroup (groupId, userId) {
|
||||
if (!FIREBASE_ENABLED) return;
|
||||
if (!userId || !groupId) throw new Error('groupId, userId are required.');
|
||||
if (groupId === TAVERN_ID) return;
|
||||
|
||||
firebaseRef.child(`members/${groupId}/${userId}`).set(true);
|
||||
firebaseRef.child(`users/${userId}/rooms/${groupId}`).set(true);
|
||||
}
|
||||
|
||||
export function removeUserFromGroup (groupId, userId) {
|
||||
if (!FIREBASE_ENABLED) return;
|
||||
if (!userId || !groupId) throw new Error('groupId, userId are required.');
|
||||
if (groupId === TAVERN_ID) return;
|
||||
|
||||
firebaseRef.child(`members/${groupId}/${userId}`).remove();
|
||||
firebaseRef.child(`users/${userId}/rooms/${groupId}`).remove();
|
||||
}
|
||||
|
||||
export function deleteGroup (groupId) {
|
||||
if (!FIREBASE_ENABLED) return;
|
||||
if (!groupId) throw new Error('groupId is required.');
|
||||
if (groupId === TAVERN_ID) return;
|
||||
|
||||
firebaseRef.child(`members/${groupId}`).remove();
|
||||
// TODO not really necessary as long as we only store room data,
|
||||
// as empty objects are automatically deleted (/members/... in future...)
|
||||
firebaseRef.child(`rooms/${groupId}`).remove();
|
||||
}
|
||||
|
||||
// TODO not really necessary as long as we only store room data,
|
||||
// as empty objects are automatically deleted
|
||||
export function deleteUser (userId) {
|
||||
if (!FIREBASE_ENABLED) return;
|
||||
if (!userId) throw new Error('userId is required.');
|
||||
|
||||
firebaseRef.child(`users/${userId}`).remove();
|
||||
}
|
||||
42
website/server/libs/api-v3/pusher.js
Normal file
42
website/server/libs/api-v3/pusher.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import Pusher from 'pusher';
|
||||
import nconf from 'nconf';
|
||||
import { InternalServerError } from './errors';
|
||||
|
||||
const IS_PUSHER_ENABLED = nconf.get('PUSHER:ENABLED') === 'true';
|
||||
|
||||
let pusherInstance;
|
||||
|
||||
if (IS_PUSHER_ENABLED) {
|
||||
pusherInstance = new Pusher({
|
||||
appId: nconf.get('PUSHER:APP_ID'),
|
||||
key: nconf.get('PUSHER:KEY'),
|
||||
secret: nconf.get('PUSHER:SECRET'),
|
||||
encrypted: true,
|
||||
});
|
||||
}
|
||||
|
||||
let api = {
|
||||
// https://github.com/pusher/pusher-http-node#publishing-events
|
||||
trigger (channel, event, data, socketId = null) {
|
||||
if (!IS_PUSHER_ENABLED) return Promise.resolve(null);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
pusherInstance.trigger(channel, event, data, socketId, (err, req, res) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve([req, res]);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// https://github.com/pusher/pusher-http-node#authenticating-private-channels
|
||||
authenticate (...args) {
|
||||
if (!IS_PUSHER_ENABLED) throw new InternalServerError('Pusher is not enabled.');
|
||||
|
||||
return pusherInstance.authenticate(...args);
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = api;
|
||||
@@ -15,7 +15,7 @@ import { mods } from '../../models/user';
|
||||
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'];
|
||||
'worldDmg', 'mods', 'IS_MOBILE', 'PUSHER:KEY', 'PUSHER:ENABLED'];
|
||||
|
||||
let env = {
|
||||
getManifestFiles,
|
||||
@@ -32,9 +32,11 @@ let env = {
|
||||
},
|
||||
};
|
||||
|
||||
'NODE_ENV BASE_URL GA_ID STRIPE_PUB_KEY FACEBOOK_KEY AMPLITUDE_KEY'.split(' ').forEach(key => {
|
||||
env[key] = nconf.get(key);
|
||||
});
|
||||
'NODE_ENV BASE_URL GA_ID STRIPE_PUB_KEY FACEBOOK_KEY AMPLITUDE_KEY PUSHER:KEY PUSHER:ENABLED'
|
||||
.split(' ')
|
||||
.forEach(key => {
|
||||
env[key] = nconf.get(key);
|
||||
});
|
||||
|
||||
module.exports = function locals (req, res, next) {
|
||||
let language = _.find(i18n.availableLanguages, {code: req.language});
|
||||
|
||||
@@ -12,12 +12,12 @@ import {
|
||||
InternalServerError,
|
||||
BadRequest,
|
||||
} from '../libs/api-v3/errors';
|
||||
import * as firebase from '../libs/api-v2/firebase';
|
||||
import baseModel from '../libs/api-v3/baseModel';
|
||||
import { sendTxn as sendTxnEmail } from '../libs/api-v3/email';
|
||||
import Bluebird from 'bluebird';
|
||||
import nconf from 'nconf';
|
||||
import sendPushNotification from '../libs/api-v3/pushNotifications';
|
||||
import pusher from '../libs/api-v3/pusher';
|
||||
|
||||
const questScrolls = shared.content.quests;
|
||||
const Schema = mongoose.Schema;
|
||||
@@ -31,8 +31,6 @@ const LARGE_GROUP_COUNT_MESSAGE_CUTOFF = shared.constants.LARGE_GROUP_COUNT_MESS
|
||||
const CRON_SAFE_MODE = nconf.get('CRON_SAFE_MODE') === 'true';
|
||||
const CRON_SEMI_SAFE_MODE = nconf.get('CRON_SEMI_SAFE_MODE') === 'true';
|
||||
|
||||
// NOTE once Firebase is enabled any change to groups' members in MongoDB will have to be run through the API
|
||||
// changes made directly to the db will cause Firebase to get out of sync
|
||||
export let schema = new Schema({
|
||||
name: {type: String, required: true},
|
||||
description: String,
|
||||
@@ -109,10 +107,6 @@ schema.pre('remove', true, async function preRemoveGroup (next, done) {
|
||||
}
|
||||
});
|
||||
|
||||
schema.post('remove', function postRemoveGroup (group) {
|
||||
firebase.deleteGroup(group._id);
|
||||
});
|
||||
|
||||
// return a clean object for user.quest
|
||||
function _cleanQuestProgress (merge) {
|
||||
let clean = {
|
||||
@@ -239,12 +233,14 @@ schema.statics.getGroups = async function getGroups (options = {}) {
|
||||
// Not putting into toJSON because there we can't access user
|
||||
schema.statics.toJSONCleanChat = function groupToJSONCleanChat (group, user) {
|
||||
let toJSON = group.toJSON();
|
||||
|
||||
if (!user.contributor.admin) {
|
||||
_.remove(toJSON.chat, chatMsg => {
|
||||
chatMsg.flags = {};
|
||||
return chatMsg.flagCount >= 2;
|
||||
});
|
||||
}
|
||||
|
||||
return toJSON;
|
||||
};
|
||||
|
||||
@@ -308,7 +304,9 @@ export function chatDefaults (msg, user) {
|
||||
}
|
||||
|
||||
schema.methods.sendChat = function sendChat (message, user) {
|
||||
this.chat.unshift(chatDefaults(message, user));
|
||||
let newMessage = chatDefaults(message, user);
|
||||
|
||||
this.chat.unshift(newMessage);
|
||||
this.chat.splice(200);
|
||||
|
||||
// do not send notifications for guilds with more than 5000 users and for the tavern
|
||||
@@ -331,6 +329,8 @@ schema.methods.sendChat = function sendChat (message, user) {
|
||||
query._id = { $ne: user ? user._id : ''};
|
||||
|
||||
User.update(query, lastSeenUpdate, {multi: true}).exec();
|
||||
|
||||
return newMessage;
|
||||
};
|
||||
|
||||
schema.methods.startQuest = async function startQuest (user) {
|
||||
@@ -719,6 +719,11 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all') {
|
||||
promises.push(User.update({_id: user._id}, {$pull: {guilds: group._id}}).exec());
|
||||
} else {
|
||||
promises.push(User.update({_id: user._id}, {$set: {party: {}}}).exec());
|
||||
// Tell the realtime clients that a user has left
|
||||
// If the user that left is still connected, they'll get disconnected
|
||||
pusher.trigger(`presence-group-${group._id}`, 'user-left', {
|
||||
userId: user._id,
|
||||
});
|
||||
}
|
||||
|
||||
// If user is the last one in group and group is private, delete it
|
||||
@@ -740,8 +745,6 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all') {
|
||||
promises.push(group.update(update).exec());
|
||||
}
|
||||
|
||||
firebase.removeUserFromGroup(group._id, user._id);
|
||||
|
||||
return await Bluebird.all(promises);
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,5 @@ router.post('/user/reset-password', getUserLanguage, auth.resetPassword);
|
||||
router.post('/user/change-password', getUserLanguage, auth.auth, auth.changePassword);
|
||||
router.post('/user/change-username', getUserLanguage, auth.auth, auth.changeUsername);
|
||||
router.post('/user/change-email', getUserLanguage, auth.auth, auth.changeEmail);
|
||||
// router.post('/user/auth/firebase', i18n.getUserLanguage, auth.auth, auth.getFirebaseToken);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -17,7 +17,6 @@ import './libs/api-v3/i18n';
|
||||
|
||||
// Load config files
|
||||
import './libs/api-v3/setupMongoose';
|
||||
import './libs/api-v3/firebase';
|
||||
import './libs/api-v3/setupPassport';
|
||||
|
||||
// Load some schemas & models
|
||||
|
||||
Reference in New Issue
Block a user