diff --git a/clientapp/app.js b/clientapp/app.js
index 7f8fab2..966dab4 100644
--- a/clientapp/app.js
+++ b/clientapp/app.js
@@ -9,6 +9,7 @@ var StanzaIO = require('stanza.io');
var AppState = require('./models/state');
var MeModel = require('./models/me');
+var LdapUsers = require('./models/ldapUsers');
var MainView = require('./views/main');
var Router = require('./router');
var Storage = require('./storage');
@@ -32,8 +33,8 @@ module.exports = {
return;
}
- config = JSON.parse(config);
- config.useStreamManagement = true;
+ app.config = JSON.parse(config);
+ app.config.useStreamManagement = true;
_.extend(this, Backbone.Events);
@@ -51,11 +52,11 @@ module.exports = {
app.mucInfos = [];
},
function (cb) {
- app.storage.profiles.get(config.jid, function (err, res) {
+ app.storage.profiles.get(app.config.jid, function (err, res) {
if (res) {
profile = res;
- profile.jid = {full: config.jid, bare: config.jid};
- config.rosterVer = res.rosterVer;
+ profile.jid = {full: app.config.jid, bare: app.config.jid};
+ app.config.rosterVer = res.rosterVer;
}
cb();
});
@@ -70,7 +71,7 @@ module.exports = {
}
};
- self.api = window.client = StanzaIO.createClient(config);
+ self.api = window.client = StanzaIO.createClient(app.config);
client.use(pushNotifications);
xmppEventHandlers(self.api, self);
@@ -121,6 +122,8 @@ module.exports = {
});
self.view.render();
+ app.ldapUsers = new LdapUsers();
+
if (me.contacts.length) {
start();
} else {
diff --git a/clientapp/libraries/jquery-impromptu.js b/clientapp/libraries/jquery-impromptu.js
new file mode 100644
index 0000000..ea04a34
--- /dev/null
+++ b/clientapp/libraries/jquery-impromptu.js
@@ -0,0 +1,4 @@
+/*! jQuery-Impromptu - v6.0.0 - 2014-12-27
+* http://trentrichardson.com/Impromptu
+* Copyright (c) 2014 Trent Richardson; Licensed MIT */
+(function(t,e){"function"==typeof define&&define.amd?define(["jquery"],e):e(t.jQuery)})(this,function(t){"use strict";var e=function(t,i){var n=this;return n.id=e.count++,e.lifo.push(n),t&&n.open(t,i),n};e.defaults={prefix:"jqi",classes:{box:"",fade:"",prompt:"",form:"",close:"",title:"",message:"",buttons:"",button:"",defaultButton:""},title:"",closeText:"×",buttons:{Ok:!0},loaded:function(){},submit:function(){},close:function(){},statechanging:function(){},statechanged:function(){},opacity:.6,zIndex:999,overlayspeed:"slow",promptspeed:"fast",show:"fadeIn",hide:"fadeOut",focus:0,defaultButton:0,useiframe:!1,top:"15%",position:{container:null,x:null,y:null,arrow:null,width:null},persistent:!0,timeout:0,states:{},state:{name:null,title:"",html:"",buttons:{Ok:!0},focus:0,defaultButton:0,position:{container:null,x:null,y:null,arrow:null,width:null},submit:function(){return!0}}},e.setDefaults=function(i){e.defaults=t.extend({},e.defaults,i)},e.setStateDefaults=function(i){e.defaults.state=t.extend({},e.defaults.state,i)},e.count=0,e.lifo=[],e.getLast=function(){var t=e.lifo.length;return t>0?e.lifo[t-1]:!1},e.removeFromStack=function(t){for(var i=e.lifo.length-1;i>=0;i--)if(e.lifo[i].id===t)return e.lifo.splice(i,1)[0]},e.prototype={id:null,open:function(i,n){var o=this;o.options=t.extend({},e.defaults,n),o.timeout&&clearTimeout(o.timeout),o.timeout=!1;var s=o.options,a=t(document.body),r=t(window),u='
';u+=s.useiframe&&t("object, applet").length>0?'
':'
',u+='
"+"
",o.jqib=t(u).appendTo(a),o.jqi=o.jqib.children("."+s.prefix),o.jqif=o.jqib.children("."+s.prefix+"fade"),i.constructor===String&&(i={state0:{title:s.title,html:i,buttons:s.buttons,position:s.position,focus:s.focus,defaultButton:s.defaultButton,submit:s.submit}}),o.options.states={};var f,l;for(f in i)l=t.extend({},e.defaults.state,{name:f},i[f]),o.addState(l.name,l),""===o.currentStateName&&(o.currentStateName=l.name);o.jqi.on("click","."+s.prefix+"buttons button",function(){var e=t(this),i=e.parents("."+s.prefix+"state"),n=o.options.states[i.data("jqi-name")],a=i.children("."+s.prefix+"message"),r=n.buttons[e.text()]||n.buttons[e.html()],u={};if(void 0===r)for(var f in n.buttons)(n.buttons[f].title===e.text()||n.buttons[f].title===e.html())&&(r=n.buttons[f].value);t.each(o.jqi.children("form").serializeArray(),function(t,e){void 0===u[e.name]?u[e.name]=e.value:typeof u[e.name]===Array||"object"==typeof u[e.name]?u[e.name].push(e.value):u[e.name]=[u[e.name],e.value]});var l=new t.Event("impromptu:submit");l.stateName=n.name,l.state=i,i.trigger(l,[r,a,u]),l.isDefaultPrevented()||o.close(!0,r,a,u)});var p=function(){if(s.persistent){var e=(""+s.top).indexOf("%")>=0?r.height()*(parseInt(s.top,10)/100):parseInt(s.top,10),i=parseInt(o.jqi.css("top").replace("px",""),10)-e;t("html,body").animate({scrollTop:i},"fast",function(){var t=0;o.jqib.addClass(s.prefix+"warning");var e=setInterval(function(){o.jqib.toggleClass(s.prefix+"warning"),t++>1&&(clearInterval(e),o.jqib.removeClass(s.prefix+"warning"))},100)})}else o.close(!0)},d=function(e){var i=window.event?event.keyCode:e.keyCode;if(27===i&&p(),13===i){var n=o.getCurrentState().find("."+s.prefix+"defaultbutton"),a=t(e.target);a.is("textarea,."+s.prefix+"button")===!1&&n.length>0&&(e.preventDefault(),n.click())}if(9===i){var r=t("input,select,textarea,button",o.getCurrentState()),u=!e.shiftKey&&e.target===r[r.length-1],f=e.shiftKey&&e.target===r[0];if(u||f)return setTimeout(function(){if(r){var t=r[f===!0?r.length-1:0];t&&t.focus()}},10),!1}};return o.position(),o.style(),o._windowResize=function(t){o.position(t)},r.resize({animate:!1},o._windowResize),o.jqif.click(p),o.jqi.find("."+s.prefix+"close").click(function(){o.close()}),o.jqib.on("keydown",d).on("impromptu:loaded",s.loaded).on("impromptu:close",s.close).on("impromptu:statechanging",s.statechanging).on("impromptu:statechanged",s.statechanged),o.jqif[s.show](s.overlayspeed),o.jqi[s.show](s.promptspeed,function(){var t=o.jqi.find("."+s.prefix+"states ."+s.prefix+"state").eq(0);o.goToState(t.data("jqi-name")),o.jqib.trigger("impromptu:loaded")}),s.timeout>0&&(o.timeout=setTimeout(function(){o.close(!0)},s.timeout)),o},close:function(i,n,o,s){var a=this;return e.removeFromStack(a.id),a.timeout&&(clearTimeout(a.timeout),a.timeout=!1),a.jqib&&a.jqib[a.options.hide]("fast",function(){a.jqib.trigger("impromptu:close",[n,o,s]),a.jqib.remove(),t(window).off("resize",a._windowResize),"function"==typeof i&&i()}),a.currentStateName="",a},addState:function(i,n,o){var s,a,r,u,f,l=this,p="",d=null,c="",m="",h=l.options,v=t("."+h.prefix+"states"),g=[],b=0;if(n=t.extend({},e.defaults.state,{name:i},n),null!==n.position.arrow&&(c=''),n.title&&""!==n.title&&(m=''+n.title+"
"),s=n.html,"function"==typeof n.html&&(s="Error: html function must return text"),p+=''+c+m+'
'+s+"
"+'
",t.isArray(n.buttons))g=n.buttons;else if(t.isPlainObject(n.buttons))for(r in n.buttons)n.buttons.hasOwnProperty(r)&&g.push({title:r,value:n.buttons[r]});for(b=0,f=g.length;f>b;b++)u=g[b],a=n.focus===b||isNaN(n.focus)&&n.defaultButton===b?h.prefix+"defaultbutton "+h.classes.defaultButton:"",p+='";return p+="
",d=t(p),d.on("impromptu:submit",n.submit),void 0!==o?v.find('[data-jqi-name="'+o+'"]').after(d):v.append(d),l.options.states[i]=n,d},removeState:function(t,e){var i=this,n=i.getState(t),o=function(){n.remove()};return 0===n.length?!1:("none"!==n.css("display")?void 0!==e&&i.getState(e).length>0?i.goToState(e,!1,o):n.next().length>0?i.nextState(o):n.prev().length>0?i.prevState(o):i.close():n.slideUp("slow",o),!0)},getApi:function(){return this},getBox:function(){return this.jqib},getPrompt:function(){return this.jqi},getState:function(t){return this.jqi.find('[data-jqi-name="'+t+'"]')},getCurrentState:function(){return this.getState(this.getCurrentStateName())},getCurrentStateName:function(){return this.currentStateName},position:function(e){var i=this,n=t.fx.off,o=i.getCurrentState(),s=i.options.states[o.data("jqi-name")],a=s?s.position:void 0,r=t(window),u=document.body.scrollHeight,f=t(window).height(),l=(t(document).height(),u>f?u:f),p=parseInt(r.scrollTop(),10)+((""+i.options.top).indexOf("%")>=0?f*(parseInt(i.options.top,10)/100):parseInt(i.options.top,10));if(void 0!==e&&e.data.animate===!1&&(t.fx.off=!0),i.jqib.css({position:"absolute",height:l,width:"100%",top:0,left:0,right:0,bottom:0}),i.jqif.css({position:"fixed",height:l,width:"100%",top:0,left:0,right:0,bottom:0}),a&&a.container){var d=t(a.container).offset();t.isPlainObject(d)&&void 0!==d.top&&(i.jqi.css({position:"absolute"}),i.jqi.animate({top:d.top+a.y,left:d.left+a.x,marginLeft:0,width:void 0!==a.width?a.width:null}),p=d.top+a.y-((""+i.options.top).indexOf("%")>=0?f*(parseInt(i.options.top,10)/100):parseInt(i.options.top,10)),t("html,body").animate({scrollTop:p},"slow","swing",function(){}))}else a&&a.width?(i.jqi.css({position:"absolute",left:"50%"}),i.jqi.animate({top:a.y||p,left:a.x||"50%",marginLeft:-1*(a.width/2),width:a.width})):i.jqi.css({position:"absolute",top:p,left:"50%",marginLeft:-1*(i.jqi.outerWidth(!1)/2)});void 0!==e&&e.data.animate===!1&&(t.fx.off=n)},style:function(){var t=this;t.jqif.css({zIndex:t.options.zIndex,display:"none",opacity:t.options.opacity}),t.jqi.css({zIndex:t.options.zIndex+1,display:"none"}),t.jqib.css({zIndex:t.options.zIndex})},goToState:function(e,i,n){var o=this,s=(o.jqi,o.options),a=o.getState(e),r=s.states[a.data("jqi-name")],u=new t.Event("impromptu:statechanging"),f=o.options;if(void 0!==r){if("function"==typeof r.html){var l=r.html;a.find("."+f.prefix+"message ").html(l())}"function"==typeof i&&(n=i,i=!1),o.jqib.trigger(u,[o.getCurrentStateName(),e]),!u.isDefaultPrevented()&&a.length>0&&(o.jqi.find("."+f.prefix+"parentstate").removeClass(f.prefix+"parentstate"),i?(o.jqi.find("."+f.prefix+"substate").not(a).slideUp(s.promptspeed).removeClass("."+f.prefix+"substate").find("."+f.prefix+"arrow").hide(),o.jqi.find("."+f.prefix+"state:visible").addClass(f.prefix+"parentstate"),a.addClass(f.prefix+"substate")):o.jqi.find("."+f.prefix+"state").not(a).slideUp(s.promptspeed).find("."+f.prefix+"arrow").hide(),o.currentStateName=r.name,a.slideDown(s.promptspeed,function(){var i=t(this);"string"==typeof r.focus?i.find(r.focus).eq(0).focus():i.find("."+f.prefix+"defaultbutton").focus(),i.find("."+f.prefix+"arrow").show(s.promptspeed),"function"==typeof n&&o.jqib.on("impromptu:statechanged",n),o.jqib.trigger("impromptu:statechanged",[e]),"function"==typeof n&&o.jqib.off("impromptu:statechanged",n)}),i||o.position())}return a},nextState:function(t){var e=this,i=e.getCurrentState().next();return i.length>0&&e.goToState(i.data("jqi-name"),t),i},prevState:function(t){var e=this,i=e.getCurrentState().prev();return i.length>0&&e.goToState(i.data("jqi-name"),t),i}},t.prompt=function(t,i){var n=new e(t,i);return n.jqi},t.each(e,function(e,i){t.prompt[e]=i}),t.each(e.prototype,function(i){t.prompt[i]=function(){var t=e.getLast();return t&&"function"==typeof t[i]?t[i].apply(t,arguments):void 0}}),t.fn.prompt=function(e){void 0===e&&(e={}),void 0===e.withDataAndEvents&&(e.withDataAndEvents=!1),t.prompt(t(this).clone(e.withDataAndEvents).html(),e)},window.Impromptu=e});
diff --git a/clientapp/models/ldapUser.js b/clientapp/models/ldapUser.js
new file mode 100644
index 0000000..ca23542
--- /dev/null
+++ b/clientapp/models/ldapUser.js
@@ -0,0 +1,79 @@
+/*global app, me, client, URL*/
+"use strict";
+
+var _ = require('underscore');
+var HumanModel = require('human-model');
+
+exports.ldapData = function (data) {
+ if (!data) data = {};
+ data.uid = app.config.jid.substring(0, app.config.jid.indexOf('@'));
+ data.password = app.config.credentials.password;
+
+ return data;
+}
+
+exports.user = HumanModel.define({
+ initialize: function (attrs) {
+
+ },
+ type: 'ldapUser',
+ props: {
+ id: ['string', true, false],
+ cn: ['string', true, false],
+ sn: ['string', false, ''],
+ givenName: ['string', false, ''],
+ displayName: ['string', false, ''],
+ mail: ['string', false, ''],
+ objectClass: ['array', false, []]
+ },
+ save: function(userInfos, cb) {
+ var change = false;
+ var oldValues = {};
+ for (var property in userInfos) {
+ if (userInfos.hasOwnProperty(property) && this[property] !== undefined) {
+ oldValues[property] = this[property];
+ if (this[property] != userInfos[property])
+ change = true;
+ }
+ }
+
+ if (!change)
+ return;
+
+ var self = this;
+ $.post('/ldap/user/' + this.id, exports.ldapData(userInfos), function(result) {
+ result = JSON.parse(result);
+
+ if (result) {
+ for (var property in userInfos) {
+ if (userInfos.hasOwnProperty(property) && self[property] !== undefined) {
+ self[property] = userInfos[property];
+ }
+ }
+ }
+ else {
+ for (var property in oldValues) {
+ if (oldValues.hasOwnProperty(property) && self[property] !== undefined) {
+ self[property] = '';
+ self[property] = oldValues[property];
+ }
+ }
+ }
+
+ cb();
+ });
+ },
+ changePassword: function(newPassword) {
+ var self = this;
+
+ $.post('/ldap/user/' + this.id + '/password', exports.ldapData({newPassword: newPassword}), function(result) {
+ result = JSON.parse(result);
+ if (!result) {
+ app.ldapUsers.fetch();
+ }
+ });
+ },
+ meIsAdmin: function() {
+ return me.isAdmin;
+ }
+});
diff --git a/clientapp/models/ldapUsers.js b/clientapp/models/ldapUsers.js
new file mode 100644
index 0000000..6641266
--- /dev/null
+++ b/clientapp/models/ldapUsers.js
@@ -0,0 +1,71 @@
+/*global app, client*/
+"use strict";
+
+var BaseCollection = require('./baseCollection');
+var ldapUser = require('./ldapUser');
+
+module.exports = BaseCollection.extend({
+ type: 'ldapUsers',
+ model: ldapUser.user,
+ comparator: function (model1, model2) {
+ var name1 = model1.displayName.toLowerCase();
+ var name2 = model2.displayName.toLowerCase();
+ if (name1 === name2) {
+ return 0;
+ }
+ if (name1 < name2) {
+ return -1;
+ }
+ return 1;
+ },
+ initialize: function (model, options) {
+ this.bind('change', this.sort, this);
+ },
+ fetch: function (cb) {
+ var self = this;
+
+ $.post('/ldap/users', ldapUser.ldapData(), function(users) {
+ var toRemove = [];
+ for ( var i = 0; i < self.models.length; i++) {
+ toRemove.push(self.models[i].id);
+ }
+
+ users = JSON.parse(users);
+ users.forEach(function(user) {
+ var existing = self.get(user.id);
+ if (!existing) {
+ self.add(user);
+ }
+
+ var index = toRemove.indexOf(user.id);
+ if (index > -1) {
+ toRemove.splice(index, 1);
+ }
+ });
+
+ self.remove(toRemove);
+
+ if (cb) cb();
+ });
+ },
+ addUser: function (id) {
+ var self = this;
+
+ $.post('/ldap/users/add', ldapUser.ldapData({newUid: id}), function(result) {
+ result = JSON.parse(result);
+ if (result) {
+ self.fetch();
+ }
+ });
+ },
+ deleteUser: function (id) {
+ var self = this;
+
+ $.post('/ldap/users/delete', ldapUser.ldapData({removeUid: id}), function(result) {
+ result = JSON.parse(result);
+ if (result) {
+ self.fetch();
+ }
+ });
+ }
+});
diff --git a/clientapp/models/me.js b/clientapp/models/me.js
index a9ee682..9daf994 100644
--- a/clientapp/models/me.js
+++ b/clientapp/models/me.js
@@ -76,6 +76,12 @@ module.exports = HumanModel.define({
return this.soundEnabled ? "primary" : "secondary";
}
},
+ isAdmin: {
+ deps: ['jid'],
+ fn: function () {
+ return this.jid.local === SERVER_CONFIG.admin ? 'meIsAdmin' : '';
+ }
+ }
},
setActiveContact: function (jid) {
var prev = this.getContact(this._activeContact);
@@ -105,6 +111,9 @@ module.exports = HumanModel.define({
self.avatar = avatar.uri;
});
},
+ hasLdapUsers: function () {
+ return app.ldapUsers.length > 0 ? 'hasLdapUsers' : '';
+ },
setSoundNotification: function(enable) {
this.soundEnabled = enable;
},
diff --git a/clientapp/pages/settings.js b/clientapp/pages/settings.js
index 3da5822..a83d0f3 100644
--- a/clientapp/pages/settings.js
+++ b/clientapp/pages/settings.js
@@ -4,13 +4,15 @@
var crypto = require('crypto');
var BasePage = require('./base');
var templates = require('../templates');
-
+var LDAPUserItem = require('../views/ldapUserItem');
module.exports = BasePage.extend({
template: templates.pages.settings,
classBindings: {
shouldAskForAlertsPermission: '.enableAlerts',
- soundEnabledClass: '.soundNotifs'
+ soundEnabledClass: '.soundNotifs',
+ hasLdapUsers: '#ldapSettings',
+ isAdmin: '#newLdapUser'
},
srcBindings: {
avatar: '#avatarChanger img'
@@ -24,13 +26,19 @@ module.exports = BasePage.extend({
'click .soundNotifs': 'handleSoundNotifs',
'dragover': 'handleAvatarChangeDragOver',
'drop': 'handleAvatarChange',
- 'change #uploader': 'handleAvatarChange'
+ 'change #uploader': 'handleAvatarChange',
+ 'keydown #newLdapUser': 'addLdapUser',
},
initialize: function (spec) {
- this.render();
+ this.listenTo(this, 'deleteLdapUser', this.deleteLdapUser);
+ var self = this;
+ app.ldapUsers.fetch(function () {
+ self.render();
+ });
},
render: function () {
this.renderAndBind();
+ this.renderCollection(app.ldapUsers, LDAPUserItem, this.$('#ldapUsers'));
return this;
},
enableAlerts: function () {
@@ -93,4 +101,14 @@ module.exports = BasePage.extend({
handleSoundNotifs: function (e) {
this.model.setSoundNotification(!this.model.soundEnabled);
},
+ addLdapUser: function (e) {
+ if (e.which === 13 && !e.shiftKey) {
+ var id = e.target.value;
+ e.target.value = '';
+ app.ldapUsers.addUser(id);
+ }
+ },
+ deleteLdapUser: function (id) {
+ app.ldapUsers.deleteUser(id);
+ }
});
diff --git a/clientapp/templates.js b/clientapp/templates.js
index b70fb14..8b05221 100644
--- a/clientapp/templates.js
+++ b/clientapp/templates.js
@@ -207,6 +207,15 @@ exports.includes.embeds = function anonymous(locals) {
return buf.join("");
};
+// ldapUserItem.jade compiled template
+exports.includes.ldapUserItem = function anonymous(locals) {
+ var buf = [];
+ with (locals || {}) {
+ buf.push('Display NameFirst NameLast NameEmail
');
+ }
+ return buf.join("");
+};
+
// message.jade compiled template
exports.includes.message = function anonymous(locals) {
var buf = [];
@@ -512,7 +521,7 @@ exports.pages.groupchat = function anonymous(locals) {
exports.pages.settings = function anonymous(locals) {
var buf = [];
with (locals || {}) {
- buf.push('Settings
Change Avatar
Drag and drop a new avatar here
Desktop Integration
');
+ buf.push('Settings
Change Avatar
Drag and drop a new avatar here
Desktop Integration
');
}
return buf.join("");
};
diff --git a/clientapp/templates/includes/ldapUserItem.jade b/clientapp/templates/includes/ldapUserItem.jade
new file mode 100644
index 0000000..24157a4
--- /dev/null
+++ b/clientapp/templates/includes/ldapUserItem.jade
@@ -0,0 +1,14 @@
+li.ldapUser
+ span.name
+ span.delete.fa.fa-trash
+ span.fa.fa-plus
+ .wrap
+ span.inputLabel Display Name
+ input(type="text").displayName
+ span.inputLabel First Name
+ input(type="text").givenName
+ span.inputLabel Last Name
+ input(type="text").sn
+ span.inputLabel Email
+ input(type="text").mail
+ button.primary.small.changePassword Change Password
diff --git a/clientapp/templates/pages/settings.jade b/clientapp/templates/pages/settings.jade
index 4caa4dd..f6f6cd4 100644
--- a/clientapp/templates/pages/settings.jade
+++ b/clientapp/templates/pages/settings.jade
@@ -20,5 +20,10 @@ section.page.main
button.primary.installFirefox Install app
button.soundNotifs sound notifications
+ div#ldapSettings
+ h4 LDAP settings
+ ul#ldapUsers
+ input(type="text", placeholder="add a ldap user")#newLdapUser
+
div
button.logout Logout
diff --git a/clientapp/views/ldapUserItem.js b/clientapp/views/ldapUserItem.js
new file mode 100644
index 0000000..c2985c7
--- /dev/null
+++ b/clientapp/views/ldapUserItem.js
@@ -0,0 +1,140 @@
+/*global $, app, me*/
+"use strict";
+
+var _ = require('underscore');
+var HumanView = require('human-view');
+var templates = require('../templates');
+
+
+module.exports = HumanView.extend({
+ template: templates.includes.ldapUserItem,
+ classBindings: {
+ meIsAdmin: '.delete, .fa-plus, .fa-minus, .wrap'
+ },
+ textBindings: {
+ id: '.name'
+ },
+ inputBindings: {
+ displayName: '.displayName',
+ givenName: '.givenName',
+ sn: '.sn',
+ mail: '.mail'
+ },
+ events: {
+ 'click .fa-plus': 'handleDisplayUser',
+ 'click .fa-minus': 'handleDisplayUser',
+ 'click .delete': 'deleteUser',
+ 'blur input': 'saveInfos',
+ 'focus input': 'handleInputFocus',
+ 'select input': 'handleInputSelect',
+ 'click .changePassword': 'changePassword'
+ },
+ render: function () {
+ this.renderAndBind();
+ return this;
+ },
+ handleDisplayUser: function (e) {
+ var icon = $(e.target);
+ var wrap = $(e.delegateTarget).find('.wrap');
+
+ if (icon.hasClass('fa-plus')) {
+ wrap.show();
+ icon.removeClass('fa-plus');
+ icon.addClass('fa-minus');
+ } else {
+ wrap.hide();
+ icon.removeClass('fa-minus');
+ icon.addClass('fa-plus');
+ }
+
+ e.preventDefault();
+ },
+ handleInputFocus: function (e) {
+ this.inputWithFocus = e.target;
+ },
+ handleInputSelect: function (e) {
+ this.inputWithSelect = e.target;
+ },
+ saveInfos: function (e) {
+ var input = e.target;
+ var classes = $(input).attr('class');
+
+ var userInfos = {};
+ userInfos[classes] = input.value;
+
+ var ldapUser = this.model;
+ if (userInfos[classes] == ldapUser[classes])
+ return;
+
+ this.inputWithFocus = null;
+ this.inputWithSelect = null;
+
+ var self = this;
+ ldapUser.save(userInfos, function () {
+ if (self.inputWithSelect) {
+ $(self.inputWithSelect).select();
+ self.inputWithSelect = null;
+ self.inputWithFocus = null;
+ }
+ else if (self.inputWithFocus) {
+ $(self.inputWithFocus).focus();
+ self.inputWithFocus = null;
+ }
+ });
+ },
+ deleteUser: function (e) {
+ var self = this;
+ var ldapUser = this.model;
+ $.prompt('Are you sure you want to remove ' + ldapUser.id + '?', {
+ title: 'Remove User',
+ buttons: { "Yes": true, "Cancel": false },
+ persistent: true,
+ submit:function (e, v, m, f) {
+ if (v) {
+ self.parent.trigger('deleteLdapUser', ldapUser.id);
+ }
+ }
+ });
+
+ e.preventDefault();
+ },
+ changePassword: function (e) {
+ var ldapUser = this.model;
+ $.prompt({
+ state0: {
+ title: 'Change password',
+ html:'
',
+ buttons: { "Ok": true, "Cancel": false },
+ focus: "input[name='newPassword1']",
+ persistent: true,
+ submit:function (e, v, m, f) {
+ if (v) {
+ if (f.newPassword1 != '') {
+ e.preventDefault();
+ $.prompt.goToState('state1');
+ return false;
+ } else {
+ $.prompt('Password can not be an empty string.', { title: 'Change password' });
+ }
+ }
+ }
+ },
+ state1: {
+ title: 'Change password',
+ html:'
',
+ buttons: { "Ok": true, "Cancel": false },
+ focus: "input[name='newPassword2']",
+ persistent: true,
+ submit:function (e, v, m, f) {
+ if (v) {
+ if (f.newPassword1 == f.newPassword2) {
+ ldapUser.changePassword(f.newPassword2);
+ } else {
+ $.prompt('the password confirmation must match your password.', { title: 'Change password' });
+ }
+ }
+ }
+ }
+ });
+ }
+});
diff --git a/dev_config.json b/dev_config.json
index 2189e53..b99216c 100644
--- a/dev_config.json
+++ b/dev_config.json
@@ -16,5 +16,13 @@
"muc": "chat.localhost",
"startup": "",
"admin": ""
+ },
+ "ldap": {
+ "address": "127.0.0.1",
+ "user": "cn=admin,dc=example.com",
+ "password": "password",
+ "base": "ou=users,dc=example.com",
+ "filter": "objectClass=person",
+ "group": "cn=mygroup,ou=groups,dc=example.com"
}
}
diff --git a/package.json b/package.json
index ee58e05..b564550 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
"attachmediastream": "1.0.1",
"backbone": "1.0.0",
"bluebird": "^2.3.2",
+ "body-parser": "1.12.0",
"bows": "0.3.0",
"browserify": "4.x",
"compression": "1.2.2",
@@ -23,6 +24,7 @@
"human-view": "1.8.0",
"jade": "1.8.2",
"jxt": "^2.7.0",
+ "LDAP": "1.2.1",
"moonboots-express": "2.x",
"node-uuid": "^1.4.1",
"notify.js": "0.0.3",
diff --git a/public/css/jquery-impromptu.css b/public/css/jquery-impromptu.css
new file mode 100644
index 0000000..50c88da
--- /dev/null
+++ b/public/css/jquery-impromptu.css
@@ -0,0 +1,124 @@
+/*! jQuery-Impromptu - v6.0.0 - 2014-12-27
+* http://trentrichardson.com/Impromptu
+* Copyright (c) 2014 Trent Richardson; Licensed MIT */
+.jqifade{
+ position: absolute;
+ background-color: #777777;
+}
+div.jqi{
+ width: 400px;
+ max-width:90%;
+ font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
+ position: absolute;
+ background-color: #ffffff;
+ font-size: 11px;
+ text-align: left;
+ border: solid 1px #eeeeee;
+ border-radius: 6px;
+ -moz-border-radius: 6px;
+ -webkit-border-radius: 6px;
+ padding: 7px;
+}
+div.jqi .jqicontainer{
+}
+div.jqi .jqiclose{
+ position: absolute;
+ top: 4px; right: -2px;
+ width: 18px;
+ cursor: default;
+ color: #bbbbbb;
+ font-weight: bold;
+}
+div.jqi .jqistate{
+ background-color: #fff;
+}
+div.jqi .jqititle{
+ padding: 5px 10px;
+ font-size: 16px;
+ line-height: 20px;
+ border-bottom: solid 1px #eeeeee;
+}
+div.jqi .jqimessage{
+ padding: 10px;
+ line-height: 20px;
+ color: #444444;
+ overflow: auto;
+}
+div.jqi .jqibuttons{
+ text-align: right;
+ margin: 0 -7px -7px -7px;
+ border-top: solid 1px #e4e4e4;
+ background-color: #f4f4f4;
+ border-radius: 0 0 6px 6px;
+ -moz-border-radius: 0 0 6px 6px;
+ -webkit-border-radius: 0 0 6px 6px;
+}
+div.jqi .jqibuttons button{
+ margin: 0;
+ padding: 15px 20px;
+ background-color: transparent;
+ font-weight: normal;
+ border: none;
+ border-left: solid 1px #e4e4e4;
+ color: #777;
+ font-weight: bold;
+ font-size: 12px;
+}
+div.jqi .jqibuttons button.jqidefaultbutton{
+ color: #489afe;
+}
+div.jqi .jqibuttons button:hover,
+div.jqi .jqibuttons button:focus{
+ color: #287ade;
+ outline: none;
+}
+.jqiwarning .jqi .jqibuttons{
+ background-color: #b95656;
+}
+
+/* sub states */
+div.jqi .jqiparentstate::after{
+ background-color: #777;
+ opacity: 0.6;
+ filter: alpha(opacity=60);
+ content: '';
+ position: absolute;
+ top:0;left:0;bottom:0;right:0;
+ border-radius: 6px;
+ -moz-border-radius: 6px;
+ -webkit-border-radius: 6px;
+}
+div.jqi .jqisubstate{
+ position: absolute;
+ top:0;
+ left: 20%;
+ width: 60%;
+ padding: 7px;
+ border: solid 1px #eeeeee;
+ border-top: none;
+ border-radius: 0 0 6px 6px;
+ -moz-border-radius: 0 0 6px 6px;
+ -webkit-border-radius: 0 0 6px 6px;
+}
+div.jqi .jqisubstate .jqibuttons button{
+ padding: 10px 18px;
+}
+
+/* arrows for tooltips/tours */
+.jqi .jqiarrow{ position: absolute; height: 0; width:0; line-height: 0; font-size: 0; border: solid 10px transparent;}
+
+.jqi .jqiarrowtl{ left: 10px; top: -20px; border-bottom-color: #ffffff; }
+.jqi .jqiarrowtc{ left: 50%; top: -20px; border-bottom-color: #ffffff; margin-left: -10px; }
+.jqi .jqiarrowtr{ right: 10px; top: -20px; border-bottom-color: #ffffff; }
+
+.jqi .jqiarrowbl{ left: 10px; bottom: -20px; border-top-color: #ffffff; }
+.jqi .jqiarrowbc{ left: 50%; bottom: -20px; border-top-color: #ffffff; margin-left: -10px; }
+.jqi .jqiarrowbr{ right: 10px; bottom: -20px; border-top-color: #ffffff; }
+
+.jqi .jqiarrowlt{ left: -20px; top: 10px; border-right-color: #ffffff; }
+.jqi .jqiarrowlm{ left: -20px; top: 50%; border-right-color: #ffffff; margin-top: -10px; }
+.jqi .jqiarrowlb{ left: -20px; bottom: 10px; border-right-color: #ffffff; }
+
+.jqi .jqiarrowrt{ right: -20px; top: 10px; border-left-color: #ffffff; }
+.jqi .jqiarrowrm{ right: -20px; top: 50%; border-left-color: #ffffff; margin-top: -10px; }
+.jqi .jqiarrowrb{ right: -20px; bottom: 10px; border-left-color: #ffffff; }
diff --git a/public/css/otalk.css b/public/css/otalk.css
index ecc139d..d391584 100644
--- a/public/css/otalk.css
+++ b/public/css/otalk.css
@@ -1594,6 +1594,12 @@ button.secondary:hover:not(:disabled) {
right: 200px;
z-index: 101;
}
+.main #ldapSettings {
+ display: none;
+}
+.main #ldapSettings.hasLdapUsers {
+ display: block;
+}
.main > div {
padding: 20px;
border-bottom: 1px solid #e0e0e0;
@@ -1620,6 +1626,66 @@ button.secondary:hover:not(:disabled) {
.main > div .soundNotifs.secondary:before {
content: 'Enable ';
}
+.main > div #newLdapUser {
+ display: none;
+ width: 500px;
+ height: 20px;
+ padding: 3px;
+ font-size: 12px;
+}
+.main > div #newLdapUser.meIsAdmin {
+ display: inline-block;
+}
+.main > div #ldapUsers {
+ border-radius: 3px;
+ border: 1px solid #e0e0e0;
+ background: #f9f9f9;
+ list-style-type: none;
+ padding: 5px 5px 5px 8px;
+ line-height: 20px;
+}
+.main > div #ldapUsers .fa-plus,
+.main > div #ldapUsers .fa-minus,
+.main > div #ldapUsers .delete {
+ display: none;
+ margin-left: 5px;
+ cursor: pointer;
+ color: #000;
+}
+.main > div #ldapUsers .fa-plus:hover,
+.main > div #ldapUsers .fa-minus:hover,
+.main > div #ldapUsers .delete:hover {
+ color: #94b021;
+}
+.main > div #ldapUsers .fa-plus.meIsAdmin,
+.main > div #ldapUsers .fa-minus.meIsAdmin,
+.main > div #ldapUsers .delete.meIsAdmin {
+ display: inline-block;
+}
+.main > div #ldapUsers .delete:hover {
+ color: #f00;
+}
+.main > div #ldapUsers .wrap {
+ background: #f0f0f0;
+ border-radius: 3px;
+ border: 1px solid #e0e0e0;
+ padding: 5px;
+ font-size: 12px;
+ margin-bottom: 5px;
+ width: 502px;
+}
+.main > div #ldapUsers .wrap.meIsAdmin {
+ display: none;
+}
+.main > div #ldapUsers .wrap input {
+ width: 500px;
+ height: 20px;
+ padding: 3px;
+}
+.main > div #ldapUsers .wrap .changePassword {
+ margin-top: 4px;
+ margin-right: 4px;
+}
.uploadRegion {
padding: 15px;
-moz-border-radius: 3px;
diff --git a/public/css/pages/settings.styl b/public/css/pages/settings.styl
index fe0a5c5..9ce2b29 100644
--- a/public/css/pages/settings.styl
+++ b/public/css/pages/settings.styl
@@ -23,6 +23,11 @@
right: 200px
z-index: 101
+ #ldapSettings
+ display: none
+ &.hasLdapUsers
+ display: block
+
> div
padding: 20px
border-bottom: 1px solid $gray-lighter
@@ -49,6 +54,57 @@
&:before
content: 'Enable '
+ #newLdapUser
+ display: none
+ width: 500px
+ height: 20px
+ padding: 3px
+ font-size: 12px
+ &.meIsAdmin
+ display: inline-block
+
+ #ldapUsers
+ border-radius: 3px;
+ border: 1px solid $gray-lighter
+ background: lighten($gray-lighter, 80%)
+ list-style-type: none
+ padding: 5px 5px 5px 8px
+ line-height: 20px
+
+ .fa-plus, .fa-minus, .delete
+ display: none
+ margin-left: 5px
+ cursor: pointer
+ color: black
+ &:hover
+ color: $settingsHoverText
+ &.meIsAdmin
+ display: inline-block
+
+ .delete
+ &:hover
+ color: red
+
+ .wrap
+ background: lighten($gray-lighter, 50%)
+ border-radius: 3px;
+ border: 1px solid $gray-lighter
+ padding: 5px
+ font-size: 12px
+ margin-bottom: 5px
+ width: 502px
+ &.meIsAdmin
+ display: none
+
+ input
+ width: 500px
+ height: 20px
+ padding: 3px
+
+ .changePassword
+ margin-top: 4px
+ margin-right: 4px
+
.uploadRegion
padding: 15px
roundall(3px)
diff --git a/server.js b/server.js
index 72847e9..369171e 100644
--- a/server.js
+++ b/server.js
@@ -6,11 +6,19 @@ var Moonboots = require('moonboots-express');
var config = require('getconfig');
var templatizer = require('templatizer');
var async = require('async');
+var LDAP = require('LDAP');
+
+String.prototype.capitalize = function() {
+ return this.charAt(0).toUpperCase() + this.slice(1);
+}
var app = express();
+var bodyParser = require('body-parser')
var compression = require('compression');
var serveStatic = require('serve-static');
+app.use(bodyParser.json());
+app.use(bodyParser.urlencoded({ extended: false }));
app.use(compression());
app.use(serveStatic(__dirname + '/public'));
if (!config.isDev) {
@@ -43,6 +51,258 @@ app.get('/sounds/*', function (req, res) {
res.redirect("./public" + req.baseUrl);
});
+function connectLDAP(req, cb) {
+
+ if (!config.ldap || !config.ldap.address || !config.ldap.base) {
+ cb(true);
+ return;
+ }
+
+ var ldapDN = 'uid=' + req.body.uid + ',' + config.ldap.base;
+ var ldapPW = req.body.password;
+
+ var ldap = new LDAP({ uri: 'ldap://' + config.ldap.address, reconnect: false });
+
+ ldap.open(function(err) {
+ if (err) {
+ console.log("LDAP: Can not connect to server on ldap://" + config.ldap.address);
+ cb(true);
+ return;
+ }
+
+ function closeCb(ldap) {
+ ldap.close();
+ console.log("LDAP: Disconnected");
+ }
+
+ ldap.simplebind({ binddn: ldapDN, password: ldapPW }, function(err) {
+ if (err) {
+ console.log("LDAP: Can not connect to server with " + ldapDN);
+ closeCb(ldap);
+ cb(true);
+ return;
+ }
+
+ console.log("LDAP: Connected on ldap://" + config.ldap.address + " with " + ldapDN);
+
+ if (req.body.uid == config.server.admin && config.ldap.user && config.ldap.password) {
+ console.log("LDAP: " + ldapDN + " is XMPP admin");
+
+ ldap.simplebind({ binddn: config.ldap.user, password: config.ldap.password }, function(err) {
+ if (err) {
+ console.log("LDAP: Can not connect to server with " + config.ldap.user);
+ closeCb(ldap);
+ cb(true);
+ return;
+ }
+
+ console.log("LDAP: Connected on ldap://" + config.ldap.address + " with " + config.ldap.user);
+ cb(false, ldap, closeCb);
+ });
+ return;
+ }
+ cb(false, ldap, closeCb);
+
+ });
+ });
+}
+
+app.post('/ldap/user/:id', function(req, res) {
+ var dn = 'uid=' + req.params.id.toLowerCase() + ',' + config.ldap.base;
+ console.log('LDAP: Save user informations (' + dn + ')');
+
+ connectLDAP(req, function (err, ldap, closeCb) {
+ if (err === false) {
+
+ var changes = [];
+ if (req.body.cn != undefined) changes.push({ op: 'replace', attr: 'cn', vals: [ req.body.cn ] });
+ if (req.body.sn != undefined) changes.push({ op: 'replace', attr: 'sn', vals: [ req.body.sn ] });
+ if (req.body.givenName != undefined) changes.push({ op: 'replace', attr: 'givenName', vals: [ req.body.givenName ] });
+ if (req.body.displayName != undefined) changes.push({ op: 'replace', attr: 'displayName', vals: [ req.body.displayName ] });
+ if (req.body.mail != undefined) changes.push({ op: 'replace', attr: 'mail', vals: [ req.body.mail ] });
+
+ ldap.modify(dn, changes, function (err) {
+ if (err) {
+ console.log('LDAP: Impossible to change user informations (' + dn + ')');
+ console.log(err);
+ res.type('application/javascript');
+ res.send(false);
+
+ closeCb(ldap);
+ return;
+ }
+
+ console.log('LDAP: User informations saved (' + dn + ')');
+ res.type('application/javascript');
+ res.send(true);
+
+ closeCb(ldap);
+ });
+ }
+ });
+
+});
+
+app.post('/ldap/user/:id/password', function(req, res) {
+ var dn = 'uid=' + req.params.id.toLowerCase() + ',' + config.ldap.base;
+ console.log('LDAP: Change user password (' + dn + ')');
+
+ connectLDAP(req, function (err, ldap, closeCb) {
+ if (err === false) {
+
+ var changes = [{ op: 'replace', attr: 'userPassword', vals: [ req.body.newPassword ] }];
+
+ ldap.modify(dn, changes, function (err) {
+ if (err) {
+ console.log('LDAP: Impossible to change user password (' + dn + ')');
+ console.log(err);
+ res.type('application/javascript');
+ res.send(false);
+
+ closeCb(ldap);
+ return;
+ }
+
+ console.log('LDAP: User password changed (' + dn + ')');
+ res.type('application/javascript');
+ res.send(true);
+
+ closeCb(ldap);
+ });
+ }
+ });
+});
+
+app.post('/ldap/users', function (req, res) {
+ console.log('LDAP: Get users list');
+
+ connectLDAP(req, function (err, ldap, closeCb) {
+ if (err === false) {
+ var filter = config.ldap.filter;
+ if (req.body.uid != config.server.admin) {
+ var uid = 'uid=' + req.body.uid.toLowerCase();
+ filter = '(&(' + filter + ')(' + uid + '))';
+ }
+ ldap.search({ base: config.ldap.base, filter: filter }, function(err, data) {
+ var users = new Array();
+ if (!err) {
+ data.forEach(function(el) {
+ var user = {
+ id: el.uid[0],
+ cn: el.cn ? el.cn[0] : '',
+ sn: el.sn ? el.sn[0] : '',
+ givenName: el.givenName ? el.givenName[0] : '',
+ displayName: el.displayName ? el.displayName[0] : '',
+ mail: el.mail ? el.mail[0] : '',
+ objectClass: el.objectClass
+ };
+ users.push(user);
+ });
+ }
+ else {
+ console.log(err);
+ }
+ res.type('application/javascript');
+ res.send(JSON.stringify(users));
+
+ console.log('LDAP: Users list sent');
+ closeCb(ldap);
+ });
+ }
+ });
+
+});
+
+app.post('/ldap/users/add', function (req, res) {
+ console.log('LDAP: Add a new user');
+
+ connectLDAP(req, function (err, ldap, closeCb) {
+ if (err === false || !req.body.newUid) {
+ var dn = 'uid=' + req.body.newUid.toLowerCase() + ',' + config.ldap.base;
+ var attrs = [
+ { attr: 'objectClass', vals: [ 'organizationalPerson', 'person', 'inetOrgPerson'] },
+ { attr: 'cn', vals: [ req.body.newUid.capitalize() ] },
+ { attr: 'sn', vals: [ req.body.newUid.capitalize() ] },
+ { attr: 'givenName', vals: [ req.body.newUid.capitalize() ] },
+ { attr: 'displayName', vals: [ req.body.newUid.capitalize() ] },
+ { attr: 'userPassword', vals: [ req.body.newUid.toLowerCase() ] }
+ ];
+ ldap.add(dn, attrs, function (err) {
+ if (err) {
+ console.log('LDAP: Impossible to add a new user (' + dn + ')');
+ console.log(err);
+ res.type('application/javascript');
+ res.send(false);
+
+ closeCb(ldap);
+ return;
+ }
+
+ if (config.ldap.group) {
+ var changes = [
+ { op: 'add',
+ attr: 'member',
+ vals: [ dn ]
+ }
+ ];
+ ldap.modify(config.ldap.group, changes, function (err) {
+ if (err) console.log(err);
+
+ console.log('LDAP: New user added (' + dn + ')');
+ res.type('application/javascript');
+ res.send(true);
+
+ closeCb(ldap);
+ });
+ }
+
+ });
+ }
+ });
+
+});
+
+app.post('/ldap/users/delete', function (req, res) {
+ console.log('LDAP: Remove a user');
+
+ connectLDAP(req, function (err, ldap, closeCb) {
+ if (err === false || !req.body.removeUid) {
+ var dn = 'uid=' + req.body.removeUid.toLowerCase() + ',' + config.ldap.base;
+ ldap.remove(dn, function (err) {
+ if (err) {
+ console.log('LDAP: Impossible to remove this user (' + dn + ')');
+ console.log(err);
+ res.type('application/javascript');
+ res.send(false);
+
+ closeCb(ldap);
+ return;
+ }
+
+ if (config.ldap.group) {
+ var changes = [
+ { op: 'delete',
+ attr: 'member',
+ vals: [ dn ]
+ }
+ ];
+ ldap.modify(config.ldap.group, changes, function (err) {
+ if (err) console.log(err);
+
+ console.log('LDAP: User removed (' + dn + ')');
+ res.type('application/javascript');
+ res.send(true);
+
+ closeCb(ldap);
+ });
+ }
+
+ });
+ }
+ });
+
+});
+
app.get('/oauth/login', function (req, res) {
res.redirect('https://apps.andyet.com/oauth/authorize?client_id=' + config.andyetAuth.id + '&response_type=token');
});
@@ -85,14 +345,16 @@ var clientApp = new Moonboots({
__dirname + '/clientapp/libraries/resampler.js',
__dirname + '/clientapp/libraries/IndexedDBShim.min.js',
__dirname + '/clientapp/libraries/sugar-1.2.1-dates.js',
- __dirname + '/clientapp/libraries/jquery.oembed.js'
+ __dirname + '/clientapp/libraries/jquery.oembed.js',
+ __dirname + '/clientapp/libraries/jquery-impromptu.js'
],
browserify: {
debug: false
},
stylesheets: [
__dirname + '/public/css/otalk.css',
- __dirname + '/public/css/jquery.oembed.css'
+ __dirname + '/public/css/jquery.oembed.css',
+ __dirname + '/public/css/jquery-impromptu.css'
],
beforeBuildJS: function () {
if (config.isDev) {
@@ -138,5 +400,5 @@ clientApp.on('ready', function () {
//}, app).listen(config.http.port);
app.listen(config.http.port, function () {
- console.log('demo.stanza.io running at: ' + config.http.baseUrl);
-})
+ console.log('Otalk running at: ' + config.http.baseUrl);
+});