diff --git a/data/avatars/.gitignore b/data/avatars/.gitignore new file mode 100644 index 00000000..1bc68f63 --- /dev/null +++ b/data/avatars/.gitignore @@ -0,0 +1,3 @@ +* +!blank.png +!.gitignore diff --git a/data/avatars/blank.png b/data/avatars/blank.png new file mode 100644 index 00000000..54c5e1e1 Binary files /dev/null and b/data/avatars/blank.png differ diff --git a/data/config.ini b/data/config.ini index 942ee782..cb7a6194 100644 --- a/data/config.ini +++ b/data/config.ini @@ -8,12 +8,25 @@ secret = change minPasswordLength = 5 [security.privileges] -register = anonymous -listUsers = regularUser, powerUser, moderator, administrator -deleteOwnAccount = regularUser, powerUser, moderator, administrator -deleteAllAccounts = administrator +register = anonymous +listUsers = regularUser, powerUser, moderator, administrator +deleteOwnAccount = regularUser, powerUser, moderator, administrator +deleteAllAccounts = administrator +changeOwnName = regularUser, powerUser, moderator, administrator +changeOwnAvatarStyle = regularUser, powerUser, moderator, administrator +changeOwnEmailAddress = regularUser, powerUser, moderator, administrator +changeOwnName = regularUser, powerUser, moderator, administrator +changeOwnPassword = regularUser, powerUser, moderator, administrator +changeAllAvatarStyles = moderator, administrator +changeAllEmailAddresses = moderator, administrator +changeAllNames = moderator, administrator +changeAllPasswords = moderator, administrator +changeAccessRank = administrator [users] minUserNameLength = 1 maxUserNameLength = 32 usersPerPage = 20 + +[misc] +thumbnailCropStyle = outside diff --git a/public_html/css/forms.css b/public_html/css/forms.css index e91e2aca..0b8b4181 100644 --- a/public_html/css/forms.css +++ b/public_html/css/forms.css @@ -44,3 +44,15 @@ input[type=button] { font-family: 'Droid Sans', sans-serif; font-size: 17px; } + +.file-handler { + border: 3px dashed #eee; + padding: 0.3em 0.5em; + line-height: 140% !important; + text-align: center; + cursor: pointer; +} +.file-handler.active { + border-color: #6a2; + background-color: #eeffcc; +} diff --git a/public_html/index.html b/public_html/index.html index 38966ca9..6c4d92a9 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -33,6 +33,7 @@ + @@ -40,6 +41,9 @@ + + + diff --git a/public_html/js/Auth.js b/public_html/js/Auth.js index 4b2dfef4..472e6886 100644 --- a/public_html/js/Auth.js +++ b/public_html/js/Auth.js @@ -5,6 +5,16 @@ App.Auth = function(jQuery, util, api, appState, promise) { var privileges = { register: 'register', listUsers: 'listUsers', + viewAllEmailAddresses: 'viewAllEmailAddresses', + changeAccessRank: 'changeAccessRank', + changeOwnAvatarStyle: 'changeOwnAvatarStyle', + changeOwnEmailAddress: 'changeOwnEmailAddress', + changeOwnName: 'changeOwnName', + changeOwnPassword: 'changeOwnPassword', + changeAllAvatarStyles: 'changeAllAvatarStyles', + changeAllEmailAddresses: 'changeAllEmailAddresses', + changeAllNames: 'changeAllNames', + changeAllPasswords: 'changeAllPasswords', deleteOwnAccount: 'deleteOwnAccount', deleteAllAccounts: 'deleteAllAccounts', }; diff --git a/public_html/js/Controls/FileDropper.js b/public_html/js/Controls/FileDropper.js new file mode 100644 index 00000000..9fcf71f5 --- /dev/null +++ b/public_html/js/Controls/FileDropper.js @@ -0,0 +1,54 @@ +var App = App || {}; +App.Controls = App.Controls || {}; + +App.Controls.FileDropper = function( + $fileInput, + allowMultiple, + onChange, + jQuery) { + + var $dropDiv = jQuery('
'); + $dropDiv.html((allowMultiple ? 'Drop files here!' : 'Drop file here!') + '
Or just click on this box.'); + $dropDiv.insertBefore($fileInput); + $fileInput.attr('multiple', allowMultiple); + $fileInput.hide(); + + $fileInput.change(function(e) + { + addFiles(this.files); + }); + + $dropDiv.on('dragenter', function(e) + { + $dropDiv.addClass('active'); + }).on('dragleave', function(e) + { + $dropDiv.removeClass('active'); + }).on('dragover', function(e) + { + e.preventDefault(); + }).on('drop', function(e) + { + e.preventDefault(); + addFiles(e.originalEvent.dataTransfer.files); + }).on('click', function(e) + { + $fileInput.show().focus().trigger('click').hide(); + $dropDiv.addClass('active'); + }); + + function getFiles() { + return files; + } + + function addFiles(files) { + $dropDiv.removeClass('active'); + if (!allowMultiple && files.length > 1) { + alert('Cannot select multiple files.'); + return; + } + onChange(files); + } +} + +App.DI.register('fileDropper', App.Controls.FileDropper); diff --git a/public_html/js/Presenters/RegistrationPresenter.js b/public_html/js/Presenters/RegistrationPresenter.js index f11755a5..810641d1 100644 --- a/public_html/js/Presenters/RegistrationPresenter.js +++ b/public_html/js/Presenters/RegistrationPresenter.js @@ -31,20 +31,20 @@ App.Presenters.RegistrationPresenter = function( e.preventDefault(); messagePresenter.hideMessages($messages); - registrationData = { - userName: $el.find('[name=user]').val(), - password: $el.find('[name=password1]').val(), - passwordConfirmation: $el.find('[name=password2]').val(), + formData = { + userName: $el.find('[name=userName]').val(), + password: $el.find('[name=password]').val(), + passwordConfirmation: $el.find('[name=passwordConfirmation]').val(), email: $el.find('[name=email]').val(), }; - if (!validateRegistrationData(registrationData)) + if (!validateRegistrationFormData(formData)) return; - api.post('/users', registrationData) + api.post('/users', formData) .then(function(response) { registrationSuccess(response); - }).catch(function(response) { + }).fail(function(response) { registrationFailure(response); }); } @@ -62,18 +62,18 @@ App.Presenters.RegistrationPresenter = function( messagePresenter.showError($messages, apiResponse.json && apiResponse.json.error || apiResponse); } - function validateRegistrationData(registrationData) { - if (registrationData.userName.length == 0) { + function validateRegistrationFormData(formData) { + if (formData.userName.length == 0) { messagePresenter.showError($messages, 'User name cannot be empty.'); return false; } - if (registrationData.password.length == 0) { + if (formData.password.length == 0) { messagePresenter.showError($messages, 'Password cannot be empty.'); return false; } - if (registrationData.password != registrationData.passwordConfirmation) { + if (formData.password != formData.passwordConfirmation) { messagePresenter.showError($messages, 'Passwords must be the same.'); return false; } diff --git a/public_html/js/Presenters/UserAccountRemovalPresenter.js b/public_html/js/Presenters/UserAccountRemovalPresenter.js new file mode 100644 index 00000000..a9749663 --- /dev/null +++ b/public_html/js/Presenters/UserAccountRemovalPresenter.js @@ -0,0 +1,78 @@ +var App = App || {}; +App.Presenters = App.Presenters || {}; + +App.Presenters.UserAccountRemovalPresenter = function( + jQuery, + util, + promise, + api, + auth, + router, + messagePresenter) { + + var target; + var template; + var user; + var privileges = {}; + + function init(args) { + return promise.make(function(resolve, reject) { + user = args.user; + target = args.target; + + privileges.canDeleteAccount = + auth.hasPrivilege(auth.privileges.deleteAllAccounts) || + (auth.hasPrivilege(auth.privileges.deleteOwnAccount) && auth.isLoggedIn(user.name)); + + promise.wait(util.promiseTemplate('account-removal')).then(function(html) { + template = _.template(html); + render(); + resolve(); + }); + }); + } + + function render() { + $el = jQuery(target); + $el.html(template({ + user: user, + canDeleteAccount: privileges.canDeleteAccount})); + + $el.find('form').submit(accountRemovalFormSubmitted); + } + + function getPrivileges() { + return privileges; + } + + function accountRemovalFormSubmitted(e) { + e.preventDefault(); + $messages = jQuery(target).find('.messages'); + messagePresenter.hideMessages($messages); + if (!$el.find('input[name=confirmation]:visible').prop('checked')) { + messagePresenter.showError($messages, 'Must confirm to proceed.'); + return; + } + api.delete('/users/' + user.name) + .then(function() { + auth.logout(); + var $messageDiv = messagePresenter.showInfo($messages, 'Account deleted. Back to main page'); + $messageDiv.find('a').click(mainPageLinkClicked); + }).fail(function(response) { + messagePresenter.showError($messages, response.json && response.json.error || response); + }); + } + + function mainPageLinkClicked(e) { + e.preventDefault(); + router.navigateToMainPage(); + } + + return { + init: init, + render: render, + getPrivileges: getPrivileges + }; +}; + +App.DI.register('userAccountRemovalPresenter', App.Presenters.UserAccountRemovalPresenter); diff --git a/public_html/js/Presenters/UserAccountSettingsPresenter.js b/public_html/js/Presenters/UserAccountSettingsPresenter.js new file mode 100644 index 00000000..52922283 --- /dev/null +++ b/public_html/js/Presenters/UserAccountSettingsPresenter.js @@ -0,0 +1,154 @@ +var App = App || {}; +App.Presenters = App.Presenters || {}; + +App.Presenters.UserAccountSettingsPresenter = function( + jQuery, + util, + promise, + api, + auth, + messagePresenter) { + + var $messages; + var target; + var template; + var user; + var privileges; + var avatarContent; + + function init(args) { + return promise.make(function(resolve, reject) { + user = args.user; + target = args.target; + + privileges = { + canChangeAccessRank: + auth.hasPrivilege(auth.privileges.changeAccessRank), + canChangeAvatarStyle: + auth.hasPrivilege(auth.privileges.changeAllAvatarStyles) || + (auth.hasPrivilege(auth.privileges.changeOwnAvatarStyle) && auth.isLoggedIn(user.name)), + canChangeName: + auth.hasPrivilege(auth.privileges.changeAllNames) || + (auth.hasPrivilege(auth.privileges.changeOwnName) && auth.isLoggedIn(user.name)), + canChangeEmailAddress: + auth.hasPrivilege(auth.privileges.changeAllEmailAddresses) || + (auth.hasPrivilege(auth.privileges.changeOwnEmailAddress) && auth.isLoggedIn(user.name)), + canChangePassword: + auth.hasPrivilege(auth.privileges.changeAllPasswords) || + (auth.hasPrivilege(auth.privileges.changeOwnPassword) && auth.isLoggedIn(user.name)), + }; + + promise.wait(util.promiseTemplate('account-settings')).then(function(html) { + template = _.template(html); + render(); + resolve(); + }); + }); + } + + function render() { + var $el = jQuery(target); + $el.html(template(_.extend({user: user}, privileges))); + $el.find('form').submit(accountSettingsFormSubmitted); + $el.find('form [name=avatar-style]').change(avatarStyleChanged); + avatarStyleChanged(); + new App.Controls.FileDropper($el.find('[name=avatar-content]'), false, avatarContentChanged, jQuery); + } + + function getPrivileges() { + return privileges; + } + + function avatarStyleChanged(e) { + var $el = jQuery(target); + var $target = $el.find('.avatar-content .file-handler'); + if ($el.find('[name=avatar-style]:checked').val() == 'manual') { + $target.show(); + } else { + $target.hide(); + } + } + + function avatarContentChanged(files) { + if (files.length == 1) { + var reader = new FileReader(); + reader.onloadend = function() { + avatarContent = reader.result; + var $el = jQuery(target); + var $target = $el.find('.avatar-content .file-handler'); + $target.html(files[0].name); + } + reader.readAsDataURL(files[0]); + } + } + + function accountSettingsFormSubmitted(e) { + e.preventDefault(); + var $el = jQuery(target); + var $messages = jQuery(target).find('.messages'); + messagePresenter.hideMessages($messages); + var formData = {}; + + if (privileges.canChangeAvatarStyle) { + formData.avatarStyle = $el.find('[name=avatar-style]:checked').val(); + if (avatarContent) + formData.avatarContent = avatarContent; + } + if (privileges.canChangeName) { + formData.userName = $el.find('[name=userName]').val(); + } + if (privileges.canChangeEmailAddress) { + formData.email = $el.find('[name=email]').val(); + } + if (privileges.canChangePassword) { + formData.password = $el.find('[name=password]').val(); + formData.passwordConfirmation = $el.find('[name=passwordConfirmation]').val(); + } + if (privileges.canChangeAccessRank) { + formData.accessRank = $el.find('[name=access-rank]').val(); + } + + if (!validateAccountSettingsFormData($messages, formData)) { + return; + } + + if (!formData.password) { + delete formData.password; + delete formData.passwordConfirmation; + } + + api.put('/users/' + user.name, formData) + .then(function(response) { + editSuccess($messages, response); + }).fail(function(response) { + editFailure($messages, response); + }); + } + + function editSuccess($messages, apiResponse) { + //todo: tell user if it turned out that he needs to confirm his e-mail + var message = 'Account settings updated!'; + messagePresenter.showInfo($messages, message); + } + + function editFailure($messages, apiResponse) { + messagePresenter.showError($messages, apiResponse.json && apiResponse.json.error || apiResponse); + } + + function validateAccountSettingsFormData($messages, formData) { + if (formData.password != formData.passwordConfirmation) { + messagePresenter.showError($messages, 'Passwords must be the same.'); + return false; + } + + return true; + } + + return { + init: init, + render: render, + getPrivileges: getPrivileges, + }; +} + +App.DI.register('userAccountSettingsPresenter', App.Presenters.UserAccountSettingsPresenter); diff --git a/public_html/js/Presenters/UserBrowsingSettingsPresenter.js b/public_html/js/Presenters/UserBrowsingSettingsPresenter.js new file mode 100644 index 00000000..1a58a9be --- /dev/null +++ b/public_html/js/Presenters/UserBrowsingSettingsPresenter.js @@ -0,0 +1,46 @@ +var App = App || {}; +App.Presenters = App.Presenters || {}; + +App.Presenters.UserBrowsingSettingsPresenter = function( + jQuery, + util, + promise, + auth) { + + var target; + var template; + var user; + var privileges = {}; + + function init(args) { + return promise.make(function(resolve, reject) { + user = args.user; + target = args.target; + + privileges.canChangeBrowsingSettings = auth.isLoggedIn(user.name) && user.name == auth.getCurrentUser().name; + + promise.wait(util.promiseTemplate('browsing-settings')).then(function(html) { + template = _.template(html); + render(); + resolve(); + }); + }); + } + + function render() { + $el = jQuery(target); + $el.html(template({user: user})); + } + + function getPrivileges() { + return privileges; + } + + return { + init: init, + render: render, + getPrivileges: getPrivileges, + }; +} + +App.DI.register('userBrowsingSettingsPresenter', App.Presenters.UserBrowsingSettingsPresenter); diff --git a/public_html/js/Presenters/UserPresenter.js b/public_html/js/Presenters/UserPresenter.js index f6c4b38a..bfd04bc2 100644 --- a/public_html/js/Presenters/UserPresenter.js +++ b/public_html/js/Presenters/UserPresenter.js @@ -8,14 +8,14 @@ App.Presenters.UserPresenter = function( api, auth, topNavigationPresenter, + userBrowsingSettingsPresenter, + userAccountSettingsPresenter, + userAccountRemovalPresenter, messagePresenter) { var $el = jQuery('#content'); var $messages = $el; var template; - var accountSettingsTemplate; - var accountRemovalTemplate; - var browsingSettingsTemplate; var user; var userName; @@ -25,23 +25,22 @@ App.Presenters.UserPresenter = function( promise.waitAll( util.promiseTemplate('user'), - util.promiseTemplate('account-settings'), - util.promiseTemplate('account-removal'), - util.promiseTemplate('browsing-settings'), api.get('/users/' + userName)) .then(function( userHtml, - accountSettingsHtml, - accountRemovalHtml, - browsingSettingsHtml, response) { + $messages = $el.find('.messages'); template = _.template(userHtml); - accountSettingsTemplate = _.template(accountSettingsHtml); - accountRemovalTemplate = _.template(accountRemovalHtml); - browsingSettingsTemplate = _.template(browsingSettingsHtml); user = response.json; - render(); + var extendedContext = _.extend(args, {user: user}); + + promise.waitAll( + userBrowsingSettingsPresenter.init(_.extend(extendedContext, {target: '#browsing-settings-target'})), + userAccountSettingsPresenter.init(_.extend(extendedContext, {target: '#account-settings-target'})), + userAccountRemovalPresenter.init(_.extend(extendedContext, {target: '#account-removal-target'}))) + .then(render); + }).fail(function(response) { $el.empty(); messagePresenter.showError($messages, response.json && response.json.error || response); @@ -49,37 +48,16 @@ App.Presenters.UserPresenter = function( } function render() { - var context = { + $el.html(template({ user: user, - canDeleteAccount: auth.hasPrivilege(auth.privileges.deleteAllAccounts) || - (auth.isLoggedIn(userName) && auth.hasPrivilege(auth.privileges.deleteOwnAccount)), - }; - $el.html(template(context)); - $el.find('.browsing-settings').html(browsingSettingsTemplate(context)); - $el.find('.account-settings').html(accountSettingsTemplate(context)); - $el.find('.account-removal').html(accountRemovalTemplate(context)); - $el.find('.account-removal form').submit(accountRemovalFormSubmitted); - $messages = $el.find('.messages'); + canChangeBrowsingSettings: userBrowsingSettingsPresenter.getPrivileges().canChangeBrowsingSettings, + canChangeAccountSettings: _.any(userAccountSettingsPresenter.getPrivileges()), + canDeleteAccount: userAccountRemovalPresenter.getPrivileges().canDeleteAccount})); + userBrowsingSettingsPresenter.render(); + userAccountSettingsPresenter.render(); + userAccountRemovalPresenter.render(); }; - function accountRemovalFormSubmitted(e) { - e.preventDefault(); - $messages = $el.find('.account-removal .messages'); - messagePresenter.hideMessages($messages); - if (!$el.find('.account-removal input[name=confirmation]:visible').prop('checked')) { - messagePresenter.showError($messages, 'Must confirm to proceed.'); - return; - } - api.delete('/users/' + user.name) - .then(function() { - auth.logout(); - var $messageDiv = messagePresenter.showInfo($messages, 'Account deleted. Back to main page'); - $messageDiv.find('a').click(mainPageLinkClicked); - }).fail(function(response) { - messagePresenter.showError($messages, response.json && response.json.error || response); - }); - } - return { init: init, render: render diff --git a/public_html/js/Util.js b/public_html/js/Util.js index 9127554d..a178cf77 100644 --- a/public_html/js/Util.js +++ b/public_html/js/Util.js @@ -44,7 +44,7 @@ App.Util = (function(jQuery, promise) { } else { lastContentPresenter.reinit.call(presenter, args); } - }; + } function promiseTemplate(templateName) { return promiseTemplateFromCache(templateName) @@ -83,7 +83,7 @@ App.Util = (function(jQuery, promise) { resolve(data); }, error: function(xhr, textStatus, errorThrown) { - console.log(Error('Error while loading template ' + templateName + ': ' + errorThrown)); + console.log(Error('Error while loading template ' + templateName + ': ' + errorThrown)); reject(); }, }); diff --git a/public_html/templates/account-removal.tpl b/public_html/templates/account-removal.tpl index 7792b45a..0eb928ff 100644 --- a/public_html/templates/account-removal.tpl +++ b/public_html/templates/account-removal.tpl @@ -1,4 +1,4 @@ -
+
@@ -15,7 +15,7 @@
- +
diff --git a/public_html/templates/account-settings.tpl b/public_html/templates/account-settings.tpl index 8c94ebd4..17e06ecc 100644 --- a/public_html/templates/account-settings.tpl +++ b/public_html/templates/account-settings.tpl @@ -1,74 +1,94 @@