diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..72e11d63 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# These files are text and should be normalized (convert crlf > lf) +*.hbs text \ No newline at end of file diff --git a/.gitconfig b/.gitconfig new file mode 100644 index 00000000..cb385d88 --- /dev/null +++ b/.gitconfig @@ -0,0 +1,5 @@ +[core] + whitespace = trailing-space,space-before-tab + autocrlf = input +[apply] + whitespace = fix \ No newline at end of file diff --git a/.gitignore b/.gitignore index c8011f2f..c94335a6 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,5 @@ debug Dockerfile container.sh make.sh + +embed/bindata_assetfs.go diff --git a/README.md b/README.md index a43b3140..2ee29e63 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,12 @@ v0.15.0  +## Auth0 Integration + +Documize is compatible with Auth0 identity as a service. + + + ## Legal https://documize.com diff --git a/app/app/components/documize-setup.js b/app/app/components/documize-setup.js new file mode 100644 index 00000000..d81c1f97 --- /dev/null +++ b/app/app/components/documize-setup.js @@ -0,0 +1,71 @@ +// Copyright 2016 Documize Inc. . All rights reserved. +// +// This software (Documize Community Edition) is licensed under +// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html +// +// You can operate outside the AGPL restrictions by purchasing +// Documize Enterprise Edition and obtaining a commercial license +// by contacting . +// +// https://documize.com + +import Ember from 'ember'; + +const { + isEmpty, + computed, + set + +} = Ember; + +export default Ember.Component.extend({ + titleEmpty: computed.empty('model.title'), + firstnameEmpty: computed.empty('model.firstname'), + lastnameEmpty: computed.empty('model.lastname'), + emailEmpty: computed.empty('model.email'), + passwordEmpty: computed.empty('model.password'), + hasEmptyTitleError: computed.and('titleEmpty', 'titleError'), + hasEmptyFirstnameError: computed.and('firstnameEmpty', 'adminFirstnameError'), + hasEmptyLastnameError: computed.and('lastnameEmpty', 'adminLastnameError'), + hasEmptyEmailError: computed.and('emailEmpty', 'adminEmailError'), + hasEmptyPasswordError: computed.and('passwordEmpty', 'adminPasswordError'), + + actions: { + save() { + if (isEmpty(this.get('model.title'))) { + set(this, 'titleError', true); + return $("#siteTitle").focus(); + } + + if (isEmpty(this.get('model.firstname'))) { + set(this, 'adminFirstnameError', true); + return $("#adminFirstname").focus(); + } + + if (isEmpty(this.get('model.lastname'))) { + set(this, 'adminLastnameError', true); + return $("#adminLastname").focus(); + } + + if (isEmpty(this.get('model.email')) || !is.email(this.get('model.email'))) { + set(this, 'adminEmailError', true); + return $("#adminEmail").focus(); + } + + if (isEmpty(this.get('model.password'))) { + set(this, 'adminPasswordError', true); + return $("#adminPassword").focus(); + } + + this.model.allowAnonymousAccess = Ember.$("#allowAnonymousAccess").prop('checked'); + + this.get('save')().then(() => { + set(this, 'titleError', false); + set(this, 'adminFirstnameError', false); + set(this, 'adminLastnameError', false); + set(this, 'adminEmailError', false); + set(this, 'adminPasswordError', false); + }); + } + } +}); diff --git a/app/app/components/forgot-password.js b/app/app/components/forgot-password.js new file mode 100644 index 00000000..261a2748 --- /dev/null +++ b/app/app/components/forgot-password.js @@ -0,0 +1,41 @@ +// Copyright 2016 Documize Inc. . All rights reserved. +// +// This software (Documize Community Edition) is licensed under +// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html +// +// You can operate outside the AGPL restrictions by purchasing +// Documize Enterprise Edition and obtaining a commercial license +// by contacting . +// +// https://documize.com + +import Ember from 'ember'; + +const { + computed, + isEmpty +} = Ember; + +export default Ember.Component.extend({ + email: "", + sayThanks: false, + emailEmpty: computed.empty('email'), + hasEmptyEmailError: computed.and('emailEmpty', 'emailIsEmpty'), + + actions: { + forgot() { + let email = this.get('email'); + + if (isEmpty(email)) { + Ember.set(this, 'emailIsEmpty', true); + return $("#email").focus(); + } + + this.get('forgot')(email).then(() => { + Ember.set(this, 'sayThanks', true); + Ember.set(this, 'email', ''); + Ember.set(this, 'emailIsEmpty', false); + }); + } + } +}); diff --git a/app/app/components/general-settings.js b/app/app/components/general-settings.js new file mode 100644 index 00000000..41a2ebc5 --- /dev/null +++ b/app/app/components/general-settings.js @@ -0,0 +1,45 @@ +// Copyright 2016 Documize Inc. . All rights reserved. +// +// This software (Documize Community Edition) is licensed under +// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html +// +// You can operate outside the AGPL restrictions by purchasing +// Documize Enterprise Edition and obtaining a commercial license +// by contacting . +// +// https://documize.com + +import Ember from 'ember'; + +const { + isEmpty, + computed, + set +} = Ember; + +export default Ember.Component.extend({ + titleEmpty: computed.empty('model.title'), + messageEmpty: computed.empty('model.message'), + hasTitleInputError: computed.and('titleEmpty', 'titleError'), + hasMessageInputError: computed.and('messageEmpty', 'messageError'), + + actions: { + save() { + if (isEmpty(this.get('model.title'))) { + set(this, 'titleError', true); + return $("#siteTitle").focus(); + } + + if (isEmpty(this.get('model.message'))) { + set(this, 'messageError', true); + return $("#siteMessage").focus(); + } + + this.model.set('allowAnonymousAccess', Ember.$("#allowAnonymousAccess").prop('checked')); + this.get('save')().then(() => { + set(this, 'titleError', false); + set(this, 'messageError', false); + }); + } + } +}); diff --git a/app/app/components/password-reset.js b/app/app/components/password-reset.js new file mode 100644 index 00000000..49cca5c0 --- /dev/null +++ b/app/app/components/password-reset.js @@ -0,0 +1,59 @@ +// Copyright 2016 Documize Inc. . All rights reserved. +// +// This software (Documize Community Edition) is licensed under +// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html +// +// You can operate outside the AGPL restrictions by purchasing +// Documize Enterprise Edition and obtaining a commercial license +// by contacting . +// +// https://documize.com + +import Ember from 'ember'; + +const { + isEmpty, + isEqual, + computed, + set + +} = Ember; + +export default Ember.Component.extend({ + password: "", + passwordConfirm: "", + mustMatch: false, + passwordEmpty: computed.empty('password'), + confirmEmpty: computed.empty('passwordConfirm'), + hasPasswordError: computed.and('passwordEmpty', 'passwordIsEmpty'), + hasConfirmError: computed.and('confirmEmpty', 'passwordConfirmIsEmpty'), + + actions: { + reset() { + let password = this.get('password'); + let passwordConfirm = this.get('passwordConfirm'); + + if (isEmpty(password)) { + set(this, 'passwordIsEmpty', true); + return $("#newPassword").focus(); + } + + if (isEmpty(passwordConfirm)) { + set(this, 'passwordConfirmIsEmpty', true); + return $("#passwordConfirm").focus(); + } + + if (!isEqual(password, passwordConfirm)) { + set(this, 'hasPasswordError', true); + set(this, 'hasConfirmError', true); + set(this, 'mustMatch', true); + return; + } + + this.get('reset')(password).then(() => { + set(this, 'passwordIsEmpty', false); + set(this, 'passwordConfirmIsEmpty', false); + }); + } + } +}); diff --git a/app/app/components/section/wysiwyg/type-editor.js b/app/app/components/section/wysiwyg/type-editor.js index ae6e77b5..07e60df1 100644 --- a/app/app/components/section/wysiwyg/type-editor.js +++ b/app/app/components/section/wysiwyg/type-editor.js @@ -12,103 +12,103 @@ import Ember from 'ember'; export default Ember.Component.extend({ - pageBody: "", - appMeta: Ember.inject.service(), + pageBody: "", + appMeta: Ember.inject.service(), - didReceiveAttrs() { - this.set('pageBody', this.get('meta.rawBody')); - }, + didReceiveAttrs() { + this.set('pageBody', this.get('meta.rawBody')); + }, - didInsertElement() { - let self = this; + didInsertElement() { + let self = this; - let options = { - selector: "#rich-text-editor", - relative_urls: false, - cache_suffix: "?v=430", - browser_spellcheck: false, - gecko_spellcheck: false, - theme: "modern", - statusbar: false, - height: $(document).height() - $(".document-editor > .toolbar").height() - 200, - entity_encoding: "raw", - paste_data_images: true, - image_advtab: true, - image_caption: true, - media_live_embeds: true, - fontsize_formats: "8pt 10pt 12pt 14pt 16pt 18pt 20pt 22pt 24pt 26pt 28pt 30pt 32pt 34pt 36pt", - formats: { - bold: { - inline: 'b' - }, - italic: { - inline: 'i' - } - }, - extended_valid_elements: "b,i,b/strong,i/em", - plugins: [ - 'advlist autolink lists link image charmap print preview hr anchor pagebreak', - 'searchreplace wordcount visualblocks visualchars code codesample fullscreen', - 'insertdatetime media nonbreaking save table directionality', - 'emoticons template paste textcolor colorpicker textpattern imagetools' - ], - menu: { - edit: { - title: 'Edit', - items: 'undo redo | cut copy paste pastetext | selectall | searchreplace' - }, - insert: { - title: 'Insert', - items: 'anchor link media | hr | charmap emoticons | blockquote' - }, - format: { - title: 'Format', - items: 'bold italic underline strikethrough superscript subscript | formats fonts | removeformat' - }, - table: { - title: 'Table', - items: 'inserttable tableprops deletetable | cell row column' - } - }, - toolbar1: "formatselect fontselect fontsizeselect | bold italic underline | link unlink | image media | codesample | outdent indent | alignleft aligncenter alignright alignjustify | bullist numlist | forecolor backcolor", - save_onsavecallback: function() { - self.send('onAction'); - } - }; + let options = { + selector: "#rich-text-editor", + relative_urls: false, + cache_suffix: "?v=430", + browser_spellcheck: false, + gecko_spellcheck: false, + theme: "modern", + statusbar: false, + height: $(document).height() - $(".document-editor > .toolbar").height() - 200, + entity_encoding: "raw", + paste_data_images: true, + image_advtab: true, + image_caption: true, + media_live_embeds: true, + fontsize_formats: "8pt 10pt 12pt 14pt 16pt 18pt 20pt 22pt 24pt 26pt 28pt 30pt 32pt 34pt 36pt", + formats: { + bold: { + inline: 'b' + }, + italic: { + inline: 'i' + } + }, + extended_valid_elements: "b,i,b/strong,i/em", + plugins: [ + 'advlist autolink lists link image charmap print preview hr anchor pagebreak', + 'searchreplace wordcount visualblocks visualchars code codesample fullscreen', + 'insertdatetime media nonbreaking save table directionality', + 'emoticons template paste textcolor colorpicker textpattern imagetools' + ], + menu: { + edit: { + title: 'Edit', + items: 'undo redo | cut copy paste pastetext | selectall | searchreplace' + }, + insert: { + title: 'Insert', + items: 'anchor link media | hr | charmap emoticons | blockquote' + }, + format: { + title: 'Format', + items: 'bold italic underline strikethrough superscript subscript | formats fonts | removeformat' + }, + table: { + title: 'Table', + items: 'inserttable tableprops deletetable | cell row column' + } + }, + toolbar1: "formatselect fontselect fontsizeselect | bold italic underline | link unlink | image media | codesample | outdent indent | alignleft aligncenter alignright alignjustify | bullist numlist | forecolor backcolor", + save_onsavecallback: function () { + self.send('onAction'); + } + }; - if (typeof tinymce === 'undefined') { - $.getScript(this.get("appMeta").getBaseUrl("tinymce/tinymce.min.js?v=430"), function() { - window.tinymce.dom.Event.domLoaded = true; - tinymce.baseURL = "//" + window.location.host + "/tinymce"; - tinymce.suffix = ".min"; - tinymce.init(options); - }); - } else { - tinymce.init(options); - } - }, + if (typeof tinymce === 'undefined') { + $.getScript("tinymce/tinymce.min.js?v=430", function () { + window.tinymce.dom.Event.domLoaded = true; + tinymce.baseURL = "//" + window.location.host + "/tinymce"; + tinymce.suffix = ".min"; + tinymce.init(options); + }); + } else { + tinymce.init(options); + } + }, - willDestroyElement() { - tinymce.remove(); - }, + willDestroyElement() { + tinymce.remove(); + }, - actions: { - isDirty() { - return is.not.undefined(tinymce) && is.not.undefined(tinymce.activeEditor) && tinymce.activeEditor.isDirty(); - }, + actions: { + isDirty() { + return is.not.undefined(tinymce) && is.not.undefined(tinymce.activeEditor) && tinymce.activeEditor.isDirty(); + }, - onCancel() { - this.attrs.onCancel(); - }, + onCancel() { + this.attrs.onCancel(); + }, - onAction(title) { - let page = this.get('page'); - let meta = this.get('meta'); + onAction(title) { + let page = this.get('page'); + let meta = this.get('meta'); - page.set('title', title); - meta.set('rawBody', tinymce.activeEditor.getContent()); + page.set('title', title); + meta.set('rawBody', tinymce.activeEditor.getContent()); - this.attrs.onAction(page, meta); - } - } -}); + this.attrs.onAction(page, meta); + } + } +}); \ No newline at end of file diff --git a/app/app/components/user-profile.js b/app/app/components/user-profile.js new file mode 100644 index 00000000..4fd4ee31 --- /dev/null +++ b/app/app/components/user-profile.js @@ -0,0 +1,83 @@ +// Copyright 2016 Documize Inc. . All rights reserved. +// +// This software (Documize Community Edition) is licensed under +// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html +// +// You can operate outside the AGPL restrictions by purchasing +// Documize Enterprise Edition and obtaining a commercial license +// by contacting . +// +// https://documize.com + +import Ember from 'ember'; + +const { + computed, + isEmpty, + isEqual, + isPresent +} = Ember; + +export default Ember.Component.extend({ + password: { password: "", confirmation: "" }, + hasFirstnameError: computed.empty('model.firstname'), + hasLastnameError: computed.empty('model.lastname'), + hasEmailError: computed.empty('model.email'), + hasPasswordError: computed('passwordError', 'password.password', { + get() { + if (isPresent(this.get('passwordError'))) { + return `error`; + } + + if (isEmpty(this.get('password.password'))) { + return null; + } + } + }), + hasConfirmPasswordError: computed('confirmPasswordError', { + get() { + if (isPresent(this.get("confirmPasswordError"))) { + return `error`; + } + + return; + } + }), + + actions: { + save() { + let password = this.get('password.password'); + let confirmation = this.get('password.confirmation'); + + if (isEmpty(this.get('model.firstname'))) { + return $("#firstname").focus(); + } + if (isEmpty(this.get('model.lastname'))) { + return $("#lastname").focus(); + } + if (isEmpty(this.get('model.email'))) { + return $("#email").focus(); + } + + if (isPresent(password) && isEmpty(confirmation)) { + Ember.set(this, 'confirmPasswordError', 'error'); + return $("#confirmPassword").focus(); + } + if (isEmpty(password) && isPresent(confirmation)) { + Ember.set(this, 'passwordError', 'error'); + return $("#password").focus(); + } + if (!isEqual(password, confirmation)) { + Ember.set(this, 'passwordError', 'error'); + return $("#password").focus(); + } + + let passwords = this.get('password'); + + this.get('save')(passwords).finally(() => { + Ember.set(this, 'password.password', ''); + Ember.set(this, 'password.confirmation', ''); + }); + } + } +}); diff --git a/app/app/components/user-settings.js b/app/app/components/user-settings.js new file mode 100644 index 00000000..593d608f --- /dev/null +++ b/app/app/components/user-settings.js @@ -0,0 +1,56 @@ +// Copyright 2016 Documize Inc. . All rights reserved. +// +// This software (Documize Community Edition) is licensed under +// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html +// +// You can operate outside the AGPL restrictions by purchasing +// Documize Enterprise Edition and obtaining a commercial license +// by contacting . +// +// https://documize.com + +import Ember from 'ember'; + +const { + isEmpty, + computed, + set, + get +} = Ember; + +export default Ember.Component.extend({ + newUser: { firstname: "", lastname: "", email: "", active: true }, + firstnameEmpty: computed.empty('newUser.firstname'), + lastnameEmpty: computed.empty('newUser.lastname'), + emailEmpty: computed.empty('newUser.email'), + hasFirstnameEmptyError: computed.and('firstnameEmpty', 'firstnameError'), + hasLastnameEmptyError: computed.and('lastnameEmpty', 'lastnameError'), + hasEmailEmptyError: computed.and('emailEmpty', 'emailError'), + + actions: { + add() { + if (isEmpty(this.get('newUser.firstname'))) { + set(this, 'firstnameError', true); + return $("#newUserFirstname").focus(); + } + if (isEmpty(this.get('newUser.lastname'))) { + set(this, 'lastnameError', true); + return $("#newUserLastname").focus(); + } + if (isEmpty(this.get('newUser.email')) || is.not.email(this.get('newUser.email'))) { + set(this, 'emailError', true); + return $("#newUserEmail").focus(); + } + + let user = get(this, 'newUser'); + + get(this, 'add')(user).then(() => { + this.set('newUser', { firstname: "", lastname: "", email: "", active: true }); + set(this, 'firstnameError', false); + set(this, 'lastnameError', false); + set(this, 'emailError', false); + $("#newUserFirstname").focus(); + }); + } + } +}); diff --git a/app/app/pods/auth/forgot/controller.js b/app/app/pods/auth/forgot/controller.js index 96a57ca7..5fa8aa66 100644 --- a/app/app/pods/auth/forgot/controller.js +++ b/app/app/pods/auth/forgot/controller.js @@ -1,11 +1,11 @@ // Copyright 2016 Documize Inc. . All rights reserved. // -// This software (Documize Community Edition) is licensed under +// This software (Documize Community Edition) is licensed under // GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html // // You can operate outside the AGPL restrictions by purchasing // Documize Enterprise Edition and obtaining a commercial license -// by contacting . +// by contacting . // // https://documize.com @@ -13,23 +13,10 @@ import Ember from 'ember'; export default Ember.Controller.extend({ userService: Ember.inject.service('user'), - email: "", - sayThanks: false, actions: { - forgot: function () { - var self = this; - var email = this.get('email'); - - if (is.empty(email)) { - $("#email").addClass("error").focus(); - return; - } - - self.set('sayThanks', true); - this.set('email', ''); - - this.get('userService').forgotPassword(email); + forgot: function (email) { + return this.get('userService').forgotPassword(email); } } -}); \ No newline at end of file +}); diff --git a/app/app/pods/auth/forgot/template.hbs b/app/app/pods/auth/forgot/template.hbs index 90ddfdc1..886add5a 100644 --- a/app/app/pods/auth/forgot/template.hbs +++ b/app/app/pods/auth/forgot/template.hbs @@ -3,20 +3,6 @@ - - {{#if sayThanks}} - Thanks. Check your email for instructions. - {{else}} - - Email - {{focus-input type="email" value=email id="email"}} - - - - Reset - - {{/if}} - {{#link-to 'auth.login'}}Sign In{{/link-to}} - + {{forgot-password forgot=(action 'forgot')}} diff --git a/app/app/pods/auth/reset/controller.js b/app/app/pods/auth/reset/controller.js index de4870a6..37fe2b87 100644 --- a/app/app/pods/auth/reset/controller.js +++ b/app/app/pods/auth/reset/controller.js @@ -1,11 +1,11 @@ // Copyright 2016 Documize Inc. . All rights reserved. // -// This software (Documize Community Edition) is licensed under +// This software (Documize Community Edition) is licensed under // GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html // // You can operate outside the AGPL restrictions by purchasing // Documize Enterprise Edition and obtaining a commercial license -// by contacting . +// by contacting . // // https://documize.com @@ -18,31 +18,10 @@ export default Ember.Controller.extend({ mustMatch: false, actions: { - reset() { - let self = this; - let password = this.get('password'); - let passwordConfirm = this.get('passwordConfirm'); - - if (is.empty(password)) { - $("#newPassword").addClass("error").focus(); - return; - } - - if (is.empty(passwordConfirm)) { - $("#passwordConfirm").addClass("error").focus(); - return; - } - - if (is.not.equal(password, passwordConfirm)) { - $("#newPassword").addClass("error").focus(); - $("#passwordConfirm").addClass("error"); - self.set('mustMatch', true); - return; - } - - this.get('userService').resetPassword(self.model, password).then(function (response) { /* jshint ignore:line */ - self.transitionToRoute('auth.login'); + reset(password) { + return this.get('userService').resetPassword(this.model, password).then(() => { /* jshint ignore:line */ + this.transitionToRoute('auth.login'); }); } } -}); \ No newline at end of file +}); diff --git a/app/app/pods/auth/reset/template.hbs b/app/app/pods/auth/reset/template.hbs index 2f26e5fb..78b551bb 100644 --- a/app/app/pods/auth/reset/template.hbs +++ b/app/app/pods/auth/reset/template.hbs @@ -2,23 +2,5 @@ - - - - New Password - Choose a strong password - {{focus-input type="password" value=password id="newPassword"}} - - - Confirm Password - Please type your new password again - {{input type="password" value=passwordConfirm id="passwordConfirm"}} - - - - Reset - Passwords must match - - - + {{password-reset reset=(action 'reset')}} diff --git a/app/app/pods/customize/general/controller.js b/app/app/pods/customize/general/controller.js index 501c0d2b..604e8976 100644 --- a/app/app/pods/customize/general/controller.js +++ b/app/app/pods/customize/general/controller.js @@ -1,11 +1,11 @@ // Copyright 2016 Documize Inc. . All rights reserved. // -// This software (Documize Community Edition) is licensed under +// This software (Documize Community Edition) is licensed under // GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html // // You can operate outside the AGPL restrictions by purchasing // Documize Enterprise Edition and obtaining a commercial license -// by contacting . +// by contacting . // // https://documize.com @@ -17,19 +17,9 @@ export default Ember.Controller.extend(NotifierMixin, { actions: { save() { - if (is.empty(this.model.get('title'))) { - $("#siteTitle").addClass("error").focus(); - return; - } - - if (is.empty(this.model.get('message'))) { - $("#siteMessage").addClass("error").focus(); - return; - } - - this.model.set('allowAnonymousAccess', Ember.$("#allowAnonymousAccess").prop('checked')); - this.get('orgService').save(this.model); - this.showNotification('Saved'); + return this.get('orgService').save(this.model).then(() => { + this.showNotification('Saved'); + }); } } -}); \ No newline at end of file +}); diff --git a/app/app/pods/customize/general/template.hbs b/app/app/pods/customize/general/template.hbs index c161a5c8..adb167fa 100644 --- a/app/app/pods/customize/general/template.hbs +++ b/app/app/pods/customize/general/template.hbs @@ -1,29 +1,3 @@ - - - General Settings - Tell people about this Documize instance - - - - Title - Describe the title of this Documize instance - {{focus-input id="siteTitle" type="text" value=model.title}} - - - Message - Describe the purpose of this Documize instance - {{textarea id="siteMessage" rows="3" value=model.message}} - - - Anonymous Access - Content within "Everyone" will be made available to anonymous users - - - Allow anyone to access this Documize instance - - - save - - +{{general-settings model=model save=(action 'save')}} diff --git a/app/app/pods/customize/users/controller.js b/app/app/pods/customize/users/controller.js index 03680753..7dddaad0 100644 --- a/app/app/pods/customize/users/controller.js +++ b/app/app/pods/customize/users/controller.js @@ -1,11 +1,11 @@ // Copyright 2016 Documize Inc. . All rights reserved. // -// This software (Documize Community Edition) is licensed under +// This software (Documize Community Edition) is licensed under // GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html // // You can operate outside the AGPL restrictions by purchasing // Documize Enterprise Edition and obtaining a commercial license -// by contacting . +// by contacting . // // https://documize.com @@ -17,30 +17,13 @@ export default Ember.Controller.extend(NotifierMixin, { newUser: { firstname: "", lastname: "", email: "", active: true }, actions: { - add: function () { - if (is.empty(this.newUser.firstname)) { - $("#newUserFirstname").addClass("error").focus(); - return; - } - if (is.empty(this.newUser.lastname)) { - $("#newUserLastname").addClass("error").focus(); - return; - } - if (is.empty(this.newUser.email) || is.not.email(this.newUser.email)) { - $("#newUserEmail").addClass("error").focus(); - return; - } + add(user) { + Ember.set(this, 'newUser', user); - $("#newUserFirstname").removeClass("error"); - $("#newUserLastname").removeClass("error"); - $("#newUserEmail").removeClass("error"); - - this.get('userService') + return this.get('userService') .add(this.get('newUser')) .then((user) => { this.showNotification('Added'); - this.set('newUser', { firstname: "", lastname: "", email: "", active: true }); - $("#newUserFirstname").focus(); this.get('model').pushObject(user); }) .catch(function (error) { @@ -76,4 +59,4 @@ export default Ember.Controller.extend(NotifierMixin, { this.showNotification('Password changed'); } } -}); \ No newline at end of file +}); diff --git a/app/app/pods/customize/users/template.hbs b/app/app/pods/customize/users/template.hbs index e858f5a8..555cf597 100644 --- a/app/app/pods/customize/users/template.hbs +++ b/app/app/pods/customize/users/template.hbs @@ -1,23 +1,5 @@ - - - Add User - New users receive an invitation email with a random password - - - Firstname - {{focus-input id="newUserFirstname" type="text" value=newUser.firstname}} - - - Lastname - {{input id="newUserLastname" type="text" value=newUser.lastname}} - - - Email - {{input id="newUserEmail" type="text" value=newUser.email}} - - Add - + {{user-settings add=(action 'add')}} {{settings/user-list users=model onDelete=(action "onDelete") onSave=(action "onSave") onPassword=(action "onPassword")}} diff --git a/app/app/pods/profile/controller.js b/app/app/pods/profile/controller.js index 431dc2b8..fb4d2edd 100644 --- a/app/app/pods/profile/controller.js +++ b/app/app/pods/profile/controller.js @@ -1,62 +1,37 @@ // Copyright 2016 Documize Inc. . All rights reserved. // -// This software (Documize Community Edition) is licensed under +// This software (Documize Community Edition) is licensed under // GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html // // You can operate outside the AGPL restrictions by purchasing // Documize Enterprise Edition and obtaining a commercial license -// by contacting . +// by contacting . // // https://documize.com import Ember from 'ember'; +const { + isPresent +} = Ember; + export default Ember.Controller.extend({ userService: Ember.inject.service('user'), - password: { password: "", confirmation: "" }, session: Ember.inject.service(), actions: { - save: function () { - if (is.empty(this.model.get('firstname'))) { - $("#firstname").addClass("error").focus(); - return; - } - if (is.empty(this.model.get('lastname'))) { - $("#lastname").addClass("error").focus(); - return; - } - if (is.empty(this.model.get('email'))) { - $("#email").addClass("error").focus(); - return; - } - if (is.not.empty(this.password.password) && is.empty(this.password.confirmation)) { - $("#confirmPassword").addClass("error").focus(); - return; - } - if (is.empty(this.password.password) && is.not.empty(this.password.confirmation)) { - $("#password").addClass("error").focus(); - return; - } - if (is.not.equal(this.password.password, this.password.confirmation)) { - $("#password").addClass("error").focus(); - return; - } + save(passwords) { + let password = passwords.password; + let confirmation = passwords.confirmation; - let self = this; - - this.get('userService').save(this.model).then(function () { - if (is.not.empty(self.password.password) && is.not.empty(self.password.confirmation)) { - self.get('userService').updatePassword(self.model.get('id'), self.password.password).then(function () { - self.password.password = ""; - self.password.confirmation = ""; - }); + return this.get('userService').save(this.model).then(() => { + if (isPresent(password) && isPresent(confirmation)) { + this.get('userService').updatePassword(this.get('model.id'), password); } - self.model.generateInitials(); - self.get('session').set('user', self.model); + this.model.generateInitials(); + this.get('session').set('user', this.model); + this.transitionToRoute('folders'); }); - - this.transitionToRoute('folders'); } } -}); \ No newline at end of file +}); diff --git a/app/app/pods/profile/template.hbs b/app/app/pods/profile/template.hbs index 69cc6f6d..f01bedae 100644 --- a/app/app/pods/profile/template.hbs +++ b/app/app/pods/profile/template.hbs @@ -13,33 +13,7 @@ {{/layout/zone-sidebar}} {{#layout/zone-content}} - - - - Firstname - {{focus-input id="firstname" type="text" value=model.firstname}} - - - Lastname - {{input id="lastname" type="text" value=model.lastname}} - - - Email - {{input id="email" type="text" value=model.email}} - - - Password - Optional change your password - {{input id="password" type="password" value=password.password}} - - - Confirm Password - Confirm your new password - {{input id="confirmPassword" type="password" value=password.confirmation}} - - save - - + {{user-profile model=model save=(action 'save')}} {{/layout/zone-content}} {{/layout/zone-container}} diff --git a/app/app/pods/setup/controller.js b/app/app/pods/setup/controller.js index 611277ab..6bfffb01 100644 --- a/app/app/pods/setup/controller.js +++ b/app/app/pods/setup/controller.js @@ -1,11 +1,11 @@ // Copyright 2016 Documize Inc. . All rights reserved. // -// This software (Documize Community Edition) is licensed under +// This software (Documize Community Edition) is licensed under // GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html // // You can operate outside the AGPL restrictions by purchasing // Documize Enterprise Edition and obtaining a commercial license -// by contacting . +// by contacting . // // https://documize.com @@ -19,39 +19,7 @@ export default Ember.Controller.extend(NotifierMixin, { actions: { save() { - if (is.empty(this.model.title)) { - $("#siteTitle").addClass("error").focus(); - return; - } - - if (is.empty(this.model.firstname)) { - $("#adminFirstname").addClass("error").focus(); - return; - } - - if (is.empty(this.model.lastname)) { - $("#adminLastname").addClass("error").focus(); - return; - } - - if (is.empty(this.model.email)) { - $("#adminEmail").addClass("error").focus(); - return; - } - - if (!is.email(this.model.email)) { - $("#adminEmail").addClass("error").focus(); - return; - } - - if (is.empty(this.model.password)) { - $("#adminPassword").addClass("error").focus(); - return; - } - - this.model.allowAnonymousAccess = Ember.$("#allowAnonymousAccess").prop('checked'); - - this.get('ajax').request("/setup", { + return this.get('ajax').request("/setup", { method: 'POST', data: this.model, dataType: "text", @@ -64,4 +32,4 @@ export default Ember.Controller.extend(NotifierMixin, { }); } } -}); \ No newline at end of file +}); diff --git a/app/app/pods/setup/template.hbs b/app/app/pods/setup/template.hbs index 25715da1..a49de2fa 100644 --- a/app/app/pods/setup/template.hbs +++ b/app/app/pods/setup/template.hbs @@ -9,46 +9,7 @@ + {{documize-setup model=model save=(action 'save')}} - - - - - Let's setup Documize - Database name is {{model.dbname}} - - - - Team - What's your tribe called? - {{focus-input id="siteTitle" type="text" value=model.title}} - - - Firstname - What do people call you? - {{input id="adminFirstname" type="text" value=model.firstname}} - - - Lastname - How the government refers to you. - {{input id="adminLastname" type="text" value=model.lastname}} - - - Email - No spam. Ever! - {{input id="adminEmail" type="email" value=model.email}} - - - Password - Something you can remember without writing it down. - {{input id="adminPassword" type="text" value=model.password}} - - Go Setup - - - - - - diff --git a/app/app/services/user.js b/app/app/services/user.js index a8385ee1..28df54e9 100644 --- a/app/app/services/user.js +++ b/app/app/services/user.js @@ -126,4 +126,4 @@ export default Ember.Service.extend({ data: password }); } -}); \ No newline at end of file +}); diff --git a/app/app/templates/components/documize-setup.hbs b/app/app/templates/components/documize-setup.hbs new file mode 100644 index 00000000..ab0df0f7 --- /dev/null +++ b/app/app/templates/components/documize-setup.hbs @@ -0,0 +1,38 @@ + + + + + Let's setup Documize + Database name is {{model.dbname}} + + + + Team + What's your tribe called? + {{focus-input id="siteTitle" type="text" value=model.title class=(if hasEmptyTitleError 'error')}} + + + Firstname + What do people call you? + {{input id="adminFirstname" type="text" value=model.firstname class=(if hasEmptyFirstnameError 'error')}} + + + Lastname + How the government refers to you. + {{input id="adminLastname" type="text" value=model.lastname class=(if hasEmptyLastnameError 'error')}} + + + Email + No spam. Ever! + {{input id="adminEmail" type="email" value=model.email class=(if hasEmptyEmailError 'error')}} + + + Password + Something you can remember without writing it down. + {{input id="adminPassword" type="text" value=model.password class=(if hasEmptyPasswordError 'error')}} + + Go Setup + + + + diff --git a/app/app/templates/components/forgot-password.hbs b/app/app/templates/components/forgot-password.hbs new file mode 100644 index 00000000..88c0aedc --- /dev/null +++ b/app/app/templates/components/forgot-password.hbs @@ -0,0 +1,15 @@ + + {{#if sayThanks}} + Thanks. Check your email for instructions. + {{else}} + + Email + {{focus-input type="email" value=email id="email" class=(if hasEmptyEmailError 'error')}} + + + + Reset + + {{/if}} + {{#link-to 'auth.login'}}Sign In{{/link-to}} + diff --git a/app/app/templates/components/general-settings.hbs b/app/app/templates/components/general-settings.hbs new file mode 100644 index 00000000..6057bdc6 --- /dev/null +++ b/app/app/templates/components/general-settings.hbs @@ -0,0 +1,27 @@ + + + General Settings + Tell people about this Documize instance + + + + Title + Describe the title of this Documize instance + {{focus-input id="siteTitle" type="text" value=model.title class=(if hasTitleInputError 'error')}} + + + Message + Describe the purpose of this Documize instance + {{textarea id="siteMessage" rows="3" value=model.message class=(if hasMessageInputError 'error')}} + + + Anonymous Access + Content within "Everyone" will be made available to anonymous users + + + Allow anyone to access this Documize instance + + + save + + diff --git a/app/app/templates/components/password-reset.hbs b/app/app/templates/components/password-reset.hbs new file mode 100644 index 00000000..8b65c352 --- /dev/null +++ b/app/app/templates/components/password-reset.hbs @@ -0,0 +1,19 @@ + + + + New Password + Choose a strong password + {{focus-input type="password" value=password id="newPassword" class=(if hasPasswordError 'error')}} + + + Confirm Password + Please type your new password again + {{input type="password" value=passwordConfirm id="passwordConfirm" class=(if hasConfirmError 'error')}} + + + + Reset + Passwords must match + + + diff --git a/app/app/templates/components/user-profile.hbs b/app/app/templates/components/user-profile.hbs new file mode 100644 index 00000000..1d98a10f --- /dev/null +++ b/app/app/templates/components/user-profile.hbs @@ -0,0 +1,27 @@ + + + + Firstname + {{focus-input id="firstname" type="text" value=model.firstname class=(if hasFirstnameError 'error')}} + + + Lastname + {{input id="lastname" type="text" value=model.lastname class=(if hasLastnameError 'error')}} + + + Email + {{input id="email" type="text" value=model.email class=(if hasEmailError 'error')}} + + + Password + Optional change your password + {{input id="password" type="password" value=password.password class=hasPasswordError}} + + + Confirm Password + Confirm your new password + {{input id="confirmPassword" type="password" value=password.confirmation class=hasConfirmPasswordError}} + + save + + diff --git a/app/app/templates/components/user-settings.hbs b/app/app/templates/components/user-settings.hbs new file mode 100644 index 00000000..da6a8d34 --- /dev/null +++ b/app/app/templates/components/user-settings.hbs @@ -0,0 +1,19 @@ + + + Add User + New users receive an invitation email with a random password + + + Firstname + {{focus-input id="newUserFirstname" type="text" value=newUser.firstname class=(if hasFirstnameEmptyError 'error')}} + + + Lastname + {{input id="newUserLastname" type="text" value=newUser.lastname class=(if hasLastnameEmptyError 'error')}} + + + Email + {{input id="newUserEmail" type="text" value=newUser.email class=(if hasEmailEmptyError 'error')}} + + Add + diff --git a/core/api/request/init.go b/core/api/request/init.go index 8b6737ce..c182359d 100644 --- a/core/api/request/init.go +++ b/core/api/request/init.go @@ -50,8 +50,9 @@ func init() { var err error environment.GetString(&connectionString, "db", true, - `"username:password@protocol(hostname:port)/databasename" for example "fred:bloggs@tcp(localhost:3306)/documize"`, + `'username:password@protocol(hostname:port)/databasename" for example "fred:bloggs@tcp(localhost:3306)/documize"`, func(*string, string) bool { + Db, err = sqlx.Open("mysql", stdConn(connectionString)) if err != nil { diff --git a/core/database/create.go b/core/database/create.go index e4a2e803..4c8c953b 100644 --- a/core/database/create.go +++ b/core/database/create.go @@ -19,9 +19,9 @@ import ( "time" "github.com/documize/community/core/api/util" - "github.com/documize/community/core/web" "github.com/documize/community/core/log" "github.com/documize/community/core/utility" + "github.com/documize/community/core/web" ) // runSQL creates a transaction per call diff --git a/core/database/migrate.go b/core/database/migrate.go index 066d5ba4..2d97976c 100644 --- a/core/database/migrate.go +++ b/core/database/migrate.go @@ -13,16 +13,20 @@ package database import ( "bytes" + "crypto/rand" "database/sql" + "fmt" + "os" "regexp" "sort" "strings" + "time" "github.com/jmoiron/sqlx" - "github.com/documize/community/core/web" "github.com/documize/community/core/log" "github.com/documize/community/core/utility" + "github.com/documize/community/core/web" ) const migrationsDir = "bindata/scripts" @@ -69,97 +73,176 @@ func migrations(lastMigration string) (migrationsT, error) { func (m migrationsT) migrate(tx *sqlx.Tx) error { for _, v := range m { log.Info("Processing migration file: " + v) + buf, err := web.Asset(migrationsDir + "/" + v) if err != nil { return err } - //fmt.Println("DEBUG database.Migrate() ", v, ":\n", string(buf)) // TODO actually run the SQL + err = processSQLfile(tx, buf) if err != nil { return err } + json := `{"database":"` + v + `"}` sql := "INSERT INTO `config` (`key`,`config`) " + "VALUES ('META','" + json + "') ON DUPLICATE KEY UPDATE `config`='" + json + "';" - _, err = tx.Exec(sql) + _, err = tx.Exec(sql) // add a record in the config file to say we have done the upgrade if err != nil { return err } - - //fmt.Println("DEBUG insert 10s wait for testing") - //time.Sleep(10 * time.Second) } return nil } -func migrateEnd(tx *sqlx.Tx, err error) error { - if tx != nil { - _, ulerr := tx.Exec("UNLOCK TABLES;") - log.IfErr(ulerr) - if err == nil { - log.IfErr(tx.Commit()) - log.Info("Database checks: completed") - return nil - } - log.IfErr(tx.Rollback()) +func lockDB() (bool, error) { + b := make([]byte, 2) + _, err := rand.Read(b) + if err != nil { + return false, err } - log.Error("Database checks: failed: ", err) - return err + wait := ((time.Duration(b[0]) << 8) | time.Duration(b[1])) * time.Millisecond / 10 // up to 6.5 secs wait + time.Sleep(wait) + + tx, err := (*dbPtr).Beginx() + if err != nil { + return false, err + } + + _, err = tx.Exec("LOCK TABLE `config` WRITE;") + if err != nil { + return false, err + } + + defer func() { + _, err = tx.Exec("UNLOCK TABLES;") + log.IfErr(err) + log.IfErr(tx.Commit()) + }() + + _, err = tx.Exec("INSERT INTO `config` (`key`,`config`) " + + fmt.Sprintf(`VALUES ('DBLOCK','{"pid": "%d"}');`, os.Getpid())) + if err != nil { + // good error would be "Error 1062: Duplicate entry 'DBLOCK' for key 'idx_config_area'" + if strings.HasPrefix(err.Error(), "Error 1062:") { + log.Info("Database locked by annother Documize instance") + return false, nil + } + return false, err + } + + log.Info("Database locked by this Documize instance") + return true, err // success! +} + +func unlockDB() error { + tx, err := (*dbPtr).Beginx() + if err != nil { + return err + } + _, err = tx.Exec("DELETE FROM `config` WHERE `key`='DBLOCK';") + if err != nil { + return err + } + return tx.Commit() +} + +func migrateEnd(tx *sqlx.Tx, err error, amLeader bool) error { + if amLeader { + defer func() { log.IfErr(unlockDB()) }() + if tx != nil { + if err == nil { + log.IfErr(tx.Commit()) + log.Info("Database checks: completed") + return nil + } + log.IfErr(tx.Rollback()) + } + log.Error("Database checks: failed: ", err) + return err + } + return nil // not the leader, so ignore errors +} + +func getLastMigration(tx *sqlx.Tx) (lastMigration string, err error) { + var stmt *sql.Stmt + stmt, err = tx.Prepare("SELECT JSON_EXTRACT(`config`,'$.database') FROM `config` WHERE `key` = 'META';") + if err == nil { + defer utility.Close(stmt) + var item = make([]uint8, 0) + + row := stmt.QueryRow() + + err = row.Scan(&item) + if err == nil { + if len(item) > 1 { + q := []byte(`"`) + lastMigration = string(bytes.TrimPrefix(bytes.TrimSuffix(item, q), q)) + } + } + } + return } // Migrate the database as required, consolidated action. func Migrate(ConfigTableExists bool) error { - lastMigration := "" + amLeader := false + + if ConfigTableExists { + var err error + amLeader, err = lockDB() + log.IfErr(err) + } else { + amLeader = true // what else can you do? + } tx, err := (*dbPtr).Beginx() if err != nil { - return migrateEnd(tx, err) + return migrateEnd(tx, err, amLeader) } + lastMigration := "" + if ConfigTableExists { - _, err = tx.Exec("LOCK TABLE `config` WRITE;") + lastMigration, err = getLastMigration(tx) if err != nil { - return migrateEnd(tx, err) - } - - log.Info("Database checks: lock taken") - - var stmt *sql.Stmt - stmt, err = tx.Prepare("SELECT JSON_EXTRACT(`config`,'$.database') FROM `config` WHERE `key` = 'META';") - if err == nil { - defer utility.Close(stmt) - var item = make([]uint8, 0) - - row := stmt.QueryRow() - - err = row.Scan(&item) - if err != nil { - return migrateEnd(tx, err) - } - - if len(item) > 1 { - q := []byte(`"`) - lastMigration = string(bytes.TrimPrefix(bytes.TrimSuffix(item, q), q)) - } + return migrateEnd(tx, err, amLeader) } log.Info("Database checks: last previously applied file was " + lastMigration) } mig, err := migrations(lastMigration) if err != nil { - return migrateEnd(tx, err) + return migrateEnd(tx, err, amLeader) } if len(mig) == 0 { log.Info("Database checks: no updates to perform") - return migrateEnd(tx, nil) // no migrations to perform + return migrateEnd(tx, nil, amLeader) // no migrations to perform } - log.Info("Database checks: will execute the following update files: " + strings.Join([]string(mig), ", ")) - return migrateEnd(tx, mig.migrate(tx)) + if amLeader { + log.Info("Database checks: will execute the following update files: " + strings.Join([]string(mig), ", ")) + return migrateEnd(tx, mig.migrate(tx), amLeader) + } + + // a follower instance + targetMigration := string(mig[len(mig)-1]) + for targetMigration != lastMigration { + time.Sleep(time.Second) + log.Info("Waiting for database migration process to complete") + tx.Rollback() // ignore error + tx, err := (*dbPtr).Beginx() // need this in order to see the changed situation since last tx + if err != nil { + return migrateEnd(tx, err, amLeader) + } + lastMigration, _ = getLastMigration(tx) + } + + return migrateEnd(tx, nil, amLeader) } func processSQLfile(tx *sqlx.Tx, buf []byte) error { diff --git a/core/product.go b/core/product.go index ddcfee0d..8cdf74e8 100644 --- a/core/product.go +++ b/core/product.go @@ -27,7 +27,7 @@ type ProdInfo struct { func Product() (p ProdInfo) { p.Major = "0" p.Minor = "16" - p.Patch = "0" + p.Patch = "1" p.Version = fmt.Sprintf("%s.%s.%s", p.Major, p.Minor, p.Patch) p.Edition = "Community" p.Title = fmt.Sprintf("%s Edition", p.Edition)