1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-19 13:19:43 +02:00

Provide in-app What's New & new release notifications

This commit is contained in:
sauls8t 2018-03-22 17:29:59 +00:00
parent 97cc5374f0
commit eaf46f06c1
25 changed files with 1166 additions and 772 deletions

View file

@ -24,4 +24,7 @@ INSERT INTO search (orgid, documentid, itemid, itemtype, content)
SELECT orgid, documentid, refid as itemid, "page" as itemtype, title as content SELECT orgid, documentid, refid as itemid, "page" as itemtype, title as content
FROM page WHERE status=0 FROM page WHERE status=0
-- whats new support
ALTER TABLE user ADD COLUMN `lastversion` CHAR(16) NOT NULL DEFAULT '' AFTER `active`;
-- deprecations -- deprecations

View file

@ -39,6 +39,7 @@ type RequestContext struct {
Expires time.Time Expires time.Time
Fullname string Fullname string
Transaction *sqlx.Tx Transaction *sqlx.Tx
AppVersion string
} }
//GetAppURL returns full HTTP url for the app //GetAppURL returns full HTTP url for the app

View file

@ -100,6 +100,7 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
requestedPassword := secrets.GenerateRandomPassword() requestedPassword := secrets.GenerateRandomPassword()
userModel.Salt = secrets.GenerateSalt() userModel.Salt = secrets.GenerateSalt()
userModel.Password = secrets.GeneratePassword(requestedPassword, userModel.Salt) userModel.Password = secrets.GeneratePassword(requestedPassword, userModel.Salt)
userModel.LastVersion = ctx.AppVersion
// only create account if not dupe // only create account if not dupe
addUser := true addUser := true

View file

