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+='
'+'
'+'
'+s.closeText+"
"+'
'+"
"+"
"+"
",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+='",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

    LDAP settings

    '); } 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); +});