mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-18 15:17:25 +01:00
Merge branch 'develop' of github.com:HabitRPG/habitrpg into common-convert
Conflicts: src/controllers/auth.js src/controllers/challenges.js src/controllers/groups.js src/controllers/members.js src/controllers/payments/index.js src/controllers/user.js src/middleware.js src/models/user.js
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
var migrationName = '20150131_birthday_goodies_fix_remove_robe.js';
|
||||
var migrationName = '20150131_birthday_goodies_fix__one_birthday__1';
|
||||
var authorName = 'Alys'; // in case script author needs to know when their ...
|
||||
var authorUuid = 'd904bd62-da08-416b-a816-ba797c9ee265'; //... own data is done
|
||||
|
||||
/**
|
||||
* remove new birthday robes from people who don't have original birthday achievement
|
||||
/*
|
||||
* remove new birthday robes and second achievement from people who shouldn't have them
|
||||
*/
|
||||
|
||||
var dbserver = 'localhost:27017' // CHANGE THIS FOR PRODUCTION DATABASE
|
||||
@@ -13,12 +13,22 @@ var _ = require('lodash');
|
||||
|
||||
var dbUsers = mongo.db(dbserver + '/habitrpg?auto_reconnect').collection('users');
|
||||
|
||||
// 'auth.timestamps.created':{$gt:new Date('2014-02-01')},
|
||||
var query = {
|
||||
'achievements.habitBirthday':{$exists:false}
|
||||
};
|
||||
'achievements.habitBirthdays':1,
|
||||
'auth.timestamps.loggedin':{$gt:new Date('2014-12-20')}
|
||||
};
|
||||
|
||||
// '_id': 'c03e41bd-501f-438c-9553-a7afdf52a08c',
|
||||
// 'achievements.habitBirthday':{$exists:false},
|
||||
// 'items.gear.owned.armor_special_birthday2015':1
|
||||
|
||||
var fields = {
|
||||
'items.gear.owned.armor_special_birthday2015':1
|
||||
// 'auth.timestamps.created':1,
|
||||
// 'achievements.habitBirthday':1,
|
||||
// 'achievements.habitBirthdays':1,
|
||||
'items.gear.owned.armor_special_birthday2015':1,
|
||||
// 'items.gear.owned.armor_special':1
|
||||
};
|
||||
|
||||
console.warn('Updating users...');
|
||||
@@ -33,9 +43,11 @@ dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
|
||||
count++;
|
||||
|
||||
var unset = {'items.gear.owned.armor_special_birthday2015': 1};
|
||||
var set = {'migration': migrationName};
|
||||
// var set = {'migration':migrationName, 'achievements.habitBirthdays':1 };
|
||||
// var inc = {'xyz':1, _v:1};
|
||||
dbUsers.update({_id:user._id}, {$unset:unset, $set:set}); // , $inc:inc});
|
||||
dbUsers.update({_id:user._id}, {$unset:unset}); // , $inc:inc});
|
||||
// dbUsers.update({_id:user._id}, {$unset:unset, $set:set});
|
||||
// console.warn(user.auth.timestamps.created);
|
||||
|
||||
if (count%progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (user._id == authorUuid) console.warn(authorName + ' processed');
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
var migrationName = '20150201_convert_creation_date_from_string_to_object__no_date_recent_signup';
|
||||
//// var migrationName = '20150201_convert_creation_date_from_string_to_object';
|
||||
|
||||
var authorName = 'Alys'; // in case script author needs to know when their ...
|
||||
var authorUuid = 'd904bd62-da08-416b-a816-ba797c9ee265'; //... own data is done
|
||||
|
||||
/*
|
||||
* For users that have no value for auth.timestamps.created, assign them
|
||||
* a recent value.
|
||||
*
|
||||
* NOTE:
|
||||
* Before this script was used as described above, it was first used to
|
||||
* find all users that have a auth.timestamps.created field that is a string
|
||||
* rather than a date object and set it to be a date object. The code used
|
||||
* for this has been commented out with four slashes: ////
|
||||
*
|
||||
* https://github.com/HabitRPG/habitrpg/issues/4601#issuecomment-72339846
|
||||
*/
|
||||
|
||||
var dbserver = 'localhost:27017' // CHANGE THIS FOR PRODUCTION DATABASE
|
||||
|
||||
var mongo = require('mongoskin');
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
|
||||
var dbUsers = mongo.db(dbserver + '/habitrpg?auto_reconnect').collection('users');
|
||||
|
||||
var uuidArrayRecent=[ // recent users with no creation dates
|
||||
'1a0d4b75-73ed-4937-974d-d504d6398884',
|
||||
'1c7ebe27-1250-4f95-ba10-965580adbfd7',
|
||||
'5f972121-4a6d-411c-95e9-7093d3e89b66',
|
||||
'ae85818a-e336-4ccd-945e-c15cef975102',
|
||||
'ba273976-d9fc-466c-975f-38559d34a824',
|
||||
];
|
||||
|
||||
var query = {
|
||||
'_id':{$in: uuidArrayRecent}
|
||||
//// 'auth':{$exists:true},
|
||||
//// 'auth.timestamps':{$exists:true},
|
||||
//// 'auth.timestamps.created':{$not: {$lt:new Date('2018-01-01')}}
|
||||
};
|
||||
|
||||
var fields = {
|
||||
'_id':1,
|
||||
'auth.timestamps.created':1
|
||||
};
|
||||
// 'achievements.habitBirthdays':1
|
||||
|
||||
console.warn('Updating users...');
|
||||
var progressCount = 1000;
|
||||
var count = 0;
|
||||
dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
|
||||
if (err) { return exiting(1, 'ERROR! ' + err); }
|
||||
if (!user) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
return displayData();
|
||||
}
|
||||
count++;
|
||||
|
||||
//// var oldDate = user.auth.timestamps.created;
|
||||
//// var newDate = moment(oldDate).toDate();
|
||||
var oldDate = 'none';
|
||||
var newDate = moment('2015-01-11').toDate();
|
||||
console.warn(user._id + ' == ' + oldDate + ' == ' + newDate);
|
||||
|
||||
//// var set = { 'migration': migrationName,
|
||||
//// 'auth.timestamps.created': newDate,
|
||||
//// 'achievements.habitBirthdays': 2,
|
||||
//// 'items.gear.owned.head_special_nye':true,
|
||||
//// 'items.gear.owned.head_special_nye2014':true,
|
||||
//// 'items.gear.owned.armor_special_birthday':true,
|
||||
//// 'items.gear.owned.armor_special_birthday2015':true,
|
||||
//// };
|
||||
|
||||
var set = { 'migration': migrationName,
|
||||
'auth.timestamps.created': newDate,
|
||||
'achievements.habitBirthdays': 1,
|
||||
'items.gear.owned.armor_special_birthday':true,
|
||||
};
|
||||
|
||||
// var unset = {'items.gear.owned.armor_special_birthday2015': 1};
|
||||
// var inc = {'xyz':1, _v:1};
|
||||
dbUsers.update({_id:user._id}, {$set:set});
|
||||
// dbUsers.update({_id:user._id}, {$unset:unset, $set:set, $inc:inc});
|
||||
|
||||
if (count%progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (user._id == authorUuid) console.warn(authorName + ' processed');
|
||||
if (user._id == '9' ) console.warn('lefnire' + ' processed');
|
||||
});
|
||||
|
||||
|
||||
function displayData() {
|
||||
console.warn('\n' + count + ' users processed\n');
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
|
||||
function exiting(code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) { msg = 'ERROR!'; }
|
||||
if (msg) {
|
||||
if (code) { console.error(msg); }
|
||||
else { console.log( msg); }
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
@@ -213,6 +213,10 @@ window.habitrpg = angular.module('habitrpg',
|
||||
url: "/subscription",
|
||||
templateUrl: "partials/options.settings.subscription.html"
|
||||
})
|
||||
.state('options.settings.notifications', {
|
||||
url: "/notifications",
|
||||
templateUrl: "partials/options.settings.notifications.html"
|
||||
})
|
||||
|
||||
var settings = JSON.parse(localStorage.getItem(STORAGE_SETTINGS_ID));
|
||||
if (settings && settings.auth) {
|
||||
|
||||
@@ -242,5 +242,14 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
|
||||
window.location.href = url;
|
||||
window.location.reload(false);
|
||||
}
|
||||
|
||||
// Universal method for sending HTTP methods
|
||||
$rootScope.http = function(method, route, data, alertMsg){
|
||||
$http[method](ApiUrl.get() + route, data).success(function(){
|
||||
if (alertMsg) Notification.text(window.env.t(alertMsg));
|
||||
User.sync();
|
||||
});
|
||||
// error will be handled via $http interceptor
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -77,33 +77,12 @@ habitrpg.controller('SettingsCtrl',
|
||||
$rootScope.$state.go('tasks');
|
||||
}
|
||||
|
||||
$scope.changeUsername = function(changeUser){
|
||||
if (!changeUser.newUsername || !changeUser.password) {
|
||||
return alert(window.env.t('fillAll'));
|
||||
}
|
||||
$http.post(ApiUrl.get() + '/api/v2/user/change-username', changeUser)
|
||||
$scope.changeUser = function(attr, updates){
|
||||
$http.post(ApiUrl.get() + '/api/v2/user/change-'+attr, updates)
|
||||
.success(function(){
|
||||
alert(window.env.t('usernameSuccess'));
|
||||
$scope.changeUser = {};
|
||||
alert(window.env.t(attr+'Success'));
|
||||
_.each(updates, function(v,k){updates[k]=null;});
|
||||
User.sync();
|
||||
})
|
||||
.error(function(data){
|
||||
alert(data.err);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.changePassword = function(changePass){
|
||||
if (!changePass.oldPassword || !changePass.newPassword || !changePass.confirmNewPassword) {
|
||||
return alert(window.env.t('fillAll'));
|
||||
}
|
||||
$http.post(ApiUrl.get() + '/api/v2/user/change-password', changePass)
|
||||
.success(function(data, status, headers, config){
|
||||
if (data.err) return alert(data.err);
|
||||
alert(window.env.t('passSuccess'));
|
||||
$scope.changePass = {};
|
||||
})
|
||||
.error(function(data, status, headers, config){
|
||||
alert(data.err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ habitrpg
|
||||
link: function(scope, element, attrs) {
|
||||
// $scope.obj needs to come from controllers, so we can pass by ref
|
||||
scope.main = attrs.main;
|
||||
scope.modal = attrs.modal;
|
||||
var dailiesView;
|
||||
if(User.user.preferences.dailyDueDefaultView) {
|
||||
dailiesView = "remaining";
|
||||
|
||||
@@ -23,6 +23,10 @@ var accountSuspended = function(uuid){
|
||||
code: 'ACCOUNT_SUSPENDED'
|
||||
};
|
||||
}
|
||||
// escape email for regex, then search case-insensitive. See http://stackoverflow.com/a/3561711/362790
|
||||
var mongoEmailRegex = function(email){
|
||||
return new RegExp('^' + email.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$', 'i');
|
||||
}
|
||||
|
||||
api.auth = function(req, res, next) {
|
||||
var uid = req.headers['x-api-user'];
|
||||
@@ -61,55 +65,57 @@ api.authWithUrl = function(req, res, next) {
|
||||
}
|
||||
|
||||
api.registerUser = function(req, res, next) {
|
||||
var confirmPassword = req.body.confirmPassword,
|
||||
email = req.body.email,
|
||||
password = req.body.password,
|
||||
username = req.body.username;
|
||||
if (!(username && password && email)) return res.json(401, {err: ":username, :email, :password, :confirmPassword required"});
|
||||
if (password !== confirmPassword) return res.json(401, {err: ":password and :confirmPassword don't match"});
|
||||
if (!validator.isEmail(email)) return res.json(401, {err: ":email invalid"});
|
||||
async.waterfall([
|
||||
function(cb) {
|
||||
User.findOne({'auth.local.email': email}, cb);
|
||||
async.auto({
|
||||
validate: function(cb) {
|
||||
if (!(req.body.username && req.body.password && req.body.email))
|
||||
return cb({code:401, err: ":username, :email, :password, :confirmPassword required"});
|
||||
if (req.body.password !== req.body.confirmPassword)
|
||||
return cb({code:401, err: ":password and :confirmPassword don't match"});
|
||||
if (!validator.isEmail(req.body.email))
|
||||
return cb({code:401, err: ":email invalid"});
|
||||
cb();
|
||||
},
|
||||
function(found, cb) {
|
||||
if (found) return cb("Email already taken");
|
||||
User.findOne({'auth.local.username': username}, cb);
|
||||
}, function(found, cb) {
|
||||
var newUser, salt, user;
|
||||
if (found) return cb("Username already taken");
|
||||
salt = utils.makeSalt();
|
||||
newUser = {
|
||||
findEmail: function(cb) {
|
||||
User.findOne({'auth.local.email': req.body.email}, cb);
|
||||
},
|
||||
findUname: function(cb) {
|
||||
User.findOne({'auth.local.username': req.body.username}, cb);
|
||||
},
|
||||
findFacebook: function(cb){
|
||||
User.findOne({_id: req.headers['x-api-user'], apiToken: req.headers['x-api-key']}, {auth:1}, cb);
|
||||
},
|
||||
register: ['validate', 'findEmail', 'findUname', 'findFacebook', function(cb, data) {
|
||||
if (data.findEmail) return cb({code:401, err:"Email already taken"});
|
||||
if (data.findUname) return cb({code:401, err:"Username already taken"});
|
||||
var salt = utils.makeSalt();
|
||||
var newUser = {
|
||||
auth: {
|
||||
local: {
|
||||
username: username,
|
||||
email: email,
|
||||
username: req.body.username,
|
||||
email: req.body.email,
|
||||
salt: salt,
|
||||
hashed_password: utils.encryptPassword(password, salt)
|
||||
hashed_password: utils.encryptPassword(req.body.password, salt)
|
||||
},
|
||||
timestamps: {created: +new Date(), loggedIn: +new Date()}
|
||||
}
|
||||
};
|
||||
newUser.preferences = newUser.preferences || {};
|
||||
newUser.preferences.language = req.language; // User language detected from browser, not saved
|
||||
user = new User(newUser);
|
||||
|
||||
// temporary for conventions
|
||||
if (req.subdomains[0] == 'con') {
|
||||
_.each(user.dailys, function(h){
|
||||
h.repeat = {m:false,t:false,w:false,th:false,f:false,s:false,su:false};
|
||||
})
|
||||
user.extra = {signupEvent: 'wondercon'};
|
||||
// existing user, allow them to add local authentication
|
||||
if (data.findFacebook) {
|
||||
data.findFacebook.auth.local = newUser.auth.local;
|
||||
data.findFacebook.save(cb);
|
||||
// new user, register them
|
||||
} else {
|
||||
newUser.preferences = newUser.preferences || {};
|
||||
newUser.preferences.language = req.language; // User language detected from browser, not saved
|
||||
var user = new User(newUser);
|
||||
utils.txnEmail(user, 'welcome');
|
||||
ga.event('register', 'Local').send();
|
||||
user.save(cb);
|
||||
}
|
||||
|
||||
user.save(cb);
|
||||
if(isProd) utils.txnEmail({name:username, email:email}, 'welcome');
|
||||
ga.event('register', 'Local').send()
|
||||
}
|
||||
], function(err, saved) {
|
||||
if (err) return res.json(401, {err: err});
|
||||
res.json(200, saved);
|
||||
email = password = username = null;
|
||||
}]
|
||||
}, function(err, data) {
|
||||
if (err) return err.code ? res.json(err.code, err) : next(err);
|
||||
res.json(200, data.register[0]);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -173,9 +179,7 @@ api.loginSocial = function(req, res, next) {
|
||||
user = new User(user);
|
||||
user.save(cb);
|
||||
|
||||
if (isProd && prof.emails && prof.emails[0] && prof.emails[0].value) {
|
||||
utils.txnEmail({name: prof.displayName || prof.username, email: prof.emails[0].value}, 'welcome');
|
||||
}
|
||||
utils.txnEmail(user, 'welcome');
|
||||
ga.event('register', network).send();
|
||||
}]
|
||||
}, function(err, results){
|
||||
@@ -188,9 +192,18 @@ api.loginSocial = function(req, res, next) {
|
||||
|
||||
/**
|
||||
* DELETE /user/auth/social
|
||||
* TODO implement
|
||||
*/
|
||||
api.deleteSocial = function(req,res,next){next()}
|
||||
api.deleteSocial = function(req,res,next){
|
||||
if (!res.locals.user.auth.local.username)
|
||||
return res.json(401, {err:"Account lacks another authentication method, can't detach Facebook"});
|
||||
//FIXME for some reason, the following gives https://gist.github.com/lefnire/f93eb306069b9089d123
|
||||
//res.locals.user.auth.facebook = null;
|
||||
//res.locals.user.auth.save(function(err, saved){
|
||||
User.update({_id:res.locals.user._id}, {$unset:{'auth.facebook':1}}, function(err){
|
||||
if (err) return next(err);
|
||||
res.send(200);
|
||||
})
|
||||
}
|
||||
|
||||
api.resetPassword = function(req, res, next){
|
||||
var email = req.body.email,
|
||||
@@ -198,9 +211,7 @@ api.resetPassword = function(req, res, next){
|
||||
newPassword = utils.makeSalt(), // use a salt as the new password too (they'll change it later)
|
||||
hashed_password = utils.encryptPassword(newPassword, salt);
|
||||
|
||||
// escape email for regex, then search case-insensitive. See http://stackoverflow.com/a/3561711/362790
|
||||
var emailRegExp = new RegExp('^' + email.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$', 'i');
|
||||
User.findOne({'auth.local.email':emailRegExp}, function(err, user){
|
||||
User.findOne({'auth.local.email':mongoEmailRegex(email)}, function(err, user){
|
||||
if (err) return next(err);
|
||||
if (!user) return res.send(500, {err:"Couldn't find a user registered for email " + email});
|
||||
user.auth.local.salt = salt;
|
||||
@@ -217,28 +228,45 @@ api.resetPassword = function(req, res, next){
|
||||
});
|
||||
};
|
||||
|
||||
var invalidPassword = function(user, password){
|
||||
var hashed_password = utils.encryptPassword(password, user.auth.local.salt);
|
||||
if (hashed_password !== user.auth.local.hashed_password)
|
||||
return {code:401, err:"Incorrect password"};
|
||||
return false;
|
||||
}
|
||||
|
||||
api.changeUsername = function(req, res, next) {
|
||||
var user = res.locals.user,
|
||||
password = req.body.password,
|
||||
newUsername = req.body.newUsername;
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
User.findOne({'auth.local.username': req.body.username}, {auth:1}, cb);
|
||||
},
|
||||
function(found, cb){
|
||||
if (found) return cb({code:401, err: "Username already taken"});
|
||||
if (invalidPassword(res.locals.user, req.body.password)) return cb(invalidPassword(res.locals.user, req.body.password));
|
||||
res.locals.user.auth.local.username = req.body.username;
|
||||
res.locals.user.save(cb);
|
||||
}
|
||||
], function(err){
|
||||
if (err) return err.code ? res.json(err.code, err) : next(err);
|
||||
res.send(200);
|
||||
})
|
||||
}
|
||||
|
||||
User.findOne({'auth.local.username': newUsername}, function(err, result) {
|
||||
if (err) next(err);
|
||||
if(result) return res.json(401, {err: "Username already taken"});
|
||||
|
||||
var salt = user.auth.local.salt;
|
||||
var hashed_password = utils.encryptPassword(password, salt);
|
||||
|
||||
if (hashed_password !== user.auth.local.hashed_password)
|
||||
return res.json(401, {err:"Incorrect password"});
|
||||
|
||||
user.auth.local.username = newUsername;
|
||||
user.save(function(err, saved){
|
||||
if (err) next(err);
|
||||
res.send(200);
|
||||
user = password = newUsername = null;
|
||||
})
|
||||
});
|
||||
api.changeEmail = function(req, res, next){
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
User.findOne({'auth.local.email': mongoEmailRegex(req.body.email)}, {auth:1}, cb);
|
||||
},
|
||||
function(found, cb){
|
||||
if(found) return cb({code:401, err: "Email already taken"});
|
||||
if (invalidPassword(res.locals.user, req.body.password)) return cb(invalidPassword(res.locals.user, req.body.password));
|
||||
res.locals.user.auth.local.email = req.body.email;
|
||||
res.locals.user.save(cb);
|
||||
}
|
||||
], function(err){
|
||||
if (err) return err.code ? res.json(err.code,err) : next(err);
|
||||
res.send(200);
|
||||
})
|
||||
}
|
||||
|
||||
api.changePassword = function(req, res, next) {
|
||||
|
||||
@@ -9,6 +9,7 @@ var Group = require('./../models/group').model;
|
||||
var Challenge = require('./../models/challenge').model;
|
||||
var logging = require('./../logging');
|
||||
var csv = require('express-csv');
|
||||
var utils = require('../utils');
|
||||
var api = module.exports;
|
||||
|
||||
|
||||
@@ -335,6 +336,11 @@ api.selectWinner = function(req, res, next) {
|
||||
winner.save(cb);
|
||||
},
|
||||
function(saved, num, cb) {
|
||||
if(saved.preferences.emailNotifications.wonChallenge !== false){
|
||||
utils.txnEmail(saved, 'won-challenge', [
|
||||
{name: 'CHALLENGE_NAME', content: chal.name}
|
||||
]);
|
||||
}
|
||||
closeChal(cid, {broken: 'CHALLENGE_CLOSED', winner: saved.profile.name}, cb);
|
||||
}
|
||||
], function(err){
|
||||
|
||||
@@ -301,13 +301,11 @@ api.flagChatMessage = function(req, res, next){
|
||||
group.markModified('chat');
|
||||
group.save(function(err,_saved){
|
||||
if(err) return next(err);
|
||||
if (isProd){
|
||||
|
||||
var addressesToSendTo = JSON.parse(nconf.get('FLAG_REPORT_EMAIL'));
|
||||
|
||||
if(Array.isArray(addressesToSendTo)){
|
||||
addressesToSendTo = addressesToSendTo.map(function(email){
|
||||
return {email: email}
|
||||
return {email: email, canSend: true}
|
||||
});
|
||||
}else{
|
||||
addressesToSendTo = {email: addressesToSendTo}
|
||||
@@ -332,7 +330,7 @@ api.flagChatMessage = function(req, res, next){
|
||||
{name: "GROUP_ID", content: group._id},
|
||||
{name: "GROUP_URL", content: group._id == 'habitrpg' ? (nconf.get('BASE_URL') + '/#/options/groups/tavern') : (group.type === 'guild' ? (nconf.get('BASE_URL')+ '/#/options/groups/guilds/' + group._id) : 'party')},
|
||||
]);
|
||||
}
|
||||
|
||||
return res.send(204);
|
||||
});
|
||||
});
|
||||
@@ -556,6 +554,26 @@ api.invite = function(req, res, next) {
|
||||
], function(err, results){
|
||||
if (err) return next(err);
|
||||
|
||||
if(invite.preferences.emailNotifications['invited' + (group.type == 'guild' ? 'Guild' : 'Party')] !== false){
|
||||
var emailVars = [
|
||||
{name: 'INVITER', content: utils.getUserInfo(res.locals.user, ['name']).name}
|
||||
];
|
||||
|
||||
if(group.type == 'guild'){
|
||||
emailVars.push(
|
||||
{name: 'GUILD_NAME', content: group.name},
|
||||
{name: 'GUILD_URL', content: nconf.get('BASE_URL') + '/#/options/groups/guilds/public'}
|
||||
);
|
||||
}else{
|
||||
emailVars.push(
|
||||
{name: 'PARTY_NAME', content: group.name},
|
||||
{name: 'PARTY_URL', content: nconf.get('BASE_URL') + '/#/options/groups/party'}
|
||||
)
|
||||
}
|
||||
|
||||
utils.txnEmail(invite, ('invited-' + (group.type == 'guild' ? 'guild' : 'party')), emailVars);
|
||||
}
|
||||
|
||||
// Have to return whole group and its members for angular to show the invited user
|
||||
res.json(results[2]);
|
||||
group = uuid = null;
|
||||
@@ -629,7 +647,7 @@ questStart = function(req, res, next) {
|
||||
var group = res.locals.group;
|
||||
var force = req.query.force;
|
||||
|
||||
// if (group.quest.active) return res.json(400,{err:'Quest already began.'});
|
||||
// if (group.quest.active) return res.json(400,{err:'Quest already began.'});
|
||||
// temporarily send error email, until we know more about this issue (then remove below, uncomment above).
|
||||
if (group.quest.active) return next('Quest already began.');
|
||||
|
||||
@@ -720,8 +738,9 @@ api.questAccept = function(req, res, next) {
|
||||
if (m == user._id) {
|
||||
group.quest.members[m] = true;
|
||||
group.quest.leader = user._id;
|
||||
} else
|
||||
} else {
|
||||
group.quest.members[m] = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
// Party member accepting the invitation
|
||||
|
||||
@@ -5,6 +5,8 @@ var api = module.exports;
|
||||
var async = require('async');
|
||||
var _ = require('lodash');
|
||||
var shared = require('../../../common');
|
||||
var utils = require('../utils');
|
||||
var nconf = require('nconf');
|
||||
|
||||
var fetchMember = function(uuid, restrict){
|
||||
return function(cb){
|
||||
@@ -48,9 +50,11 @@ api.sendMessage = function(user, member, data){
|
||||
}
|
||||
|
||||
api.sendPrivateMessage = function(req, res, next){
|
||||
var fetchedMember;
|
||||
async.waterfall([
|
||||
fetchMember(req.params.uuid),
|
||||
function(member, cb) {
|
||||
fetchedMember = member;
|
||||
if (~member.inbox.blocks.indexOf(res.locals.user._id) // can't send message if that user blocked me
|
||||
|| ~res.locals.user.inbox.blocks.indexOf(member._id) // or if I blocked them
|
||||
|| member.inbox.optOut) { // or if they've opted out of messaging
|
||||
@@ -64,6 +68,14 @@ api.sendPrivateMessage = function(req, res, next){
|
||||
}
|
||||
], function(err){
|
||||
if (err) return sendErr(err, res, next);
|
||||
|
||||
if(fetchedMember.preferences.emailNotifications.newPM !== false){
|
||||
utils.txnEmail(fetchedMember, 'new-pm', [
|
||||
{name: 'SENDER', content: utils.getUserInfo(res.locals.user, ['name']).name},
|
||||
{name: 'PMS_INBOX_URL', content: nconf.get('BASE_URL') + '/#/options/groups/inbox'}
|
||||
]);
|
||||
}
|
||||
|
||||
res.send(200);
|
||||
})
|
||||
}
|
||||
@@ -84,6 +96,12 @@ api.sendGift = function(req, res, next){
|
||||
member.balance += amt;
|
||||
user.balance -= amt;
|
||||
api.sendMessage(user, member, req.body);
|
||||
if(member.preferences.emailNotifications.giftedGems !== false){
|
||||
utils.txnEmail(member, 'gifted-gems', [
|
||||
{name: 'GIFTER', content: utils.getUserInfo(user, ['name']).name},
|
||||
{name: 'X_GEMS_GIFTED', content: req.body.gems.amount}
|
||||
]);
|
||||
}
|
||||
return async.parallel([
|
||||
function (cb2) { member.save(cb2) },
|
||||
function (cb2) { user.save(cb2) }
|
||||
|
||||
@@ -73,7 +73,15 @@ exports.createSubscription = function(data, cb) {
|
||||
utils.ga.transaction(data.user._id, block.price).item(block.price, 1, data.paymentMethod.toLowerCase() + '-subscription', data.paymentMethod).send();
|
||||
}
|
||||
data.user.purchased.txnCount++;
|
||||
if (data.gift) members.sendMessage(data.user, data.gift.member, data.gift);
|
||||
if (data.gift){
|
||||
members.sendMessage(data.user, data.gift.member, data.gift);
|
||||
if(data.gift.member.preferences.emailNotifications.giftedSubscription !== false){
|
||||
utils.txnEmail(member, 'gifted-subscription', [
|
||||
{name: 'GIFTER', content: utils.getUserInfo(data.user, ['name']).name},
|
||||
{name: 'X_MONTHS_SUBSCRIPTION', content: months}
|
||||
]);
|
||||
}
|
||||
}
|
||||
async.parallel([
|
||||
function(cb2){data.user.save(cb2)},
|
||||
function(cb2){data.gift ? data.gift.member.save(cb2) : cb2(null);}
|
||||
@@ -96,7 +104,7 @@ exports.cancelSubscription = function(data, cb) {
|
||||
p.extraMonths = 0; // clear extra time. If they subscribe again, it'll be recalculated from p.dateTerminated
|
||||
|
||||
data.user.save(cb);
|
||||
if(isProduction) utils.txnEmail(data.user, 'cancel-subscription');
|
||||
utils.txnEmail(data.user, 'cancel-subscription');
|
||||
utils.ga.event('unsubscribe', data.paymentMethod).send();
|
||||
}
|
||||
|
||||
@@ -110,7 +118,15 @@ exports.buyGems = function(data, cb) {
|
||||
//TODO ga.transaction to reflect whether this is gift or self-purchase
|
||||
utils.ga.transaction(data.user._id, amt).item(amt, 1, data.paymentMethod.toLowerCase() + "-checkout", "Gems > " + data.paymentMethod).send();
|
||||
}
|
||||
if (data.gift) members.sendMessage(data.user, data.gift.member, data.gift);
|
||||
if (data.gift){
|
||||
members.sendMessage(data.user, data.gift.member, data.gift);
|
||||
if(data.gift.member.preferences.emailNotifications.giftedGems !== false){
|
||||
utils.txnEmail(member, 'gifted-gems', [
|
||||
{name: 'GIFTER', content: utils.getUserInfo(data.user, ['name']).name},
|
||||
{name: 'X_GEMS_GIFTED', content: data.gift.gems.amount || 20}
|
||||
]);
|
||||
}
|
||||
}
|
||||
async.parallel([
|
||||
function(cb2){data.user.save(cb2)},
|
||||
function(cb2){data.gift ? data.gift.member.save(cb2) : cb2(null);}
|
||||
@@ -137,4 +153,4 @@ exports.paypalCheckoutSuccess = paypal.executePayment;
|
||||
exports.paypalIPN = paypal.ipn;
|
||||
|
||||
exports.iapAndroidVerify = iap.androidVerify;
|
||||
exports.iapIosVerify = iap.iosVerify;
|
||||
exports.iapIosVerify = iap.iosVerify;
|
||||
@@ -261,58 +261,36 @@ api.update = function(req, res, next) {
|
||||
};
|
||||
|
||||
api.cron = function(req, res, next) {
|
||||
try{
|
||||
var user = res.locals.user,
|
||||
progress = user.fns.cron(),
|
||||
ranCron = user.isModified(),
|
||||
quest = shared.content.quests[user.party.quest.key];
|
||||
var user = res.locals.user,
|
||||
progress = user.fns.cron(),
|
||||
ranCron = user.isModified(),
|
||||
quest = shared.content.quests[user.party.quest.key];
|
||||
|
||||
if (ranCron) res.locals.wasModified = true;
|
||||
if (!ranCron) return next(null,user);
|
||||
Group.tavernBoss(user,progress);
|
||||
if (!quest) return user.save(next);
|
||||
|
||||
// FOR DEBUGGING, PLEASE IGNORE
|
||||
var opStatus = null;
|
||||
|
||||
// If user is on a quest, roll for boss & player, or handle collections
|
||||
// FIXME this saves user, runs db updates, loads user. Is there a better way to handle this?
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
opStatus = 'saveUser';
|
||||
user.save(cb); // make sure to save the cron effects
|
||||
},
|
||||
function(saved, count, cb){
|
||||
opStatus = 'runQuest';
|
||||
var type = quest.boss ? 'boss' : 'collect';
|
||||
Group[type+'Quest'](user,progress,cb);
|
||||
},
|
||||
function(){
|
||||
var cb = arguments[arguments.length-1];
|
||||
// User has been updated in boss-grapple, reload
|
||||
User.findById(user._id, cb);
|
||||
}
|
||||
], function(err, saved) {
|
||||
if(err) logging.loggly({
|
||||
error: "Cron caught",
|
||||
stack: (err.stack || err.message || err),
|
||||
body: req.body, headers: req.header,
|
||||
auth: req.headers['x-api-user'],
|
||||
originalUrl: req.originalUrl,
|
||||
opStatus: opStatus
|
||||
});
|
||||
res.locals.user = saved;
|
||||
next(err,saved);
|
||||
user = progress = quest = null;
|
||||
});
|
||||
}catch(e){
|
||||
logging.loggly({
|
||||
error: "Cron uncaught",
|
||||
stack: e.stack || e
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
if (ranCron) res.locals.wasModified = true;
|
||||
if (!ranCron) return next(null,user);
|
||||
Group.tavernBoss(user,progress);
|
||||
if (!quest) return user.save(next);
|
||||
|
||||
// If user is on a quest, roll for boss & player, or handle collections
|
||||
// FIXME this saves user, runs db updates, loads user. Is there a better way to handle this?
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
user.save(cb); // make sure to save the cron effects
|
||||
},
|
||||
function(saved, count, cb){
|
||||
var type = quest.boss ? 'boss' : 'collect';
|
||||
Group[type+'Quest'](user,progress,cb);
|
||||
},
|
||||
function(){
|
||||
var cb = arguments[arguments.length-1];
|
||||
// User has been updated in boss-grapple, reload
|
||||
User.findById(user._id, cb);
|
||||
}
|
||||
], function(err, saved) {
|
||||
res.locals.user = saved;
|
||||
next(err,saved);
|
||||
user = progress = quest = null;
|
||||
});
|
||||
};
|
||||
|
||||
// api.reroll // Shared.ops
|
||||
@@ -437,16 +415,34 @@ api.cast = function(req, res, next) {
|
||||
api.inviteFriends = function(req, res, next) {
|
||||
Group.findOne({type:'party', members:{'$in': [res.locals.user._id]}}).select('_id name').exec(function(err,party){
|
||||
if (err) return next(err);
|
||||
var link = nconf.get('BASE_URL')+'?partyInvite='+ utils.encrypt(JSON.stringify({id:party._id, inviter:res.locals.user._id, name:party.name}));
|
||||
|
||||
_.each(req.body.emails, function(invite){
|
||||
if (invite.email) {
|
||||
var variables = [
|
||||
{name: 'LINK', content: link},
|
||||
{name: 'INVITER', content: req.body.inviter || res.locals.user.profile.name},
|
||||
{name: 'INVITEE', content: invite.name}
|
||||
];
|
||||
// TODO implement "users can only be invited once"
|
||||
utils.txnEmail(invite, 'invite-friend', variables);
|
||||
|
||||
User.findOne({$or: [
|
||||
{'auth.local.email': invite.email},
|
||||
{'auth.facebook.emails.value': invite.email}
|
||||
]}).select({_id: true, 'preferences.emailNotifications': true})
|
||||
.exec(function(err, userToContact){
|
||||
if(err) return next(err);
|
||||
|
||||
var link = nconf.get('BASE_URL')+'?partyInvite='+ utils.encrypt(JSON.stringify({id:party._id, inviter:res.locals.user._id, name:party.name}));
|
||||
|
||||
var variables = [
|
||||
{name: 'LINK', content: link},
|
||||
{name: 'INVITER', content: req.body.inviter || utils.getUserInfo(res.locals.user, ['name']).name}
|
||||
];
|
||||
|
||||
invite.canSend = true;
|
||||
|
||||
// We check for unsubscribeFromAll here because don't pass through utils.getUserInfo
|
||||
if(!userToContact || (userToContact.preferences.emailNotifications.invitedParty !== false &&
|
||||
userToContact.preferences.emailNotifications.unsubscribeFromAll !== true)){
|
||||
// TODO implement "users can only be invited once"
|
||||
utils.txnEmail(invite, 'invite-friend', variables);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
res.send(200);
|
||||
@@ -477,7 +473,7 @@ api.sessionPartyInvite = function(req,res,next){
|
||||
}
|
||||
|
||||
/**
|
||||
* All other user.ops which can easily be mapped to ../../common/scripts/index.coffee, not requiring custom API-wrapping
|
||||
* All other user.ops which can easily be mapped to habitrpg-shared/index.coffee, not requiring custom API-wrapping
|
||||
*/
|
||||
_.each(shared.wrap({}).ops, function(op,k){
|
||||
if (!api[k]) {
|
||||
|
||||
@@ -5,6 +5,7 @@ require('winston-newrelic');
|
||||
|
||||
var logger, loggly;
|
||||
|
||||
// Currently disabled
|
||||
if (nconf.get('LOGGLY:enabled')){
|
||||
loggly = require('loggly').createClient({
|
||||
token: nconf.get('LOGGLY:token'),
|
||||
|
||||
@@ -75,13 +75,13 @@ module.exports.errorHandler = function(err, req, res, next) {
|
||||
"\n\nbody: " + JSON.stringify(req.body) +
|
||||
(res.locals.ops ? "\n\ncompleted ops: " + JSON.stringify(res.locals.ops) : "");
|
||||
logging.error(stack);
|
||||
logging.loggly({
|
||||
/*logging.loggly({
|
||||
error: "Uncaught error",
|
||||
stack: (err.stack || err.message || err),
|
||||
body: req.body, headers: req.header,
|
||||
auth: req.headers['x-api-user'],
|
||||
originalUrl: req.originalUrl
|
||||
});
|
||||
});*/
|
||||
var message = err.message ? err.message : err;
|
||||
message = (message.length < 200) ? message : message.substring(0,100) + message.substring(message.length-100,message.length);
|
||||
res.json(500,{err:message}); //res.end(err.message);
|
||||
|
||||
@@ -297,7 +297,20 @@ var UserSchema = new Schema({
|
||||
advancedCollapsed: {type: Boolean, 'default': false},
|
||||
toolbarCollapsed: {type:Boolean, 'default':false},
|
||||
background: String,
|
||||
webhooks: {type: Schema.Types.Mixed, 'default': {}}
|
||||
webhooks: {type: Schema.Types.Mixed, 'default': {}},
|
||||
// For this fields make sure to use strict comparison when searching for falsey values (=== false)
|
||||
// As users who didn't login after these were introduced may have them undefined/null
|
||||
emailNotifications: {
|
||||
unsubscribeFromAll: {type: Boolean, 'default': false},
|
||||
newPM: {type: Boolean, 'default': true},
|
||||
wonChallenge: {type: Boolean, 'default': true},
|
||||
giftedGems: {type: Boolean, 'default': true},
|
||||
giftedSubscription: {type: Boolean, 'default': true},
|
||||
invitedParty: {type: Boolean, 'default': true},
|
||||
invitedGuild: {type: Boolean, 'default': true},
|
||||
//remindersToLogin: {type: Boolean, 'default': true},
|
||||
importantAnnouncements: {type: Boolean, 'default': true}
|
||||
}
|
||||
},
|
||||
profile: {
|
||||
blurb: String,
|
||||
|
||||
@@ -8,9 +8,11 @@ auth.setupPassport(router); //FIXME make this consistent with the others
|
||||
router.post('/api/v2/register', i18n.getUserLanguage, auth.registerUser);
|
||||
router.post('/api/v2/user/auth/local', i18n.getUserLanguage, auth.loginLocal);
|
||||
router.post('/api/v2/user/auth/social', i18n.getUserLanguage, auth.loginSocial);
|
||||
router.delete('/api/v2/user/auth/social', i18n.getUserLanguage, auth.auth, auth.deleteSocial);
|
||||
router.post('/api/v2/user/reset-password', i18n.getUserLanguage, auth.resetPassword);
|
||||
router.post('/api/v2/user/change-password', i18n.getUserLanguage, auth.auth, auth.changePassword);
|
||||
router.post('/api/v2/user/change-username', i18n.getUserLanguage, auth.auth, auth.changeUsername);
|
||||
router.post('/api/v2/user/change-email', i18n.getUserLanguage, auth.auth, auth.changeEmail);
|
||||
|
||||
router.post('/api/v1/register', i18n.getUserLanguage, auth.registerUser);
|
||||
router.post('/api/v1/user/auth/local', i18n.getUserLanguage, auth.loginLocal);
|
||||
|
||||
@@ -4,6 +4,9 @@ var crypto = require('crypto');
|
||||
var path = require("path");
|
||||
var request = require('request');
|
||||
|
||||
// Set when utils.setupConfig is run
|
||||
var isProd, baseUrl;
|
||||
|
||||
module.exports.ga = undefined; // set Google Analytics on nconf init
|
||||
|
||||
module.exports.sendEmail = function(mailData) {
|
||||
@@ -22,48 +25,75 @@ module.exports.sendEmail = function(mailData) {
|
||||
});
|
||||
}
|
||||
|
||||
function getMailingInfo(user) {
|
||||
var email, name;
|
||||
if(user.auth.local && user.auth.local.email){
|
||||
email = user.auth.local.email;
|
||||
name = user.profile.name || user.auth.local.username;
|
||||
}else if(user.auth.facebook && user.auth.facebook.emails && user.auth.facebook.emails[0] && user.auth.facebook.emails[0].value){
|
||||
email = user.auth.facebook.emails[0].value;
|
||||
name = user.auth.facebook.displayName || user.auth.facebook.username;
|
||||
function getUserInfo(user, fields) {
|
||||
var info = {};
|
||||
|
||||
if(fields.indexOf('name') != -1){
|
||||
if(user.auth.local){
|
||||
info.name = user.profile.name || user.auth.local.username;
|
||||
}else if(user.auth.facebook){
|
||||
info.name = user.auth.facebook.displayName || user.auth.facebook.username;
|
||||
}
|
||||
}
|
||||
return {email: email, name: name};
|
||||
|
||||
if(fields.indexOf('email') != -1){
|
||||
if(user.auth.local){
|
||||
info.email = user.auth.local.email;
|
||||
}else if(user.auth.facebook && user.auth.facebook.emails && user.auth.facebook.emails[0] && user.auth.facebook.emails[0].value){
|
||||
info.email = user.auth.facebook.emails[0].value;
|
||||
}
|
||||
}
|
||||
|
||||
if(fields.indexOf('canSend') != -1){
|
||||
info.canSend = user.preferences.emailNotifications.unsubscribeFromAll !== true;
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
module.exports.getUserInfo = getUserInfo;
|
||||
|
||||
module.exports.txnEmail = function(mailingInfoArray, emailType, variables){
|
||||
var variables = [{name: 'BASE_URL', content: nconf.get('BASE_URL')}].concat(variables || []);
|
||||
var mailingInfoArray = Array.isArray(mailingInfoArray) ? mailingInfoArray : [mailingInfoArray];
|
||||
var variables = [
|
||||
{name: 'BASE_URL', content: baseUrl},
|
||||
{name: 'EMAIL_SETTINGS_URL', content: baseUrl + '/#/options/settings/notifications'}
|
||||
].concat(variables || []);
|
||||
|
||||
// It's important to pass at least a user with its `preferences` as we need to check if he unsubscribed
|
||||
mailingInfoArray = mailingInfoArray.map(function(mailingInfo){
|
||||
return mailingInfo._id ? getMailingInfo(mailingInfo) : mailingInfo;
|
||||
return mailingInfo._id ? getUserInfo(mailingInfo, ['email', 'name', 'canSend']) : mailingInfo;
|
||||
}).filter(function(mailingInfo){
|
||||
return mailingInfo.email ? true : false;
|
||||
return (mailingInfo.email && mailingInfo.canSend);
|
||||
});
|
||||
|
||||
request({
|
||||
url: nconf.get('EMAIL_SERVER:url') + '/job',
|
||||
method: 'POST',
|
||||
auth: {
|
||||
user: nconf.get('EMAIL_SERVER:authUser'),
|
||||
pass: nconf.get('EMAIL_SERVER:authPassword')
|
||||
},
|
||||
json: {
|
||||
type: 'email',
|
||||
data: {
|
||||
emailType: emailType,
|
||||
to: mailingInfoArray,
|
||||
variables: variables
|
||||
// When only one recipient send his info as variables
|
||||
if(mailingInfoArray.length === 1 && mailingInfoArray[0].name){
|
||||
variables.push({name: 'RECIPIENT_NAME', content: mailingInfoArray[0].name});
|
||||
}
|
||||
|
||||
if(isProd && mailingInfoArray.length > 0){
|
||||
request({
|
||||
url: nconf.get('EMAIL_SERVER:url') + '/job',
|
||||
method: 'POST',
|
||||
auth: {
|
||||
user: nconf.get('EMAIL_SERVER:authUser'),
|
||||
pass: nconf.get('EMAIL_SERVER:authPassword')
|
||||
},
|
||||
options: {
|
||||
attemps: 5,
|
||||
backoff: {delay: 10*60*1000, type: 'fixed'}
|
||||
json: {
|
||||
type: 'email',
|
||||
data: {
|
||||
emailType: emailType,
|
||||
to: mailingInfoArray,
|
||||
variables: variables
|
||||
},
|
||||
options: {
|
||||
attemps: 5,
|
||||
backoff: {delay: 10*60*1000, type: 'fixed'}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Encryption using http://dailyjs.com/2010/12/06/node-tutorial-5/
|
||||
@@ -93,6 +123,9 @@ module.exports.setupConfig = function(){
|
||||
if (nconf.get('NODE_ENV') === 'production')
|
||||
require('newrelic');
|
||||
|
||||
isProd = nconf.get('NODE_ENV') === 'production';
|
||||
baseUrl = nconf.get('BASE_URL');
|
||||
|
||||
module.exports.ga = require('universal-analytics')(nconf.get('GA_ID'));
|
||||
};
|
||||
|
||||
|
||||
@@ -23,14 +23,14 @@ script(type='text/ng-template', id='partials/options.inventory.seasonalshop.html
|
||||
.container-fluid
|
||||
.stable.row
|
||||
.col-md-2
|
||||
.seasonalshop_winter2015
|
||||
.seasonalshop_closed
|
||||
.col-md-10
|
||||
.popover.static-popover.fade.right.in
|
||||
.arrow
|
||||
h3.popover-title!=env.t('seasonalShopTitle', {linkStart:"<a href='http://blog.habitrpg.com/who' target='_blank'>", linkEnd: "</a>"})
|
||||
h3.popover-title!=env.t('seasonalShopClosedTitle', {linkStart:"<a href='http://blog.habitrpg.com/who' target='_blank'>", linkEnd: "</a>"})
|
||||
.popover-content
|
||||
p!=env.t('seasonalShopText')
|
||||
br
|
||||
p!=env.t('seasonalShopClosedText', {linkStart:"<a href='http://habitrpg.wikia.com/wiki/Grand_Galas' target='_blank'>", linkEnd: "</a>"})
|
||||
// br
|
||||
.well(ng-if='User.user.achievements.rebirths > 0')=env.t('seasonalShopRebirth')
|
||||
li.customize-menu.inventory-gear
|
||||
menu.pets-menu(label='{{::label}}', ng-repeat='(set,label) in ::{candycane:env.t("candycaneSet"), ski:env.t("skiSet"), snowflake:env.t("snowflakeSet"), yeti:env.t("yetiSet")}')
|
||||
|
||||
@@ -63,7 +63,7 @@ mixin customizeProfile(mobile)
|
||||
button(type='button', ng-if='user.purchased.hair.color.#{color}', class='customize-option hair hair_bangs_1_#{color}', ng-click='unlock("hair.color.#{color}")')
|
||||
+buyPref('hair.color', ['rainbow','yellow','green','purple','blue','TRUred'], 'rainbowColors')
|
||||
+buyPref('hair.color', ['candycorn','ghostwhite','halloween','midnight','pumpkin','zombie'], 'hauntedColors', 'disabled')
|
||||
+buyPref('hair.color', ['aurora','festive','hollygreen','peppermint','snowy','winterstar'], 'winteryColors')
|
||||
+buyPref('hair.color', ['aurora','festive','hollygreen','peppermint','snowy','winterstar'], 'winteryColors', 'disabled')
|
||||
|
||||
li.customize-menu
|
||||
menu(label=env.t('bodyHair'))
|
||||
|
||||
@@ -14,6 +14,8 @@ script(id='partials/options.settings.html', type="text/ng-template")
|
||||
=env.t('coupon')
|
||||
li(ng-class="{ active: $state.includes('options.settings.subscription') }")
|
||||
a(ui-sref='options.settings.subscription')=env.t('subscription')
|
||||
li(ng-class="{ active: $state.includes('options.settings.notifications') }")
|
||||
a(ui-sref='options.settings.notifications')=env.t('notifications')
|
||||
|
||||
.tab-content
|
||||
.tab-pane.active
|
||||
@@ -84,11 +86,29 @@ script(type='text/ng-template', id='partials/options.settings.settings.html')
|
||||
|
|
||||
=env.t('subWarning3')
|
||||
|
||||
.personal-options.col-md-6
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
span Registration
|
||||
.panel-body
|
||||
p(ng-if='user.auth.facebook.id')=env.t('registeredWithFb')
|
||||
div(ng-if='user.auth.facebook.id')
|
||||
button.btn.btn-primary(disabled='disabled', ng-if='!user.auth.local.username')=env.t('registeredWithFb')
|
||||
button.btn.btn-danger(ng-click='http("delete","/api/v2/user/auth/social",null,"detachedFacebook")', ng-if='user.auth.local.username')=env.t('detachFacebook')
|
||||
hr
|
||||
div(ng-if='!user.auth.local.username')
|
||||
p Add local authentication:
|
||||
form(ng-submit='http("post","/api/v2/register",localAuth,"addedLocalAuth")', ng-init='localAuth={}', name='localAuth', novalidate)
|
||||
//-.alert.alert-danger(ng-messages='changeUsername.$error && changeUsername.submitted')=env.t('fillAll')
|
||||
.form-group
|
||||
input.form-control(type='text', placeholder=env.t('username'), ng-model='localAuth.username', required)
|
||||
.form-group
|
||||
input.form-control(type='text', placeholder=env.t('email'), ng-model='localAuth.email', required)
|
||||
.form-group
|
||||
input.form-control(type='password', placeholder=env.t('password'), ng-model='localAuth.password', required)
|
||||
.form-group
|
||||
input.form-control(type='password', placeholder=env.t('confirmPass'), ng-model='localAuth.confirmPassword', required)
|
||||
input.btn.btn-default(type='submit', ng-disabled='localAuth.$invalid', value=env.t('submit'))
|
||||
|
||||
div(ng-if='user.auth.local.username')
|
||||
p=env.t('username')
|
||||
|: {{user.auth.local.username}}
|
||||
@@ -100,31 +120,35 @@ script(type='text/ng-template', id='partials/options.settings.settings.html')
|
||||
|
|
||||
=env.t('loginNameDescription3')
|
||||
p=env.t('email')
|
||||
|: {{::user.auth.local.email}}
|
||||
p
|
||||
small.muted
|
||||
=env.t('emailChange1')
|
||||
|
|
||||
a(href='mailto:admin@habitrpg.com')=env.t('emailChange2')
|
||||
|
|
||||
=env.t('emailChange3')
|
||||
|: {{user.auth.local.email}}
|
||||
hr
|
||||
|
||||
h5=env.t('changeUsername')
|
||||
form(ng-submit='changeUsername(changeUser)', ng-show='user.auth.local')
|
||||
form(ng-submit='changeUser("username", usernameUpdates)', ng-init='usernameUpdates={}', ng-show='user.auth.local', name='changeUsername', novalidate)
|
||||
//-.alert.alert-danger(ng-messages='changeUsername.$error && changeUsername.submitted')=env.t('fillAll')
|
||||
.form-group
|
||||
input.form-control(type='text', placeholder=env.t('newUsername'), ng-model='changeUser.newUsername', required)
|
||||
input.form-control(type='text', placeholder=env.t('newUsername'), ng-model='usernameUpdates.username', required)
|
||||
.form-group
|
||||
input.form-control(type='password', placeholder=env.t('password'), ng-model='changeUser.password', required)
|
||||
input.btn.btn-default(type='submit', value=env.t('submit'))
|
||||
input.form-control(type='password', placeholder=env.t('password'), ng-model='usernameUpdates.password', required)
|
||||
input.btn.btn-default(type='submit', ng-disabled='changeUsername.$invalid', value=env.t('submit'))
|
||||
|
||||
h5=env.t('changeEmail')
|
||||
form(ng-submit='changeUser("email", emailUpdates)', ng-show='user.auth.local', name='changeEmail', novalidate)
|
||||
.form-group
|
||||
input.form-control(type='text', placeholder=env.t('newEmail'), ng-model='emailUpdates.email', required)
|
||||
.form-group
|
||||
input.form-control(type='password', placeholder=env.t('password'), ng-model='emailUpdates.password', required)
|
||||
input.btn.btn-default(type='submit', ng-disabled='changeEmail.$invalid', value=env.t('submit'))
|
||||
|
||||
h5=env.t('changePass')
|
||||
form(ng-submit='changePassword(changePass)', ng-show='user.auth.local')
|
||||
form(ng-submit='changeUser("password", passwordUpdates)', ng-show='user.auth.local', name='changePassword', novalidate)
|
||||
.form-group
|
||||
input.form-control(type='password', placeholder=env.t('oldPass'), ng-model='changePass.oldPassword', required)
|
||||
input.form-control(type='password', placeholder=env.t('oldPass'), ng-model='passwordUpdates.oldPassword', required)
|
||||
.form-group
|
||||
input.form-control(type='password', placeholder=env.t('newPass'), ng-model='changePass.newPassword', required)
|
||||
input.form-control(type='password', placeholder=env.t('newPass'), ng-model='passwordUpdates.newPassword', required)
|
||||
.form-group
|
||||
input.form-control(type='password', placeholder=env.t('confirmPass'), ng-model='changePass.confirmNewPassword', required)
|
||||
input.btn.btn-default(type='submit', value=env.t('submit'))
|
||||
input.form-control(type='password', placeholder=env.t('confirmPass'), ng-model='passwordUpdates.confirmNewPassword', required)
|
||||
input.btn.btn-default(type='submit', ng-disabled='changePassword.$invalid', value=env.t('submit'))
|
||||
|
||||
|
||||
.panel.panel-default
|
||||
@@ -242,6 +266,64 @@ script(id='partials/feature-matrix-check.html',type='text/ng-template')
|
||||
input.focusable(type='checkbox', checked)
|
||||
label
|
||||
|
||||
script(id='partials/options.settings.notifications.html', type="text/ng-template")
|
||||
.container-fluid
|
||||
.row
|
||||
.personal-options.col-md-6
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
=env.t('emailNotifications')
|
||||
.panel-body
|
||||
|
||||
.checkbox
|
||||
label
|
||||
input(type='checkbox', ng-disabled='user.preferences.emailNotifications.unsubscribeFromAll === true', ng-model='user.preferences.emailNotifications.newPM', ng-change='set({"preferences.emailNotifications.newPM": user.preferences.emailNotifications.newPM ? true: false})')
|
||||
span=env.t('newPM')
|
||||
|
||||
.checkbox
|
||||
label
|
||||
input(type='checkbox', ng-disabled='user.preferences.emailNotifications.unsubscribeFromAll === true', ng-model='user.preferences.emailNotifications.wonChallenge', ng-change='set({"preferences.emailNotifications.wonChallenge": user.preferences.emailNotifications.wonChallenge ? true: false})')
|
||||
span=env.t('wonChallenge')
|
||||
|
||||
.checkbox
|
||||
label
|
||||
input(type='checkbox', ng-disabled='user.preferences.emailNotifications.unsubscribeFromAll === true', ng-model='user.preferences.emailNotifications.giftedGems', ng-change='set({"preferences.emailNotifications.giftedGems": user.preferences.emailNotifications.giftedGems ? true: false})')
|
||||
span=env.t('giftedGems')
|
||||
|
||||
.checkbox
|
||||
label
|
||||
input(type='checkbox', ng-disabled='user.preferences.emailNotifications.unsubscribeFromAll === true', ng-model='user.preferences.emailNotifications.giftedSubscription', ng-change='set({"preferences.emailNotifications.giftedSubscription": user.preferences.emailNotifications.giftedSubscription ? true: false})')
|
||||
span=env.t('giftedSubscription')
|
||||
|
||||
.checkbox
|
||||
label
|
||||
input(type='checkbox', ng-disabled='user.preferences.emailNotifications.unsubscribeFromAll === true', ng-model='user.preferences.emailNotifications.invitedParty', ng-change='set({"preferences.emailNotifications.invitedParty": user.preferences.emailNotifications.invitedParty ? true: false})')
|
||||
span=env.t('invitedParty')
|
||||
|
||||
.checkbox
|
||||
label
|
||||
input(type='checkbox', ng-disabled='user.preferences.emailNotifications.unsubscribeFromAll === true', ng-model='user.preferences.emailNotifications.invitedGuild', ng-change='set({"preferences.emailNotifications.invitedGuild": user.preferences.emailNotifications.invitedGuild ? true: false})')
|
||||
span=env.t('invitedGuild')
|
||||
|
||||
//.checkbox
|
||||
label
|
||||
input(type='checkbox', ng-disabled='user.preferences.emailNotifications.unsubscribeFromAll === true', ng-model='user.preferences.emailNotifications.remindersToLogin', ng-change='set({"preferences.emailNotifications.remindersToLogin": user.preferences.emailNotifications.remindersToLogin ? true: false})')
|
||||
span=env.t('remindersToLogin')
|
||||
|
||||
.checkbox
|
||||
label
|
||||
input(type='checkbox', ng-disabled='user.preferences.emailNotifications.unsubscribeFromAll === true', ng-model='user.preferences.emailNotifications.importantAnnouncements', ng-change='set({"preferences.emailNotifications.importantAnnouncements": user.preferences.emailNotifications.importantAnnouncements ? true: false})')
|
||||
span=env.t('importantAnnouncements')
|
||||
|
||||
hr
|
||||
|
||||
.checkbox
|
||||
label
|
||||
input(type='checkbox', ng-model='user.preferences.emailNotifications.unsubscribeFromAll', ng-change='set({"preferences.emailNotifications.unsubscribeFromAll": user.preferences.emailNotifications.unsubscribeFromAll ? true: false})')
|
||||
span=env.t('unsubscribeAllEmails')
|
||||
|
||||
small=env.t('unsubscribeAllEmailsText')
|
||||
|
||||
script(id='partials/options.settings.subscription.html',type='text/ng-template')
|
||||
//-h2=env.t('individualSub')
|
||||
.container-fluid(ng-init='_subscription={key:"basic_earned"}')
|
||||
|
||||
@@ -16,7 +16,7 @@ script(type='text/ng-template', id='partials/options.social.challenges.detail.me
|
||||
button.close(type='button', ng-click='$state.go("^")', aria-hidden='true') ×
|
||||
h3 {{obj.profile.name}}
|
||||
.modal-body
|
||||
habitrpg-tasks(main=false)
|
||||
habitrpg-tasks(main=false, modal='true')
|
||||
.modal-footer
|
||||
a.btn.btn-default(ng-click='$state.go("^")')=env.t('close')
|
||||
|
||||
|
||||
@@ -242,6 +242,8 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}')
|
||||
a(ui-sref='options.settings.coupon') Coupon
|
||||
li
|
||||
a(ui-sref='options.settings.subscription')=env.t('subscription')
|
||||
li
|
||||
a(ui-sref='options.settings.notifications')=env.t('notifications')
|
||||
ul.toolbar-submenu(ng-click='expandMenu(null)')
|
||||
li
|
||||
a(href="http://habitrpg.wikia.com/wiki/FAQ", target='_blank')=env.t('FAQ')
|
||||
|
||||
@@ -32,9 +32,9 @@ script(type='text/ng-template', id='modals/member.html')
|
||||
include ../profiles/achievements
|
||||
.modal-footer
|
||||
.btn-group.pull-left(ng-if='::user')
|
||||
button.btn.btn-md.btn-default(ng-show='user.inbox.blocks | contains:profile._id', tooltip=env.t('unblock'), ng-click="user.ops.blockUser({params:{uuid:profile._id}})", tooltip-placement='right')
|
||||
button.btn.btn-md.btn-default(ng-if='user.inbox.blocks | contains:profile._id', tooltip=env.t('unblock'), ng-click="user.ops.blockUser({params:{uuid:profile._id}})", tooltip-placement='right')
|
||||
span.glyphicon.glyphicon-plus
|
||||
button.btn.btn-md.btn-default(ng-hide='profile._id == user._id || user.inbox.blocks | contains:profile._id', tooltip=env.t('block'), ng-click="user.ops.blockUser({params:{uuid:profile._id}})", tooltip-placement='right')
|
||||
button.btn.btn-md.btn-default(ng-if='profile._id != user._id && !profile.contributor.admin && !(user.inbox.blocks | contains:profile._id)', tooltip=env.t('block'), ng-click="user.ops.blockUser({params:{uuid:profile._id}})", tooltip-placement='right')
|
||||
span.glyphicon.glyphicon-ban-circle
|
||||
button.btn.btn-md.btn-default(tooltip=env.t('sendPM'), ng-click="openModal('private-message',{controller:'MemberModalCtrl'})", tooltip-placement='right')
|
||||
span.glyphicon.glyphicon-envelope
|
||||
|
||||
@@ -1,31 +1,54 @@
|
||||
h5 1/30/2015 - HABITRPG BIRTHDAY BASH AND PARTY ROBES! PLUS, LAST CHANCE FOR STARRY KNIGHT ITEM SET, AND WINTER WONDERLAND OUTFITS AND HAIR COLORS!
|
||||
h5 2/3/2015
|
||||
hr
|
||||
tr
|
||||
td
|
||||
.npc_alex.pull-left
|
||||
h5 HabitRPG Birthday Bash
|
||||
p January 31st is HabitRPG's Birthday! All of the NPCs are celebrating, and we've awarded you a bunch of cake for your pets and mounts!
|
||||
tr
|
||||
td
|
||||
h5 Party Robes
|
||||
.shop_armor_special_birthday.pull-right
|
||||
.shop_armor_special_birthday2015.pull-right
|
||||
p Until February 1st only, there are Party Robes available for free in the Rewards store! If this is your first Birthday bash with us, you can find some Absurd Party Robes; if you already got some last year, then you will find the Silly Party Robes.
|
||||
tr
|
||||
td
|
||||
.promo_mystery_201501.pull-left
|
||||
h5 Last Chance for Starry Knight Item Set
|
||||
p Reminder: this is the final day to <a href='https://habitrpg.com/#/options/settings/subscription' target='_blank'>subscribe</a> and receive the Starry Knight Item Set! If you want the Starry Helm or the Starry Armor, now's the time! Thanks so much for your support <3
|
||||
tr
|
||||
td
|
||||
h5 Last Chance for Winter Wonderland Outfits + Hair Colors
|
||||
.promo_winterclasses2015.pull-right
|
||||
p Tomorrow everything will be back to normal in Habitica, so if you still have any remaining Winter Wonderland Items that you want to buy, you'd better do it now! The <a href='https://habitrpg.com/#/options/inventory/seasonalshop' target='_blank'>Seasonal Edition items</a> and <a href='https://habitrpg.com/#/options/profile/avatar' target='_blank'>Hair Colors</a> won't be back until next December, and if the Limited Edition items return they will have increased prices or changed art, so strike while the iron is hot!
|
||||
h5 FEBRUARY BACKGROUNDS REVEALED
|
||||
.background_distant_castle.pull-right
|
||||
p There are three new avatar backgrounds in the <a href='https://habitrpg.com/#/options/profile/backgrounds' target='_blank'>Background Shop</a>! Now your avatar can survey a Distant Castle, toil in the Blacksmithy, or explore a Crystal Cave!
|
||||
p.small.muted by Holseties, Hanztan, and Twitching
|
||||
|
||||
hr
|
||||
a(href='/static/old-news', target='_blank') Read older news
|
||||
|
||||
mixin oldNews
|
||||
h5 2/2/2015
|
||||
tr
|
||||
td
|
||||
h5 February Mystery Box
|
||||
.inventory_present.pull-right
|
||||
p Ooh... What could it be? All Habiticans who are subscribed during the month of February will receive the February Mystery Item Set! It will be revealed on the 24th, so keep your eyes peeled. Thanks for supporting the site <3
|
||||
p.small.muted by Lemoness
|
||||
tr
|
||||
td
|
||||
h5 New Quest Descriptions
|
||||
p We've updated quest descriptions so that when you hover over them, you can now see the Boss or Collection stats and the Rewards that you will gain when you complete the quest!
|
||||
p.small.muted by Blade
|
||||
tr
|
||||
td
|
||||
h5 Spread the Word Challenge Has Ended
|
||||
p The Spread the Word Challenge has ended! Thank you to all the participants. It will be some time before the winners are announced because we have to go over all the entries ourselves. Thanks for your patience!
|
||||
h5 1/30/2015
|
||||
tr
|
||||
td
|
||||
.npc_alex.pull-left
|
||||
h5 HabitRPG Birthday Bash
|
||||
p January 31st is HabitRPG's Birthday! All of the NPCs are celebrating, and we've awarded you a bunch of cake for your pets and mounts!
|
||||
tr
|
||||
td
|
||||
h5 Party Robes
|
||||
.shop_armor_special_birthday.pull-right
|
||||
.shop_armor_special_birthday2015.pull-right
|
||||
p Until February 1st only, there are Party Robes available for free in the Rewards store! If this is your first Birthday bash with us, you can find some Absurd Party Robes; if you already got some last year, then you will find the Silly Party Robes.
|
||||
tr
|
||||
td
|
||||
.promo_mystery_201501.pull-left
|
||||
h5 Last Chance for Starry Knight Item Set
|
||||
p Reminder: this is the final day to <a href='https://habitrpg.com/#/options/settings/subscription' target='_blank'>subscribe</a> and receive the Starry Knight Item Set! If you want the Starry Helm or the Starry Armor, now's the time! Thanks so much for your support <3
|
||||
tr
|
||||
td
|
||||
h5 Last Chance for Winter Wonderland Outfits + Hair Colors
|
||||
.promo_winterclasses2015.pull-right
|
||||
p Tomorrow everything will be back to normal in Habitica, so if you still have any remaining Winter Wonderland Items that you want to buy, you'd better do it now! The <a href='https://habitrpg.com/#/options/inventory/seasonalshop' target='_blank'>Seasonal Edition items</a> and <a href='https://habitrpg.com/#/options/profile/avatar' target='_blank'>Hair Colors</a> won't be back until next December, and if the Limited Edition items return they will have increased prices or changed art, so strike while the iron is hot!
|
||||
h5 1/26/2015
|
||||
tr
|
||||
td
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
li(bindonce='list', bo-id='"task-"+task.id', ng-repeat='task in obj[list.type+"s"]', class='task {{Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main)}}', ng-click='spell && (list.type != "reward") && castEnd(task, "task", $event)', ng-class='{"cast-target":spell && (list.type != "reward")}', popover-trigger='mouseenter', data-popover-html="{{task.notes | markdown}}", popover-placement="top", popover-append-to-body='true', ng-show='shouldShow(task, list, user.preferences)')
|
||||
li(bindonce='list', bo-id='"task-"+task.id', ng-repeat='task in obj[list.type+"s"]', class='task {{Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main)}}', ng-click='spell && (list.type != "reward") && castEnd(task, "task", $event)', ng-class='{"cast-target":spell && (list.type != "reward")}', popover-trigger='mouseenter', data-popover-html="{{task.notes | markdown}}", popover-placement="top", popover-append-to-body='{{::modal ? "false":"true"}}', ng-show='shouldShow(task, list, user.preferences)')
|
||||
// right-hand side control buttons
|
||||
.task-meta-controls
|
||||
|
||||
|
||||
Reference in New Issue
Block a user