@ -35,8 +35,8 @@ func (s Scope) Add(ctx domain.RequestContext, u user.User) (err error) {
u.Created = time.Now().UTC() u.Created = time.Now().UTC()
u.Revised = time.Now().UTC() u.Revised = time.Now().UTC()
_, err = ctx.Transaction.Exec("INSERT INTO user (refid, firstname, lastname, email, initials, password, salt, reset, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", _, err = ctx.Transaction.Exec("INSERT INTO user (refid, firstname, lastname, email, initials, password, salt, reset, lastversion, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
u.RefID, u.Firstname, u.Lastname, strings.ToLower(u.Email), u.Initials, u.Password, u.Salt, "", u.Created, u.Revised) u.RefID, u.Firstname, u.Lastname, strings.ToLower(u.Email), u.Initials, u.Password, u.Salt, "", u.LastVersion, u.Created, u.Revised)
if err != nil { if err != nil {
err = errors.Wrap(err, "execute user insert") err = errors.Wrap(err, "execute user insert")
@ -47,7 +47,7 @@ func (s Scope) Add(ctx domain.RequestContext, u user.User) (err error) {
// Get returns the user record for the given id. // Get returns the user record for the given id.
func (s Scope) Get(ctx domain.RequestContext, id string) (u user.User, err error) { func (s Scope) Get(ctx domain.RequestContext, id string) (u user.User, err error) {
err = s.Runtime.Db.Get(&u, "SELECT id, refid, firstname, lastname, email, initials, global, password, salt, reset, created, revised FROM user WHERE refid=?", id) err = s.Runtime.Db.Get(&u, "SELECT id, refid, firstname, lastname, email, initials, global, password, salt, reset, lastversion, created, revised FROM user WHERE refid=?", id)
if err != nil { if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute select for user %s", id)) err = errors.Wrap(err, fmt.Sprintf("unable to execute select for user %s", id))
@ -60,7 +60,7 @@ func (s Scope) Get(ctx domain.RequestContext, id string) (u user.User, err error
func (s Scope) GetByDomain(ctx domain.RequestContext, domain, email string) (u user.User, err error) { func (s Scope) GetByDomain(ctx domain.RequestContext, domain, email string) (u user.User, err error) {
email = strings.TrimSpace(strings.ToLower(email)) email = strings.TrimSpace(strings.ToLower(email))
err = s.Runtime.Db.Get(&u, "SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.global, u.password, u.salt, u.reset, u.created, u.revised FROM user u, account a, organization o WHERE TRIM(LOWER(u.email))=? AND u.refid=a.userid AND a.orgid=o.refid AND TRIM(LOWER(o.domain))=?", err = s.Runtime.Db.Get(&u, "SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.global, u.password, u.salt, u.reset, u.lastversion, u.created, u.revised FROM user u, account a, organization o WHERE TRIM(LOWER(u.email))=? AND u.refid=a.userid AND a.orgid=o.refid AND TRIM(LOWER(o.domain))=?",
email, domain) email, domain)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
@ -74,7 +74,7 @@ func (s Scope) GetByDomain(ctx domain.RequestContext, domain, email string) (u u
func (s Scope) GetByEmail(ctx domain.RequestContext, email string) (u user.User, err error) { func (s Scope) GetByEmail(ctx domain.RequestContext, email string) (u user.User, err error) {
email = strings.TrimSpace(strings.ToLower(email)) email = strings.TrimSpace(strings.ToLower(email))
err = s.Runtime.Db.Get(&u, "SELECT id, refid, firstname, lastname, email, initials, global, password, salt, reset, created, revised FROM user WHERE TRIM(LOWER(email))=?", email) err = s.Runtime.Db.Get(&u, "SELECT id, refid, firstname, lastname, email, initials, global, password, salt, reset, lastversion, created, revised FROM user WHERE TRIM(LOWER(email))=?", email)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
err = errors.Wrap(err, fmt.Sprintf("execute select user by email %s", email)) err = errors.Wrap(err, fmt.Sprintf("execute select user by email %s", email))
@ -85,7 +85,7 @@ func (s Scope) GetByEmail(ctx domain.RequestContext, email string) (u user.User,
// GetByToken returns a user record given a reset token value. // GetByToken returns a user record given a reset token value.
func (s Scope) GetByToken(ctx domain.RequestContext, token string) (u user.User, err error) { func (s Scope) GetByToken(ctx domain.RequestContext, token string) (u user.User, err error) {
err = s.Runtime.Db.Get(&u, "SELECT id, refid, firstname, lastname, email, initials, global, password, salt, reset, created, revised FROM user WHERE reset=?", token) err = s.Runtime.Db.Get(&u, "SELECT id, refid, firstname, lastname, email, initials, global, password, salt, reset, lastversion, created, revised FROM user WHERE reset=?", token)
if err != nil { if err != nil {
err = errors.Wrap(err, fmt.Sprintf("execute user select by token %s", token)) err = errors.Wrap(err, fmt.Sprintf("execute user select by token %s", token))
@ -98,7 +98,7 @@ func (s Scope) GetByToken(ctx domain.RequestContext, token string) (u user.User,
// This occurs when we you share a folder with a new user and they have to complete // This occurs when we you share a folder with a new user and they have to complete
// the onboarding process. // the onboarding process.
func (s Scope) GetBySerial(ctx domain.RequestContext, serial string) (u user.User, err error) { func (s Scope) GetBySerial(ctx domain.RequestContext, serial string) (u user.User, err error) {
err = s.Runtime.Db.Get(&u, "SELECT id, refid, firstname, lastname, email, initials, global, password, salt, reset, created, revised FROM user WHERE salt=?", serial) err = s.Runtime.Db.Get(&u, "SELECT id, refid, firstname, lastname, email, initials, global, password, salt, reset, lastversion, created, revised FROM user WHERE salt=?", serial)
if err != nil { if err != nil {
err = errors.Wrap(err, fmt.Sprintf("execute user select by serial %s", serial)) err = errors.Wrap(err, fmt.Sprintf("execute user select by serial %s", serial))
@ -111,7 +111,7 @@ func (s Scope) GetBySerial(ctx domain.RequestContext, serial string) (u user.Use
// identified in the Persister. // identified in the Persister.
func (s Scope) GetActiveUsersForOrganization(ctx domain.RequestContext) (u []user.User, err error) { func (s Scope) GetActiveUsersForOrganization(ctx domain.RequestContext) (u []user.User, err error) {
err = s.Runtime.Db.Select(&u, err = s.Runtime.Db.Select(&u,
`SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.created, u.revised, `SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.lastversion, u.created, u.revised,
u.global, a.active, a.editor, a.admin, a.users as viewusers u.global, a.active, a.editor, a.admin, a.users as viewusers
FROM user u, account a FROM user u, account a
WHERE u.refid=a.userid AND a.orgid=? AND a.active=1 WHERE u.refid=a.userid AND a.orgid=? AND a.active=1
@ -139,7 +139,7 @@ func (s Scope) GetUsersForOrganization(ctx domain.RequestContext, filter string)
} }
err = s.Runtime.Db.Select(&u, err = s.Runtime.Db.Select(&u,
`SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.created, u.revised, `SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.lastversion, u.created, u.revised,
u.global, a.active, a.editor, a.admin, a.users as viewusers u.global, a.active, a.editor, a.admin, a.users as viewusers
FROM user u, account a FROM user u, account a
WHERE u.refid=a.userid AND a.orgid=? `+likeQuery+ WHERE u.refid=a.userid AND a.orgid=? `+likeQuery+
@ -160,7 +160,7 @@ func (s Scope) GetUsersForOrganization(ctx domain.RequestContext, filter string)
// GetSpaceUsers returns a slice containing all user records for given space. // GetSpaceUsers returns a slice containing all user records for given space.
func (s Scope) GetSpaceUsers(ctx domain.RequestContext, spaceID string) (u []user.User, err error) { func (s Scope) GetSpaceUsers(ctx domain.RequestContext, spaceID string) (u []user.User, err error) {
err = s.Runtime.Db.Select(&u, ` err = s.Runtime.Db.Select(&u, `
SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.created, u.revised, u.global, SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.created, u.lastversion, u.revised, u.global,
a.active, a.users AS viewusers, a.editor, a.admin a.active, a.users AS viewusers, a.editor, a.admin
FROM user u, account a FROM user u, account a
WHERE a.orgid=? AND u.refid = a.userid AND a.active=1 AND u.refid IN ( WHERE a.orgid=? AND u.refid = a.userid AND a.active=1 AND u.refid IN (
@ -189,7 +189,7 @@ func (s Scope) GetUsersForSpaces(ctx domain.RequestContext, spaces []string) (u
} }
query, args, err := sqlx.In(` query, args, err := sqlx.In(`
SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.created, u.revised, u.global, SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.lastversion, u.created, u.revised, u.global,
a.active, a.users AS viewusers, a.editor, a.admin a.active, a.users AS viewusers, a.editor, a.admin
FROM user u, account a FROM user u, account a
WHERE a.orgid=? AND u.refid = a.userid AND a.active=1 AND u.refid IN ( WHERE a.orgid=? AND u.refid = a.userid AND a.active=1 AND u.refid IN (
@ -219,7 +219,7 @@ func (s Scope) UpdateUser(ctx domain.RequestContext, u user.User) (err error) {
u.Email = strings.ToLower(u.Email) u.Email = strings.ToLower(u.Email)
_, err = ctx.Transaction.NamedExec( _, err = ctx.Transaction.NamedExec(
"UPDATE user SET firstname=:firstname, lastname=:lastname, email=:email, revised=:revised, initials=:initials WHERE refid=:refid", &u) "UPDATE user SET firstname=:firstname, lastname=:lastname, email=:email, revised=:revised, initials=:initials, lastversion=:lastversion WHERE refid=:refid", &u)
if err != nil { if err != nil {
err = errors.Wrap(err, fmt.Sprintf("execute user update %s", u.RefID)) err = errors.Wrap(err, fmt.Sprintf("execute user update %s", u.RefID))
@ -289,7 +289,7 @@ func (s Scope) MatchUsers(ctx domain.RequestContext, text string, maxMatches int
} }
err = s.Runtime.Db.Select(&u, err = s.Runtime.Db.Select(&u,
`SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.created, u.revised, `SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.lastversion, u.created, u.revised,
u.global, a.active, a.editor, a.admin, a.users as viewusers u.global, a.active, a.editor, a.admin, a.users as viewusers
FROM user u, account a FROM user u, account a
WHERE a.orgid=? AND u.refid=a.userid AND a.active=1 `+likeQuery+ WHERE a.orgid=? AND u.refid=a.userid AND a.active=1 `+likeQuery+

File diff suppressed because one or more lines are too long

View file

@ -9,11 +9,30 @@
// //
// https://documize.com // https://documize.com
import $ from 'jquery';
import { empty } from '@ember/object/computed'; import { empty } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import Component from '@ember/component'; import Component from '@ember/component';
export default Component.extend({ export default Component.extend({
appMeta: service(),
LicenseError: empty('model.license'), LicenseError: empty('model.license'),
changelog: '',
init() {
this._super(...arguments);
let self = this;
let cacheBuster = + new Date();
$.ajax({
url: `https://storage.googleapis.com/documize/downloads/updates/summary.html?cb=${cacheBuster}`,
type: 'GET',
dataType: 'html',
success: function (response) {
self.set('changelog', response);
}
});
},
actions: { actions: {
saveLicense() { saveLicense() {

View file

@ -9,12 +9,14 @@
// //
// https://documize.com // https://documize.com
import Component from '@ember/component'; import $ from 'jquery';
import { notEmpty } from '@ember/object/computed'; import { notEmpty } from '@ember/object/computed';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service'
import ModalMixin from '../../mixins/modal';
import constants from '../../utils/constants'; import constants from '../../utils/constants';
import Component from '@ember/component';
export default Component.extend({ export default Component.extend(ModalMixin, {
folderService: service('folder'), folderService: service('folder'),
appMeta: service(), appMeta: service(),
session: service(), session: service(),
@ -24,6 +26,8 @@ export default Component.extend({
hasPins: notEmpty('pins'), hasPins: notEmpty('pins'),
hasSpacePins: notEmpty('spacePins'), hasSpacePins: notEmpty('spacePins'),
hasDocumentPins: notEmpty('documentPins'), hasDocumentPins: notEmpty('documentPins'),
hasWhatsNew: false,
newsContent: '',
init() { init() {
this._super(...arguments); this._super(...arguments);
@ -34,6 +38,23 @@ export default Component.extend({
config = JSON.parse(config); config = JSON.parse(config);
this.set('enableLogout', !config.disableLogout); this.set('enableLogout', !config.disableLogout);
} }
this.get('session').hasWhatsNew().then((v) => {
this.set('hasWhatsNew', v);
});
let version = this.get('appMeta.version');
let self = this;
let cacheBuster = + new Date();
$.ajax({
url: `https://storage.googleapis.com/documize/downloads/updates/${version}.html?cb=${cacheBuster}`,
type: 'GET',
dataType: 'html',
success: function (response) {
self.set('newsContent', response);
}
});
}, },
didInsertElement() { didInsertElement() {
@ -80,6 +101,12 @@ export default Component.extend({
let folder = this.get('store').peekRecord('folder', folderId); let folder = this.get('store').peekRecord('folder', folderId);
this.get('router').transitionTo('document', folderId, folder.get('slug'), documentId, 'document'); this.get('router').transitionTo('document', folderId, folder.get('slug'), documentId, 'document');
} }
},
onShowWhatsNewModal() {
this.modalOpen("#whats-new-modal", { "show": true });
this.get('session').seenNewVersion();
this.set('hasWhatsNew', false);
} }
} }
}); });

View file

@ -25,6 +25,7 @@ export default Model.extend({
global: attr('boolean', { defaultValue: false }), global: attr('boolean', { defaultValue: false }),
accounts: attr(), accounts: attr(),
groups: attr(), groups: attr(),
lastVersion: attr('string'),
created: attr(), created: attr(),
revised: attr(), revised: attr(),

View file

@ -10,7 +10,6 @@
// https://documize.com // https://documize.com
import { Promise as EmberPromise } from 'rsvp'; import { Promise as EmberPromise } from 'rsvp';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import Controller from '@ember/controller'; import Controller from '@ember/controller';
import NotifierMixin from "../../../mixins/notifier"; import NotifierMixin from "../../../mixins/notifier";
@ -18,13 +17,13 @@ import NotifierMixin from "../../../mixins/notifier";
export default Controller.extend(NotifierMixin, { export default Controller.extend(NotifierMixin, {
global: service(), global: service(),
appMeta: service(), appMeta: service(),
session: service(), session: service(),
actions: { actions: {
onSave(data) { onSave(data) {
return new EmberPromise((resolve) => { return new EmberPromise((resolve) => {
if(!this.get('session.isGlobalAdmin')) { if (!this.get('session.isGlobalAdmin')) {
resolve(); resolve();
} else { } else {
this.get('global').saveAuthConfig(data).then(() => { this.get('global').saveAuthConfig(data).then(() => {
@ -46,7 +45,7 @@ export default Controller.extend(NotifierMixin, {
this.get('session').logout(); this.get('session').logout();
this.set('appMeta.authProvider', data.authProvider); this.set('appMeta.authProvider', data.authProvider);
this.set('appMeta.authConfig', data.authConfig); this.set('appMeta.authConfig', data.authConfig);
window.location.href= '/'; window.location.href = '/';
} }
} }
}); });

View file

@ -2,7 +2,7 @@
{{#toolbar/t-toolbar}} {{#toolbar/t-toolbar}}
{{#toolbar/t-links}} {{#toolbar/t-links}}
{{#link-to "folders" class="link" tagName="li"}}Spaces{{/link-to}} {{#link-to "folders" class="link" tagName="li" }}Spaces{{/link-to}}
{{/toolbar/t-links}} {{/toolbar/t-links}}
{{#toolbar/t-actions}} {{#toolbar/t-actions}}
{{/toolbar/t-actions}} {{/toolbar/t-actions}}
@ -14,24 +14,20 @@
<div class="form-group mt-4"> <div class="form-group mt-4">
{{focus-input type="text" value=filter class="form-control mb-4" placeholder='a OR b, x AND y, "phrase mat*"'}} {{focus-input type="text" value=filter class="form-control mb-4" placeholder='a OR b, x AND y, "phrase mat*"'}}
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<label class="form-check-label"> {{input type="checkbox" id="search-1" class="form-check-input" checked=matchDoc}}
{{input type="checkbox" id=checkId class="form-check-input" checked=matchDoc}} document title <label class="form-check-label" for="search-1">&nbsp;document title</label>
</label>
</div> </div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<label class="form-check-label"> {{input type="checkbox" id="search-2" class="form-check-input" checked=matchContent}}
{{input type="checkbox" id=checkId class="form-check-input" checked=matchContent}} content <label class="form-check-label" for="search-2">&nbsp;content</label>
</label>
</div> </div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<label class="form-check-label"> {{input type="checkbox" id="search-3" class="form-check-input" checked=matchTag}}
{{input type="checkbox" id=checkId class="form-check-input" checked=matchTag}} tag name <label class="form-check-label" for="search-3">&nbsp;tag name</label>
</label>
</div> </div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<label class="form-check-label"> {{input type="checkbox" id="search-4" class="form-check-input" checked=matchFile}}
{{input type="checkbox" id=checkId class="form-check-input" checked=matchFile}} attachment name <label class="form-check-label" for="search-4">&nbsp;attachment name</label>
</label>
</div> </div>
</div> </div>

View file

@ -9,6 +9,7 @@
// //
// https://documize.com // https://documize.com
import $ from 'jquery';
import { htmlSafe } from '@ember/string'; import { htmlSafe } from '@ember/string';
import { resolve } from 'rsvp'; import { resolve } from 'rsvp';
import Service, { inject as service } from '@ember/service'; import Service, { inject as service } from '@ember/service';
@ -63,6 +64,7 @@ export default Service.extend({
return this.get('ajax').request('public/meta').then((response) => { return this.get('ajax').request('public/meta').then((response) => {
this.setProperties(response); this.setProperties(response);
this.set('version', 'v' + this.get('version'));
if (requestedRoute === 'secure') { if (requestedRoute === 'secure') {
this.setProperties({ this.setProperties({
@ -76,6 +78,26 @@ export default Service.extend({
this.get('localStorage').storeSessionItem('entryUrl', requestedUrl); this.get('localStorage').storeSessionItem('entryUrl', requestedUrl);
} }
let self = this;
let cacheBuster = + new Date();
$.getJSON(`https://storage.googleapis.com/documize/downloads/updates/meta.json?cb=${cacheBuster}`, function (versions) {
let cv = 'v' + versions.community.version;
let ev = 'v' + versions.enterprise.version;
let re = self.get('edition');
let rv = self.get('version');
self.set('communityLatest', cv);
self.set('enterpriseLatest', ev);
self.set('updateAvailable', false); // set to true for testing
if (re === 'Community' && rv < cv) {
self.set('updateAvailable', true);
}
if (re === 'Enterprise' && rv < ev) {
self.set('updateAvailable', true);
}
});
return response; return response;
}); });
} }

View file

@ -11,11 +11,13 @@
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { computed } from '@ember/object'; import { computed } from '@ember/object';
import { Promise as EmberPromise } from 'rsvp';
import SimpleAuthSession from 'ember-simple-auth/services/session'; import SimpleAuthSession from 'ember-simple-auth/services/session';
export default SimpleAuthSession.extend({ export default SimpleAuthSession.extend({
ajax: service(), ajax: service(),
appMeta: service(), appMeta: service(),
userSvc: service('user'),
store: service(), store: service(),
localStorage: service(), localStorage: service(),
folderPermissions: null, folderPermissions: null,
@ -23,11 +25,11 @@ export default SimpleAuthSession.extend({
isMac: false, isMac: false,
isMobile: false, isMobile: false,
hasAccounts: computed('isAuthenticated', 'session.content.authenticated.user', function() { hasAccounts: computed('isAuthenticated', 'session.content.authenticated.user', function () {
return this.get('session.authenticator') !== 'authenticator:anonymous' && this.get('session.content.authenticated.user.accounts').length > 0; return this.get('session.authenticator') !== 'authenticator:anonymous' && this.get('session.content.authenticated.user.accounts').length > 0;
}), }),
accounts: computed('hasAccounts', function() { accounts: computed('hasAccounts', function () {
return this.get('session.content.authenticated.user.accounts'); return this.get('session.content.authenticated.user.accounts');
}), }),
@ -35,7 +37,9 @@ export default SimpleAuthSession.extend({
if (this.get('isAuthenticated')) { if (this.get('isAuthenticated')) {
let user = this.get('session.content.authenticated.user') || { id: '0' }; let user = this.get('session.content.authenticated.user') || { id: '0' };
let data = this.get('store').normalize('user', user); let data = this.get('store').normalize('user', user);
return this.get('store').push(data); let um = this.get('store').push(data)
return um;
} }
}), }),
@ -71,5 +75,21 @@ export default SimpleAuthSession.extend({
logout() { logout() {
this.get('localStorage').clearAll(); this.get('localStorage').clearAll();
},
seenNewVersion() {
this.get('userSvc').getUser(this.get('user.id')).then((user) => {
user.set('lastVersion', this.get('appMeta.version'));
this.get('userSvc').save(user);
});
},
// set what's new indicator
hasWhatsNew() {
return new EmberPromise((resolve) => {
return this.get('userSvc').getUser(this.get('user.id')).then((user) => {
resolve(user.get('lastVersion') !== this.get('appMeta.version'));
});
});
} }
}); });

View file

@ -31,6 +31,7 @@
@import "section/plantuml.scss"; @import "section/plantuml.scss";
@import "section/papertrail.scss"; @import "section/papertrail.scss";
@import "section/wysiwyg.scss"; @import "section/wysiwyg.scss";
@import "news.scss";
@import "enterprise/all.scss"; @import "enterprise/all.scss";
// Bootstrap override that removes gutter space on smaller screens // Bootstrap override that removes gutter space on smaller screens

View file

@ -101,8 +101,7 @@ $link-hover-decoration: none;
@import "node_modules/bootstrap/scss/tooltip"; @import "node_modules/bootstrap/scss/tooltip";
@import "node_modules/bootstrap/scss/tables"; @import "node_modules/bootstrap/scss/tables";
@import "node_modules/bootstrap/scss/badge"; @import "node_modules/bootstrap/scss/badge";
// @import "node_modules/bootstrap/scss/navbar";
// @import "node_modules/bootstrap/scss/images";
.modal-80 { .modal-80 {
max-width: 80% !important; max-width: 80% !important;
@ -111,3 +110,18 @@ $link-hover-decoration: none;
body.modal-open { body.modal-open {
padding-right: 0 !important; padding-right: 0 !important;
} }
.modal-header-white {
background-color: $color-white !important;
border: none !important;
> .close {
background-color: $color-white !important;
border: none !important;
> span {
color: $color-gray;
font-size: 2rem;
}
}
}

View file

@ -33,6 +33,7 @@ $color-red: #9E0D1F;
$color-green: #348A37; $color-green: #348A37;
$color-blue: #2667af; $color-blue: #2667af;
$color-goldy: #FFD700; $color-goldy: #FFD700;
$color-orange: #FFAD15;
// widgets // widgets
$color-checkbox: #2667af; $color-checkbox: #2667af;
@ -40,6 +41,7 @@ $color-symbol-box: #dce5e8;
$color-symbol-icon: #a4b8be; $color-symbol-icon: #a4b8be;
$color-card: #F9F9F9; $color-card: #F9F9F9;
$color-stroke: #e1e1e1; $color-stroke: #e1e1e1;
$color-whats-new: #fc1530;
// Color utility classes for direct usage in HTML // Color utility classes for direct usage in HTML
.color-white { .color-white {
@ -75,6 +77,12 @@ $color-stroke: #e1e1e1;
.color-gold { .color-gold {
color: $color-goldy !important; color: $color-goldy !important;
} }
.color-orange {
color: $color-orange !important;
}
.color-whats-new {
color: $color-whats-new !important;
}
.background-color-white { .background-color-white {
background-color: $color-white !important; background-color: $color-white !important;

175
gui/app/styles/news.scss Normal file
View file

@ -0,0 +1,175 @@
.product-update {
text-align: left;
margin: 50px 0;
> .update-summary {
padding: 25px;
border: 1px solid $color-orange;
background-color: $color-off-white;
@include border-radius(2px);
> .caption {
font-weight: bold;
font-size: 1.5rem;
color: $color-orange;
margin-bottom: 15px;
display: inline-block;
}
> .instructions {
font-weight: normal;
font-size: 1.3rem;
color: $color-gray;
}
> .version {
margin: 30px 0 0 20px;
font-size: 1.3rem;
color: $color-gray;
font-weight: bold;
}
> .changes {
margin: 10px 0 0 40px;
> li {
list-style-type: disc;
padding: 5px 0;
font-size: 1.2rem;
color: $color-black;
> .tag-edition {
margin: 10px 10px 10px 10px;
padding: 5px 10px;
background-color: $color-gray-light;
color: $color-primary;
font-weight: bold;
font-size: 0.9rem;
}
}
}
}
}
.product-about {
text-align: center;
margin: 30px 30px;
> .edition {
font-weight: normal;
font-size: 1.5rem;
color: $color-black;
margin-bottom: 5px;
}
> .version {
font-weight: bold;
font-size: 1.1rem;
color: $color-gray;
margin-bottom: 20px;
}
> .dotcom {
font-weight: bold;
font-size: 1.2rem;
color: $color-link;
margin-bottom: 40px;
}
> .copyright {
text-align: center;
font-weight: normal;
font-size: 1rem;
color: $color-off-black;
margin-bottom: 20px;
}
> .license {
text-align: left;
font-weight: normal;
font-size: 1rem;
color: $color-gray;
}
}
.update-available-dot {
border-radius: 10px;
width: 10px;
height: 10px;
background-color: $color-orange;
position: absolute;
bottom: 0;
right: 0;
}
.whats-new-dot {
border-radius: 10px;
width: 10px;
height: 10px;
background-color: $color-whats-new;
position: absolute;
top: 0;
right: 0;
}
.product-news {
text-align: left;
margin: 0 30px;
> h2 {
margin: 0 0 10px 0;
text-align: center;
font-size: 2rem;
color: $color-off-black;
}
> .news-item {
padding: 30px 0;
border-bottom: 1px solid $color-border;
text-align: center;
> .title {
color: $color-primary;
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 5px;
}
> .date {
color: $color-gray;
font-size: 1rem;
font-weight: 600;
margin-bottom: 10px;
}
> .info {
color: $color-black;
font-size: 1.1rem;
font-weight: normal;
margin-top: 15px;
}
> .tag-edition {
margin: 10px 10px 10px 10px;
padding: 5px 10px;
background-color: $color-off-white;
color: $color-gray;
font-weight: 700;
font-size: 0.9rem;
display: inline-block;
}
> img {
max-width: 450px;
max-height: 350px;
}
}
> .action {
margin: 20px 0;
text-align: center;
color: $color-gray;
font-weight: 800;
font-size: 1.3rem;
}
}

View file

@ -201,7 +201,7 @@
display: inline-block; display: inline-block;
cursor: default; cursor: default;
position: relative; position: relative;
overflow: hidden; // overflow: hidden; // kills update dots
width: 35px; width: 35px;
height: 35px; height: 35px;
line-height: 34px; line-height: 34px;

View file

@ -8,9 +8,7 @@
</div> </div>
<div class="view-customize"> <div class="view-customize">
<form class="mt-5"> <form class="mt-5">
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label">Provider</label> <label class="col-sm-2 col-form-label">Provider</label>
<div class="col-sm-10"> <div class="col-sm-10">
@ -62,7 +60,8 @@
<label for="keycloak-admin-user" class="col-sm-2 col-form-label">Keycloak Username</label> <label for="keycloak-admin-user" class="col-sm-2 col-form-label">Keycloak Username</label>
<div class="col-sm-10"> <div class="col-sm-10">
{{input id="keycloak-admin-user" type="text" value=keycloakConfig.adminUser class=(if KeycloakAdminUserError 'form-control is-invalid' 'form-control')}} {{input id="keycloak-admin-user" type="text" value=keycloakConfig.adminUser class=(if KeycloakAdminUserError 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">Used to connect with Keycloak and sync users with Documize (create user under Master Realm and assign 'view-users' role against Realm specified above)</small> <small class="form-text text-muted">Used to connect with Keycloak and sync users with Documize (create user under Master Realm and assign 'view-users' role
against Realm specified above)</small>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
@ -75,24 +74,26 @@
<div class="form-group row"> <div class="form-group row">
<label for="keycloak-admin-password" class="col-sm-2 col-form-label">Logout</label> <label for="keycloak-admin-password" class="col-sm-2 col-form-label">Logout</label>
<div class="col-sm-10"> <div class="col-sm-10">
<label class="form-check-label"> <div class="form-check">
{{input type="checkbox" class="form-check-input" checked=keycloakConfig.disableLogout}} {{input type="checkbox" class="form-check-input" id="keycloak-logout" checked=keycloakConfig.disableLogout}}
Hide the logout button for Keycloak users <label class="form-check-label" for="keycloak-logout">
</label> Hide the logout button for Keycloak users
</label>
</div>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="keycloak-admin-password" class="col-sm-2 col-form-label">Space Permission</label> <label for="keycloak-admin-password" class="col-sm-2 col-form-label">Space Permission</label>
<div class="col-sm-10"> <div class="col-sm-10">
<label class="form-check-label"> <div class="form-check">
{{input type="checkbox" class="form-check-input" checked=keycloakConfig.defaultPermissionAddSpace}} {{input type="checkbox" class="form-check-input" id="keycloak-perm" checked=keycloakConfig.defaultPermissionAddSpace}}
Can add spaces <label class="form-check-label" for="keycloak-perm">
</label> Can add spaces
</label>
</div>
</div> </div>
</div> </div>
{{/if}} {{/if}}
<div class="btn btn-success mt-4" {{action 'onSave'}}>Save</div> <div class="btn btn-success mt-4" {{action 'onSave'}}>Save</div>
</form> </form>
</div> </div>

View file

@ -26,10 +26,13 @@
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label">Anonymous Access</label> <label class="col-sm-2 col-form-label">Anonymous Access</label>
<div class="col-sm-10"> <div class="col-sm-10">
<label class="form-check-label"> <div class="form-check">
<input type="checkbox" class="form-check-input" id="allowAnonymousAccess" checked={{model.general.allowAnonymousAccess}} /> <input type="checkbox" class="form-check-input" id="allowAnonymousAccess" checked= {{model.general.allowAnonymousAccess}}
Make content marked as "Everyone" available to anonymous users />
</label> <label class="form-check-label" for="allowAnonymousAccess">
Make content marked as "Everyone" available to anonymous users
</label>
</div>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
@ -37,7 +40,8 @@
<div class="col-sm-10"> <div class="col-sm-10">
{{input id="conversionEndpoint" type="text" value=model.general.conversionEndpoint class=(if hasConversionEndpointInputError 'form-control is-invalid' 'form-control')}} {{input id="conversionEndpoint" type="text" value=model.general.conversionEndpoint class=(if hasConversionEndpointInputError 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted"> <small class="form-text text-muted">
Endpoint for handling import/export (e.g. https://api.documize.com, <a href="https://docs.documize.com/s/WNEpptWJ9AABRnha/administration-guides/d/WO0pt_MXigAB6sJ7/general-options">view documentation</a>) Endpoint for handling import/export (e.g. https://api.documize.com,
<a href="https://docs.documize.com/s/WNEpptWJ9AABRnha/administration-guides/d/WO0pt_MXigAB6sJ7/general-options">view documentation</a>)
</small> </small>
</div> </div>
</div> </div>

View file

@ -1,21 +1,55 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div class="view-customize"> <div class="view-customize">
<h1 class="admin-heading">Product License</h1> <h1 class="admin-heading">{{appMeta.edition}} Edition {{appMeta.version}}</h1>
<h2 class="sub-heading">Optional Enterprise Edition license</h2> <h2 class="sub-heading">Enterprise Edition unlocks
<a class="" href="https://documize.com/pricing">premium capabilities and product support</a>
</h2>
</div> </div>
</div> </div>
</div> </div>
<div class="view-customize"> <div class="view-customize">
<form class="mt-5"> <form class="mt-5 ">
<div class="form-group row"> <div class="form-group row">
<label for="smtp-host" class="col-sm-2 col-form-label">License Key</label> <label for="smtp-host " class="col-sm-2 col-form-label ">Enterprise Edition License Key</label>
<div class="col-sm-10"> <div class="col-sm-10 ">
{{textarea value=model.license rows="15" class=(if LicenseError 'form-control is-invalid' 'form-control')}} {{textarea value=model.license rows="10" class=(if LicenseError 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">XML format</small> <small class="form-text text-muted ">XML format</small>
{{#if appMeta.valid}}
<p class="mt-2 color-green">Valid</p>
{{else}}
<p class="mt-2 color-red">Invalid</p>
{{/if}}
<div class="btn btn-success mt-3" {{action 'saveLicense'}}>Save</div>
</div> </div>
</div> </div>
<div class="btn btn-success mt-4" {{action 'saveLicense'}}>Save</div>
</form> </form>
</div> </div>
<div class="product-update">
<div class="update-summary">
{{#if appMeta.updateAvailable}}
<a href="https://documize.com/downloads" class="caption">New version available</a>
<p class="instructions">
To upgrade, replace existing binary and restart Documize. Migrate between Community and Enterprise editions seamlessly.
</p>
{{else}}
<div class="caption">Release Summary</div>
{{/if}}
<p>
<span class="color-off-black">Community Edition {{appMeta.communityLatest}}</span>&nbsp;&nbsp;&nbsp;
<a href="https://storage.googleapis.com/documize/downloads/documize-community-windows-amd64.exe" class="font-weight-bold">Windows</a>&nbsp;&middot;
<a href="https://storage.googleapis.com/documize/downloads/documize-community-linux-amd64" class="font-weight-bold">Linux</a>&nbsp;&middot;
<a href="https://storage.googleapis.com/documize/downloads/documize-community-darwin-amd64" class="font-weight-bold">macOS</a>&nbsp;
</p>
<p>
<span class="color-off-black">Enterprise Edition {{appMeta.enterpriseLatest}}</span>&nbsp;&nbsp;&nbsp;
<a href="https://storage.googleapis.com/documize/downloads/documize-enterprise-windows-amd64.exe" class="font-weight-bold color-blue">Windows</a>&nbsp;&middot;
<a href="https://storage.googleapis.com/documize/downloads/documize-enterprise-linux-amd64" class="font-weight-bold color-blue">Linux</a>&nbsp;&middot;
<a href="https://storage.googleapis.com/documize/downloads/documize-enterprise-darwin-amd64" class="font-weight-bold color-blue">macOS</a>&nbsp;
</p>
<div class="my-5" />
{{{changelog}}}
</div>
</div>

View file

@ -74,7 +74,7 @@
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-4 col-form-label">SSL</label> <label class="col-sm-4 col-form-label">SSL</label>
<div class="col-sm-8"> <div class="col-sm-8">
<div class="form-check"> <div class="form-check">
{{input id="smtp-usessl" type="checkbox" checked=model.smtp.usessl class='form-check-input'}} {{input id="smtp-usessl" type="checkbox" checked=model.smtp.usessl class='form-check-input'}}
<label class="form-check-label" for="smtp-usessl">Use SSL</label> <label class="form-check-label" for="smtp-usessl">Use SSL</label>
</div> </div>

View file

@ -1,7 +1,7 @@
<div id="nav-bar" class="nav-bar clearfix container-fluid"> <div id="nav-bar" class="nav-bar clearfix container-fluid">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col col-sm-9"> <div class="col col-sm-9">
{{#link-to "folders" class='nav-link'}} {{#link-to "folders" class='nav-link' }}
<div class="nav-title">{{appMeta.title}}</div> <div class="nav-title">{{appMeta.title}}</div>
<div class="nav-msg text-truncate">{{appMeta.message}}</div> <div class="nav-msg text-truncate">{{appMeta.message}}</div>
{{/link-to}} {{/link-to}}
@ -9,7 +9,7 @@
<div class="col col-sm-3"> <div class="col col-sm-3">
<div class="nav-right"> <div class="nav-right">
<div class="btn-group"> <div class="btn-group">
{{#link-to "search" class="button-icon-white"}} {{#link-to "search" class="button-icon-white" }}
<i class="material-icons">search</i> <i class="material-icons">search</i>
{{/link-to}} {{/link-to}}
</div> </div>
@ -25,13 +25,13 @@
{{#if hasSpacePins}} {{#if hasSpacePins}}
<h6 class="dropdown-header">Spaces</h6> <h6 class="dropdown-header">Spaces</h6>
{{#each spacePins as |pin|}} {{#each spacePins as |pin|}}
<a class="dropdown-item" href="#" {{action 'jumpToPin' pin}} data-id={{pin.id}} id="pin-{{pin.id}}">{{pin.pin}}</a> <a class="dropdown-item" href="#" {{action 'jumpToPin' pin}} data-id= {{pin.id}} id="pin-{{pin.id}}">{{pin.pin}}</a>
{{/each}} {{/each}}
{{/if}} {{/if}}
{{#if hasDocumentPins}} {{#if hasDocumentPins}}
<h6 class="dropdown-header">Documents</h6> <h6 class="dropdown-header">Documents</h6>
{{#each documentPins as |pin|}} {{#each documentPins as |pin|}}
<a class="dropdown-item" href="#" {{action 'jumpToPin' pin}} data-id={{pin.id}} id="pin-{{pin.id}}">{{pin.pin}}</a> <a class="dropdown-item" href="#" {{action 'jumpToPin' pin}} data-id= {{pin.id}} id="pin-{{pin.id}}">{{pin.pin}}</a>
{{/each}} {{/each}}
{{/if}} {{/if}}
</div> </div>
@ -42,22 +42,38 @@
<div class="btn-group"> <div class="btn-group">
<div class="button-gravatar-white align-text-bottom" id="profile-button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <div class="button-gravatar-white align-text-bottom" id="profile-button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{session.user.initials}} {{session.user.initials}}
{{#if hasWhatsNew}}
<div class="whats-new-dot" />
{{/if}}
{{#if session.isAdmin}}
{{#if appMeta.updateAvailable}}
<div class="update-available-dot" />
{{/if}}
{{/if}}
</div> </div>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="profile-button"> <div class="dropdown-menu dropdown-menu-right" aria-labelledby="profile-button">
{{#link-to 'profile' class="dropdown-item"}}Profile{{/link-to}} {{#link-to 'profile' class="dropdown-item" }}Profile{{/link-to}}
{{#if session.isAdmin}} {{#if session.isAdmin}}
{{#link-to 'customize.general' class="dropdown-item"}}Settings{{/link-to}} {{#link-to 'customize.general' class="dropdown-item" }}Settings{{/link-to}}
{{/if}} {{/if}}
<div class="dropdown-divider"></div>
{{#if session.isAdmin}}
{{#if appMeta.updateAvailable}}
{{#link-to 'customize.license' class="dropdown-item font-weight-bold color-orange" }}Update available{{/link-to}}
{{/if}}
{{/if}}
<a href="#" class="dropdown-item {{if hasWhatsNew 'color-whats-new font-weight-bold'}}" {{action 'onShowWhatsNewModal'}}>What's New</a>
<a href="#" class="dropdown-item" data-toggle="modal" data-target="#about-documize-modal" data-backdrop="static">About Documize</a>
{{#if enableLogout}} {{#if enableLogout}}
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
{{#link-to 'auth.logout' class="dropdown-item"}}Logout{{/link-to}} {{#link-to 'auth.logout' class="dropdown-item" }}Logout{{/link-to}}
{{/if}} {{/if}}
</div> </div>
</div> </div>
{{else}} {{else}}
<div class="button-icon-gap" /> <div class="button-icon-gap" />
<div class="btn-group"> <div class="btn-group">
{{#link-to 'auth.login' class="button-icon-white "}} {{#link-to 'auth.login' class="button-icon-white " }}
<i class="material-icons">lock_open</i> <i class="material-icons">lock_open</i>
{{/link-to}} {{/link-to}}
</div> </div>
@ -66,3 +82,67 @@
</div> </div>
</div> </div>
</div> </div>
{{#if session.authenticated}}
<div id="whats-new-modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-header modal-header-white">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true" data-dismiss="modal" aria-label="Close">&times;</span>
</button>
</div>
<div class="modal-content">
<div class="modal-body">
<div class="product-news">
<h2>What's New</h2>
{{{newsContent}}}
<div class="action">
Have an idea? Suggestion or feedback? <a href="mailto:support@documize.com">Get in touch!</a>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div id="about-documize-modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-body">
<div class="product-about">
<div class="edition">
Documize {{appMeta.edition}} Edition
</div>
<div class="version">
{{appMeta.version}}
</div>
<div class="dotcom">
<a href="https://documize.com">https://documize.com</a>
</div>
{{#if (eq appMeta.edition 'Community')}}
<div class="copyright">
&copy; Documize Inc. All rights reserved.
</div>
<div class="license">
<br/>
<br/> This software (Documize Community Edition) is licensed under
<a href="http://www.gnu.org/licenses/agpl-3.0.en.html">GNU AGPL v3</a>
You can operate outside the AGPL restrictions by purchasing Documize Enterprise Edition and obtaining a commercial licenseby
contacting
<a href="mailto:sales@documize.com">sales@documize.com</a>
</div>
{{/if}}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{{/if}}

View file

@ -1,14 +0,0 @@
{
"community": {
"version": "1.59.0",
"major": 1,
"minor": 59,
"patch": 0
},
"enterprise": {
"version": "1.61.0",
"major": 1,
"minor": 61,
"patch": 0
}
}

View file

@ -22,20 +22,21 @@ import (
// User defines a login. // User defines a login.
type User struct { type User struct {
model.BaseEntity model.BaseEntity
Firstname string `json:"firstname"` Firstname string `json:"firstname"`
Lastname string `json:"lastname"` Lastname string `json:"lastname"`
Email string `json:"email"` Email string `json:"email"`
Initials string `json:"initials"` Initials string `json:"initials"`
Active bool `json:"active"` Active bool `json:"active"`
Editor bool `json:"editor"` Editor bool `json:"editor"`
Admin bool `json:"admin"` Admin bool `json:"admin"`
ViewUsers bool `json:"viewUsers"` ViewUsers bool `json:"viewUsers"`
Global bool `json:"global"` Global bool `json:"global"`
Password string `json:"-"` Password string `json:"-"`
Salt string `json:"-"` Salt string `json:"-"`
Reset string `json:"-"` Reset string `json:"-"`
Accounts []account.Account `json:"accounts"` LastVersion string `json:"lastVersion"`
Groups []group.Record `json:"groups"` Accounts []account.Account `json:"accounts"`
Groups []group.Record `json:"groups"`
} }
// ProtectSecrets blanks sensitive data. // ProtectSecrets blanks sensitive data.

View file

@ -144,6 +144,7 @@ func (m *middleware) Authorize(w http.ResponseWriter, r *http.Request, next http
rc.AppURL = r.Host rc.AppURL = r.Host
rc.Subdomain = organization.GetSubdomainFromHost(r) rc.Subdomain = organization.GetSubdomainFromHost(r)
rc.SSL = r.TLS != nil rc.SSL = r.TLS != nil
rc.AppVersion = fmt.Sprintf("v%s", m.Runtime.Product.Version)
// get user IP from request // get user IP from request
i := strings.LastIndex(r.RemoteAddr, ":") i := strings.LastIndex(r.RemoteAddr, ":")