1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-19 05:09:42 +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
FROM page WHERE status=0
-- whats new support
ALTER TABLE user ADD COLUMN `lastversion` CHAR(16) NOT NULL DEFAULT '' AFTER `active`;
-- deprecations

View file

@ -39,6 +39,7 @@ type RequestContext struct {
Expires time.Time
Fullname string
Transaction *sqlx.Tx
AppVersion string
}
//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()
userModel.Salt = secrets.GenerateSalt()
userModel.Password = secrets.GeneratePassword(requestedPassword, userModel.Salt)
userModel.LastVersion = ctx.AppVersion
// only create account if not dupe
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.Revised = time.Now().UTC()
_, err = ctx.Transaction.Exec("INSERT INTO user (refid, firstname, lastname, email, initials, password, salt, reset, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
u.RefID, u.Firstname, u.Lastname, strings.ToLower(u.Email), u.Initials, u.Password, u.Salt, "", u.Created, u.Revised)
_, 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.LastVersion, u.Created, u.Revised)
if err != nil {
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.
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 {
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) {
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)
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) {
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 {
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.
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 {
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
// the onboarding process.
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 {
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.
func (s Scope) GetActiveUsersForOrganization(ctx domain.RequestContext) (u []user.User, err error) {
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
FROM user u, account a
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,
`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
FROM user u, account a
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.
func (s Scope) GetSpaceUsers(ctx domain.RequestContext, spaceID string) (u []user.User, err error) {
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
FROM user u, account a
WHERE a.orgid=? AND u.refid = a.userid AND a.active=1 AND u.refid IN (
@ -189,12 +189,12 @@ func (s Scope) GetUsersForSpaces(ctx domain.RequestContext, spaces []string) (u
}
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
FROM user u, account a
WHERE a.orgid=? AND u.refid = a.userid AND a.active=1 AND u.refid IN (
SELECT whoid from permission WHERE orgid=? AND who='user' AND scope='object' AND location='space' AND refid IN(?) UNION ALL
SELECT r.userid from rolemember r LEFT JOIN permission p ON p.whoid=r.roleid WHERE p.orgid=? AND p.who='role' AND p.scope='object' AND p.location='space' AND p.refid IN(?)
SELECT r.userid from rolemember r LEFT JOIN permission p ON p.whoid=r.roleid WHERE p.orgid=? AND p.who='role' AND p.scope='object' AND p.location='space' AND p.refid IN(?)
)
ORDER BY u.firstname, u.lastname
`, ctx.OrgID, ctx.OrgID, spaces, ctx.OrgID, spaces)
@ -219,7 +219,7 @@ func (s Scope) UpdateUser(ctx domain.RequestContext, u user.User) (err error) {
u.Email = strings.ToLower(u.Email)
_, 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 {
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,
`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
FROM user u, account a
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
import $ from 'jquery';
import { empty } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import Component from '@ember/component';
export default Component.extend({
appMeta: service(),
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: {
saveLicense() {

View file

@ -9,12 +9,14 @@
//
// https://documize.com
import Component from '@ember/component';
import $ from 'jquery';
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 Component from '@ember/component';
export default Component.extend({
export default Component.extend(ModalMixin, {
folderService: service('folder'),
appMeta: service(),
session: service(),
@ -24,6 +26,8 @@ export default Component.extend({
hasPins: notEmpty('pins'),
hasSpacePins: notEmpty('spacePins'),
hasDocumentPins: notEmpty('documentPins'),
hasWhatsNew: false,
newsContent: '',
init() {
this._super(...arguments);
@ -34,6 +38,23 @@ export default Component.extend({
config = JSON.parse(config);
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() {
@ -80,6 +101,12 @@ export default Component.extend({
let folder = this.get('store').peekRecord('folder', folderId);
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 }),
accounts: attr(),
groups: attr(),
lastVersion: attr('string'),
created: attr(),
revised: attr(),

View file

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

View file

@ -2,7 +2,7 @@
{{#toolbar/t-toolbar}}
{{#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-actions}}
{{/toolbar/t-actions}}
@ -14,27 +14,23 @@
<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*"'}}
<div class="form-check form-check-inline">
<label class="form-check-label">
{{input type="checkbox" id=checkId class="form-check-input" checked=matchDoc}} document title
</label>
{{input type="checkbox" id="search-1" class="form-check-input" checked=matchDoc}}
<label class="form-check-label" for="search-1">&nbsp;document title</label>
</div>
<div class="form-check form-check-inline">
<label class="form-check-label">
{{input type="checkbox" id=checkId class="form-check-input" checked=matchContent}} content
</label>
{{input type="checkbox" id="search-2" class="form-check-input" checked=matchContent}}
<label class="form-check-label" for="search-2">&nbsp;content</label>
</div>
<div class="form-check form-check-inline">
<label class="form-check-label">
{{input type="checkbox" id=checkId class="form-check-input" checked=matchTag}} tag name
</label>
{{input type="checkbox" id="search-3" class="form-check-input" checked=matchTag}}
<label class="form-check-label" for="search-3">&nbsp;tag name</label>
</div>
<div class="form-check form-check-inline">
<label class="form-check-label">
{{input type="checkbox" id=checkId class="form-check-input" checked=matchFile}} attachment name
</label>
{{input type="checkbox" id="search-4" class="form-check-input" checked=matchFile}}
<label class="form-check-label" for="search-4">&nbsp;attachment name</label>
</div>
</div>
{{search/search-results results=results}}
</div>
</div>
</div>

View file

@ -9,6 +9,7 @@
//
// https://documize.com
import $ from 'jquery';
import { htmlSafe } from '@ember/string';
import { resolve } from 'rsvp';
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) => {
this.setProperties(response);
this.set('version', 'v' + this.get('version'));
if (requestedRoute === 'secure') {
this.setProperties({
@ -70,12 +72,32 @@ export default Service.extend({
allowAnonymousAccess: true,
secureMode: true
});
this.get('localStorage').clearAll();
} else if (is.not.include(requestedUrl, '/auth/')) {
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;
});
}

View file

@ -11,11 +11,13 @@
import { inject as service } from '@ember/service';
import { computed } from '@ember/object';
import { Promise as EmberPromise } from 'rsvp';
import SimpleAuthSession from 'ember-simple-auth/services/session';
export default SimpleAuthSession.extend({
ajax: service(),
appMeta: service(),
userSvc: service('user'),
store: service(),
localStorage: service(),
folderPermissions: null,
@ -23,11 +25,11 @@ export default SimpleAuthSession.extend({
isMac: 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;
}),
accounts: computed('hasAccounts', function() {
accounts: computed('hasAccounts', function () {
return this.get('session.content.authenticated.user.accounts');
}),
@ -35,7 +37,9 @@ export default SimpleAuthSession.extend({
if (this.get('isAuthenticated')) {
let user = this.get('session.content.authenticated.user') || { id: '0' };
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() {
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/papertrail.scss";
@import "section/wysiwyg.scss";
@import "news.scss";
@import "enterprise/all.scss";
// Bootstrap override that removes gutter space on smaller screens
@ -39,4 +40,4 @@
// width: 100%;
// max-width: none;
// }
// }
// }

View file

@ -101,8 +101,7 @@ $link-hover-decoration: none;
@import "node_modules/bootstrap/scss/tooltip";
@import "node_modules/bootstrap/scss/tables";
@import "node_modules/bootstrap/scss/badge";
// @import "node_modules/bootstrap/scss/navbar";
// @import "node_modules/bootstrap/scss/images";
.modal-80 {
max-width: 80% !important;
@ -111,3 +110,18 @@ $link-hover-decoration: none;
body.modal-open {
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-blue: #2667af;
$color-goldy: #FFD700;
$color-orange: #FFAD15;
// widgets
$color-checkbox: #2667af;
@ -40,6 +41,7 @@ $color-symbol-box: #dce5e8;
$color-symbol-icon: #a4b8be;
$color-card: #F9F9F9;
$color-stroke: #e1e1e1;
$color-whats-new: #fc1530;
// Color utility classes for direct usage in HTML
.color-white {
@ -75,6 +77,12 @@ $color-stroke: #e1e1e1;
.color-gold {
color: $color-goldy !important;
}
.color-orange {
color: $color-orange !important;
}
.color-whats-new {
color: $color-whats-new !important;
}
.background-color-white {
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;
cursor: default;
position: relative;
overflow: hidden;
// overflow: hidden; // kills update dots
width: 35px;
height: 35px;
line-height: 34px;

View file

@ -8,9 +8,7 @@
</div>
<div class="view-customize">
<form class="mt-5">
<div class="form-group row">
<label class="col-sm-2 col-form-label">Provider</label>
<div class="col-sm-10">
@ -62,7 +60,8 @@
<label for="keycloak-admin-user" class="col-sm-2 col-form-label">Keycloak Username</label>
<div class="col-sm-10">
{{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 class="form-group row">
@ -75,24 +74,26 @@
<div class="form-group row">
<label for="keycloak-admin-password" class="col-sm-2 col-form-label">Logout</label>
<div class="col-sm-10">
<label class="form-check-label">
{{input type="checkbox" class="form-check-input" checked=keycloakConfig.disableLogout}}
Hide the logout button for Keycloak users
</label>
<div class="form-check">
{{input type="checkbox" class="form-check-input" id="keycloak-logout" checked=keycloakConfig.disableLogout}}
<label class="form-check-label" for="keycloak-logout">
Hide the logout button for Keycloak users
</label>
</div>
</div>
</div>
<div class="form-group row">
<label for="keycloak-admin-password" class="col-sm-2 col-form-label">Space Permission</label>
<div class="col-sm-10">
<label class="form-check-label">
{{input type="checkbox" class="form-check-input" checked=keycloakConfig.defaultPermissionAddSpace}}
Can add spaces
</label>
<div class="form-check">
{{input type="checkbox" class="form-check-input" id="keycloak-perm" checked=keycloakConfig.defaultPermissionAddSpace}}
<label class="form-check-label" for="keycloak-perm">
Can add spaces
</label>
</div>
</div>
</div>
{{/if}}
<div class="btn btn-success mt-4" {{action 'onSave'}}>Save</div>
</form>
</div>
</div>

View file

@ -26,10 +26,13 @@
<div class="form-group row">
<label class="col-sm-2 col-form-label">Anonymous Access</label>
<div class="col-sm-10">
<label class="form-check-label">
<input type="checkbox" class="form-check-input" id="allowAnonymousAccess" checked={{model.general.allowAnonymousAccess}} />
Make content marked as "Everyone" available to anonymous users
</label>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="allowAnonymousAccess" checked= {{model.general.allowAnonymousAccess}}
/>
<label class="form-check-label" for="allowAnonymousAccess">
Make content marked as "Everyone" available to anonymous users
</label>
</div>
</div>
</div>
<div class="form-group row">
@ -37,7 +40,8 @@
<div class="col-sm-10">
{{input id="conversionEndpoint" type="text" value=model.general.conversionEndpoint class=(if hasConversionEndpointInputError 'form-control is-invalid' 'form-control')}}
<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>
</div>
</div>

View file

@ -1,21 +1,55 @@
<div class="row">
<div class="col">
<div class="view-customize">
<h1 class="admin-heading">Product License</h1>
<h2 class="sub-heading">Optional Enterprise Edition license</h2>
<h1 class="admin-heading">{{appMeta.edition}} Edition {{appMeta.version}}</h1>
<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 class="view-customize">
<form class="mt-5">
<form class="mt-5 ">
<div class="form-group row">
<label for="smtp-host" class="col-sm-2 col-form-label">License Key</label>
<div class="col-sm-10">
{{textarea value=model.license rows="15" class=(if LicenseError 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">XML format</small>
<label for="smtp-host " class="col-sm-2 col-form-label ">Enterprise Edition License Key</label>
<div class="col-sm-10 ">
{{textarea value=model.license rows="10" class=(if LicenseError 'form-control is-invalid' 'form-control')}}
<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 class="btn btn-success mt-4" {{action 'saveLicense'}}>Save</div>
</form>
</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">
<label class="col-sm-4 col-form-label">SSL</label>
<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'}}
<label class="form-check-label" for="smtp-usessl">Use SSL</label>
</div>

View file

@ -1,7 +1,7 @@
<div id="nav-bar" class="nav-bar clearfix container-fluid">
<div class="row no-gutters">
<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-msg text-truncate">{{appMeta.message}}</div>
{{/link-to}}
@ -9,7 +9,7 @@
<div class="col col-sm-3">
<div class="nav-right">
<div class="btn-group">
{{#link-to "search" class="button-icon-white"}}
{{#link-to "search" class="button-icon-white" }}
<i class="material-icons">search</i>
{{/link-to}}
</div>
@ -25,13 +25,13 @@
{{#if hasSpacePins}}
<h6 class="dropdown-header">Spaces</h6>
{{#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}}
{{/if}}
{{#if hasDocumentPins}}
<h6 class="dropdown-header">Documents</h6>
{{#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}}
{{/if}}
</div>
@ -42,22 +42,38 @@
<div class="btn-group">
<div class="button-gravatar-white align-text-bottom" id="profile-button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{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 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}}
{{#link-to 'customize.general' class="dropdown-item"}}Settings{{/link-to}}
{{#link-to 'customize.general' class="dropdown-item" }}Settings{{/link-to}}
{{/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}}
<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}}
</div>
</div>
{{else}}
<div class="button-icon-gap" />
<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>
{{/link-to}}
</div>
@ -66,3 +82,67 @@
</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.
type User struct {
model.BaseEntity
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
Email string `json:"email"`
Initials string `json:"initials"`
Active bool `json:"active"`
Editor bool `json:"editor"`
Admin bool `json:"admin"`
ViewUsers bool `json:"viewUsers"`
Global bool `json:"global"`
Password string `json:"-"`
Salt string `json:"-"`
Reset string `json:"-"`
Accounts []account.Account `json:"accounts"`
Groups []group.Record `json:"groups"`
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
Email string `json:"email"`
Initials string `json:"initials"`
Active bool `json:"active"`
Editor bool `json:"editor"`
Admin bool `json:"admin"`
ViewUsers bool `json:"viewUsers"`
Global bool `json:"global"`
Password string `json:"-"`
Salt string `json:"-"`
Reset string `json:"-"`
LastVersion string `json:"lastVersion"`
Accounts []account.Account `json:"accounts"`
Groups []group.Record `json:"groups"`
}
// 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.Subdomain = organization.GetSubdomainFromHost(r)
rc.SSL = r.TLS != nil
rc.AppVersion = fmt.Sprintf("v%s", m.Runtime.Product.Version)
// get user IP from request
i := strings.LastIndex(r.RemoteAddr, ":")