mirror of
https://github.com/documize/community.git
synced 2025-07-19 13:19:43 +02:00
Improve Space permissions
Closes out loopholes that allowed managers to kick owners.
This commit is contained in:
parent
09635b67ab
commit
5d632712e0
30 changed files with 1015 additions and 877 deletions
|
@ -58,9 +58,9 @@ Space view.
|
|||
|
||||
## Latest Release
|
||||
|
||||
[Community Edition: v1.76.0](https://github.com/documize/community/releases)
|
||||
[Community Edition: v1.76.1](https://github.com/documize/community/releases)
|
||||
|
||||
[Enterprise Edition: v1.76.0](https://documize.com/downloads)
|
||||
[Enterprise Edition: v1.76.1](https://documize.com/downloads)
|
||||
|
||||
## OS support
|
||||
|
||||
|
|
|
@ -504,8 +504,11 @@ func (h *Handler) FetchSpaceData(w http.ResponseWriter, r *http.Request) {
|
|||
fetch := category.FetchSpaceModel{}
|
||||
|
||||
// get space categories visible to user
|
||||
cat, err := h.Store.Category.GetBySpace(ctx, spaceID)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
var cat []category.Category
|
||||
var err error
|
||||
|
||||
cat, err = h.Store.Category.GetBySpace(ctx, spaceID)
|
||||
if err != nil {
|
||||
h.Runtime.Log.Error("get space categories visible to user failed", err)
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
|
@ -527,7 +530,7 @@ func (h *Handler) FetchSpaceData(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// get category membership records
|
||||
member, err := h.Store.Category.GetSpaceCategoryMembership(ctx, spaceID)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
if err != nil {
|
||||
h.Runtime.Log.Error("get document category membership for space", err)
|
||||
response.WriteServerError(w, method, err)
|
||||
return
|
||||
|
|
|
@ -51,6 +51,8 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
org.StripSecrets()
|
||||
|
||||
response.WriteJSON(w, org)
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,8 @@ type Store struct {
|
|||
func (s Store) AddOrganization(ctx domain.RequestContext, o org.Organization) (err error) {
|
||||
_, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_org (c_refid, c_company, c_title, c_message, c_domain, c_email, c_anonaccess, c_serial, c_maxtags, c_sub, c_created, c_revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
|
||||
o.RefID, o.Company, o.Title, o.Message, strings.ToLower(o.Domain),
|
||||
strings.ToLower(o.Email), o.AllowAnonymousAccess, o.Serial, o.MaxTags, o.Subscription, o.Created, o.Revised)
|
||||
strings.ToLower(o.Email), o.AllowAnonymousAccess, o.Serial, o.MaxTags,
|
||||
o.Subscription, o.Created, o.Revised)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "unable to execute insert for org")
|
||||
|
@ -43,13 +44,14 @@ func (s Store) AddOrganization(ctx domain.RequestContext, o org.Organization) (e
|
|||
return nil
|
||||
}
|
||||
|
||||
// GetOrganization returns the Organization reocrod from the organization database table with the given id.
|
||||
// GetOrganization returns the Organization record from the organization database table with the given id.
|
||||
func (s Store) GetOrganization(ctx domain.RequestContext, id string) (org org.Organization, err error) {
|
||||
err = s.Runtime.Db.Get(&org, s.Bind(`SELECT id, c_refid AS refid,
|
||||
c_title AS title, c_message AS message, c_domain AS domain,
|
||||
c_service AS conversionendpoint, c_email AS email, c_serial AS serial, c_active AS active,
|
||||
c_anonaccess AS allowanonymousaccess, c_authprovider AS authprovider,
|
||||
coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig, coalesce(c_sub,`+s.EmptyJSON()+`) AS subscription,
|
||||
coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig,
|
||||
coalesce(c_sub,`+s.EmptyJSON()+`) AS subscription,
|
||||
c_maxtags AS maxtags, c_created AS created, c_revised AS revised
|
||||
FROM dmz_org
|
||||
WHERE c_refid=?`),
|
||||
|
@ -80,7 +82,8 @@ func (s Store) GetOrganizationByDomain(subdomain string) (o org.Organization, er
|
|||
c_title AS title, c_message AS message, c_domain AS domain,
|
||||
c_service AS conversionendpoint, c_email AS email, c_serial AS serial, c_active AS active,
|
||||
c_anonaccess AS allowanonymousaccess, c_authprovider AS authprovider,
|
||||
coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig, coalesce(c_sub,`+s.EmptyJSON()+`) AS subscription,
|
||||
coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig,
|
||||
coalesce(c_sub,`+s.EmptyJSON()+`) AS subscription,
|
||||
c_maxtags AS maxtags, c_created AS created, c_revised AS revised
|
||||
FROM dmz_org
|
||||
WHERE c_domain=? AND c_active=true`),
|
||||
|
@ -95,7 +98,8 @@ func (s Store) GetOrganizationByDomain(subdomain string) (o org.Organization, er
|
|||
c_title AS title, c_message AS message, c_domain AS domain,
|
||||
c_service AS conversionendpoint, c_email AS email, c_serial AS serial, c_active AS active,
|
||||
c_anonaccess AS allowanonymousaccess, c_authprovider AS authprovider,
|
||||
coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig, coalesce(c_sub,`+s.EmptyJSON()+`) AS subscription,
|
||||
coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig,
|
||||
coalesce(c_sub,`+s.EmptyJSON()+`) AS subscription,
|
||||
c_maxtags AS maxtags, c_created AS created, c_revised AS revised
|
||||
FROM dmz_org
|
||||
WHERE c_domain='' AND c_active=true`))
|
||||
|
|
|
@ -120,11 +120,6 @@ func (h *Handler) SetSpacePermissions(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
url := ctx.GetAppURL(fmt.Sprintf("s/%s/%s", sp.RefID, stringutil.MakeSlug(sp.Name)))
|
||||
me := false
|
||||
hasEveryoneRole := false
|
||||
roleCount := 0
|
||||
|
||||
// Permissions can be assigned to both groups and individual users.
|
||||
// Pre-fetch users with group membership to help us work out
|
||||
// if user belongs to a group with permissions.
|
||||
|
@ -136,6 +131,18 @@ func (h *Handler) SetSpacePermissions(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// url is sent in 'space shared with you' invitation emails.
|
||||
url := ctx.GetAppURL(fmt.Sprintf("s/%s/%s", sp.RefID, stringutil.MakeSlug(sp.Name)))
|
||||
// me tracks if the user who changed permissions, also has some space permissions.
|
||||
me := false
|
||||
// hasEveryRole tracks if "everyone" has been give access to space.
|
||||
hasEveryoneRole := false
|
||||
// hasOwner tracks is at least one person or user group has been marked as space owner.
|
||||
hasOwner := false
|
||||
// roleCount tracks the number of permission records created for this space.
|
||||
// It's used to determine if space has multiple participants, see below.
|
||||
roleCount := 0
|
||||
|
||||
for _, perm := range model.Permissions {
|
||||
perm.OrgID = ctx.OrgID
|
||||
perm.SpaceID = id
|
||||
|
@ -154,6 +161,12 @@ func (h *Handler) SetSpacePermissions(w http.ResponseWriter, r *http.Request) {
|
|||
me = true
|
||||
}
|
||||
|
||||
// Detect is we have at least one space owner permission.
|
||||
// Result used below to prevent lock-outs.
|
||||
if hasOwner == false && perm.SpaceOwner {
|
||||
hasOwner = true
|
||||
}
|
||||
|
||||
// Only persist if there is a role!
|
||||
if permission.HasAnyPermission(perm) {
|
||||
// identify publically shared spaces
|
||||
|
@ -209,7 +222,31 @@ func (h *Handler) SetSpacePermissions(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
// Do we need to ensure permissions for space owner when shared?
|
||||
// Catch and prevent lock-outs so we don't have
|
||||
// zombie spaces that nobody can access.
|
||||
if len(model.Permissions) == 0 {
|
||||
// When no permissions are assigned we
|
||||
// default to current user as being owner and viewer.
|
||||
perm := permission.Permission{}
|
||||
perm.OrgID = ctx.OrgID
|
||||
perm.Who = permission.UserPermission
|
||||
perm.WhoID = ctx.UserID
|
||||
perm.Scope = permission.ScopeRow
|
||||
perm.Location = permission.LocationSpace
|
||||
perm.RefID = id
|
||||
perm.Action = "" // we send allowable actions in function call...
|
||||
err = h.Store.Permission.AddPermissions(ctx, perm, permission.SpaceOwner, permission.SpaceView)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// So we have permissions but we must check for at least one space owner.
|
||||
if !hasOwner {
|
||||
// So we have no space owner, make current user the owner
|
||||
// if we have no permssions thus far.
|
||||
if !me {
|
||||
perm := permission.Permission{}
|
||||
perm.OrgID = ctx.OrgID
|
||||
|
@ -218,9 +255,8 @@ func (h *Handler) SetSpacePermissions(w http.ResponseWriter, r *http.Request) {
|
|||
perm.Scope = permission.ScopeRow
|
||||
perm.Location = permission.LocationSpace
|
||||
perm.RefID = id
|
||||
perm.Action = "" // we send array for actions below
|
||||
|
||||
err = h.Store.Permission.AddPermissions(ctx, perm, permission.SpaceView, permission.SpaceManage)
|
||||
perm.Action = "" // we send allowable actions in function call...
|
||||
err = h.Store.Permission.AddPermissions(ctx, perm, permission.SpaceOwner, permission.SpaceView)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
|
@ -228,6 +264,8 @@ func (h *Handler) SetSpacePermissions(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark up space type as either public, private or restricted access.
|
||||
if hasEveryoneRole {
|
||||
|
|
|
@ -116,24 +116,35 @@ func (s Store) GetViewable(ctx domain.RequestContext) (sp []space.Space, err err
|
|||
return
|
||||
}
|
||||
|
||||
// GetAll for admin users!
|
||||
func (s Store) GetAll(ctx domain.RequestContext) (sp []space.Space, err error) {
|
||||
qry := s.Bind(`SELECT id, c_refid AS refid,
|
||||
// AdminList returns all shared spaces and orphaned spaces that have no owner.
|
||||
func (s Store) AdminList(ctx domain.RequestContext) (sp []space.Space, err error) {
|
||||
qry := s.Bind(`
|
||||
SELECT id, c_refid AS refid,
|
||||
c_name AS name, c_orgid AS orgid, c_userid AS userid,
|
||||
c_type AS type, c_lifecycle AS lifecycle, c_likes AS likes,
|
||||
c_created AS created, c_revised AS revised
|
||||
FROM dmz_space
|
||||
WHERE c_orgid=?
|
||||
ORDER BY c_name`)
|
||||
|
||||
err = s.Runtime.Db.Select(&sp, qry, ctx.OrgID)
|
||||
WHERE c_orgid=? AND (c_type=? OR c_type=?)
|
||||
UNION ALL
|
||||
SELECT id, c_refid AS refid,
|
||||
c_name AS name, c_orgid AS orgid, c_userid AS userid,
|
||||
c_type AS type, c_lifecycle AS lifecycle, c_likes AS likes,
|
||||
c_created AS created, c_revised AS revised
|
||||
FROM dmz_space
|
||||
WHERE c_orgid=? AND (c_type=? OR c_type=?) AND c_refid NOT IN
|
||||
(SELECT c_refid FROM dmz_permission WHERE c_orgid=? AND c_action='own')
|
||||
ORDER BY name`)
|
||||
|
||||
err = s.Runtime.Db.Select(&sp, qry,
|
||||
ctx.OrgID, space.ScopePublic, space.ScopeRestricted,
|
||||
ctx.OrgID, space.ScopePublic, space.ScopeRestricted,
|
||||
ctx.OrgID)
|
||||
if err == sql.ErrNoRows {
|
||||
err = nil
|
||||
sp = []space.Space{}
|
||||
}
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("failed space.GetAll org %s", ctx.OrgID))
|
||||
err = errors.Wrap(err, fmt.Sprintf("failed space.AdminList org %s", ctx.OrgID))
|
||||
}
|
||||
|
||||
return
|
||||
|
|
|
@ -59,9 +59,9 @@ type SpaceStorer interface {
|
|||
Get(ctx domain.RequestContext, id string) (sp space.Space, err error)
|
||||
PublicSpaces(ctx domain.RequestContext, orgID string) (sp []space.Space, err error)
|
||||
GetViewable(ctx domain.RequestContext) (sp []space.Space, err error)
|
||||
GetAll(ctx domain.RequestContext) (sp []space.Space, err error)
|
||||
Update(ctx domain.RequestContext, sp space.Space) (err error)
|
||||
Delete(ctx domain.RequestContext, id string) (rows int64, err error)
|
||||
AdminList(ctx domain.RequestContext) (sp []space.Space, err error)
|
||||
}
|
||||
|
||||
// CategoryStorer defines required methods for category and category membership management
|
||||
|
|
|
@ -41,8 +41,8 @@ func main() {
|
|||
rt.Product = domain.Product{}
|
||||
rt.Product.Major = "1"
|
||||
rt.Product.Minor = "76"
|
||||
rt.Product.Patch = "0"
|
||||
rt.Product.Revision = 181113144232
|
||||
rt.Product.Patch = "1"
|
||||
rt.Product.Revision = 181116171324
|
||||
rt.Product.Version = fmt.Sprintf("%s.%s.%s", rt.Product.Major, rt.Product.Minor, rt.Product.Patch)
|
||||
rt.Product.Edition = domain.CommunityEdition
|
||||
rt.Product.Title = fmt.Sprintf("%s Edition", rt.Product.Edition)
|
||||
|
|
1350
embed/bindata.go
1350
embed/bindata.go
File diff suppressed because one or more lines are too long
|
@ -22,6 +22,7 @@ export default Component.extend(Notifier, Modals, {
|
|||
subscription: null,
|
||||
planCloud: false,
|
||||
planSelfhost: false,
|
||||
comment: 'Nothing in particular -- just passing through. Please close my Documize account.',
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
@ -47,10 +48,17 @@ export default Component.extend(Notifier, Modals, {
|
|||
});
|
||||
},
|
||||
|
||||
onRequestClosure() {
|
||||
this.modalOpen("#deactivation-request-modal", {"show": true}, '#close-comment');
|
||||
},
|
||||
|
||||
onDeactivate() {
|
||||
this.get('global').deactivate().then(() => {
|
||||
this.modalClose("#deactivation-request-modal");
|
||||
let comment = this.get('comment');
|
||||
|
||||
this.get('global').deactivate(comment).then(() => {
|
||||
this.showDone();
|
||||
this.modalOpen("#deactivation-modal", {"show": true});
|
||||
this.modalOpen("#deactivation-confirmation-modal", {"show": true});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
105
gui/app/components/customize/space-admin.js
Normal file
105
gui/app/components/customize/space-admin.js
Normal file
|
@ -0,0 +1,105 @@
|
|||
// Copyright 2016 Documize Inc. <legal@documize.com>. 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 <sales@documize.com>.
|
||||
//
|
||||
// https://documize.com
|
||||
|
||||
import $ from 'jquery';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { computed } from '@ember/object';
|
||||
import Notifier from '../../mixins/notifier';
|
||||
import Modals from '../../mixins/modal';
|
||||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend(Notifier, Modals, {
|
||||
spaceSvc: service('folder'),
|
||||
browserSvc: service('browser'),
|
||||
documentSvc: service('document'),
|
||||
spaces: null,
|
||||
|
||||
label: computed('model', function() {
|
||||
switch (this.get('model').length) {
|
||||
case 1:
|
||||
return "space";
|
||||
default:
|
||||
return "spaces";
|
||||
}
|
||||
}),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.deleteSpace = {
|
||||
id: '',
|
||||
name: ''
|
||||
};
|
||||
},
|
||||
|
||||
loadData() {
|
||||
this.get('spaceSvc').manage().then((s) => {
|
||||
this.set('spaces', s);
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
onShow(id) {
|
||||
this.set('deleteSpace.id', id);
|
||||
},
|
||||
|
||||
onDelete() {
|
||||
let deleteSpace = this.get('deleteSpace');
|
||||
let spaceId = deleteSpace.id;
|
||||
let spaceNameTyped = deleteSpace.name;
|
||||
let space = this.get('spaces').findBy('id', spaceId);
|
||||
let spaceName = space.get('name');
|
||||
|
||||
if (spaceNameTyped !== spaceName || spaceNameTyped === '' || spaceName === '') {
|
||||
$('#delete-space-name').addClass('is-invalid').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
$('#space-delete-modal').modal('hide');
|
||||
$('#space-delete-modal').modal('dispose');
|
||||
|
||||
this.get('spaceSvc').delete(spaceId).then(() => { /* jshint ignore:line */
|
||||
this.set('deleteSpace.id', '');
|
||||
this.set('deleteSpace.name', '');
|
||||
this.loadData();
|
||||
});
|
||||
},
|
||||
|
||||
onExport() {
|
||||
this.showWait();
|
||||
|
||||
let spec = {
|
||||
spaceId: '',
|
||||
data: _.pluck(this.get('folders'), 'id'),
|
||||
filterType: 'space',
|
||||
};
|
||||
|
||||
this.get('documentSvc').export(spec).then((htmlExport) => {
|
||||
this.get('browserSvc').downloadFile(htmlExport, 'documize.html');
|
||||
this.showDone();
|
||||
});
|
||||
},
|
||||
|
||||
onOwner(spaceId) {
|
||||
this.showWait();
|
||||
|
||||
this.get('spaceSvc').grantOwnerPermission(spaceId).then(() => { /* jshint ignore:line */
|
||||
this.showDone();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -62,6 +62,15 @@ export default Component.extend(ModalMixin, TooltipMixin, Notifer, {
|
|||
cat.set('documents', docCount);
|
||||
cat.set('users', userCount);
|
||||
});
|
||||
|
||||
this.get('categorySvc').getUserVisible(this.get('space.id')).then((cm) => {
|
||||
cm.forEach((cm) => {
|
||||
let cat = _.findWhere(c, {id: cm.get('id') });
|
||||
if (is.not.undefined(cat)) {
|
||||
cat.set('access', is.not.undefined(cat));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
|
|
@ -34,6 +34,9 @@ export default Component.extend(Notifier, Modals, {
|
|||
isSpaceAdmin: computed('permissions', function() {
|
||||
return this.get('permissions.spaceOwner') || this.get('permissions.spaceManage');
|
||||
}),
|
||||
isNotSpaceOwner: computed('permissions', function() {
|
||||
return !this.get('permissions.spaceOwner');
|
||||
}),
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
|
|
@ -29,7 +29,6 @@ export default Component.extend(AuthMixin, {
|
|||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
// this.filteredDocs = [];
|
||||
this.setup();
|
||||
},
|
||||
|
||||
|
|
|
@ -9,85 +9,7 @@
|
|||
//
|
||||
// https://documize.com
|
||||
|
||||
import $ from 'jquery';
|
||||
import { computed } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import Notifier from '../../../mixins/notifier';
|
||||
import TooltipMixin from '../../../mixins/tooltip';
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
export default Controller.extend(TooltipMixin, Notifier, {
|
||||
folderService: service('folder'),
|
||||
browserSvc: service('browser'),
|
||||
documentSvc: service('document'),
|
||||
dropdown: null,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.folders = [];
|
||||
this.deleteSpace = {
|
||||
id: '',
|
||||
name: ''
|
||||
};
|
||||
},
|
||||
|
||||
label: computed('folders', function() {
|
||||
switch (this.get('folders').length) {
|
||||
case 1:
|
||||
return "space";
|
||||
default:
|
||||
return "spaces";
|
||||
}
|
||||
}),
|
||||
|
||||
actions: {
|
||||
onShow(id) {
|
||||
this.set('deleteSpace.id', id);
|
||||
},
|
||||
|
||||
onDelete() {
|
||||
let deleteSpace = this.get('deleteSpace');
|
||||
let spaceId = deleteSpace.id;
|
||||
let spaceNameTyped = deleteSpace.name;
|
||||
let space = this.get('folders').findBy('id', spaceId);
|
||||
let spaceName = space.get('name');
|
||||
|
||||
if (spaceNameTyped !== spaceName || spaceNameTyped === '' || spaceName === '') {
|
||||
$('#delete-space-name').addClass('is-invalid').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
$('#space-delete-modal').modal('hide');
|
||||
$('#space-delete-modal').modal('dispose');
|
||||
|
||||
this.get('folderService').delete(spaceId).then(() => { /* jshint ignore:line */
|
||||
this.set('deleteSpace.id', '');
|
||||
this.set('deleteSpace.name', '');
|
||||
|
||||
this.get('folderService').adminList().then((folders) => {
|
||||
let nonPrivateFolders = folders.rejectBy('spaceType', 2);
|
||||
if (is.empty(nonPrivateFolders) || is.null(folders) || is.undefined(folders)) {
|
||||
nonPrivateFolders = [];
|
||||
}
|
||||
|
||||
this.set('folders', nonPrivateFolders);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onExport() {
|
||||
this.showWait();
|
||||
|
||||
let spec = {
|
||||
spaceId: '',
|
||||
data: _.pluck(this.get('folders'), 'id'),
|
||||
filterType: 'space',
|
||||
};
|
||||
|
||||
this.get('documentSvc').export(spec).then((htmlExport) => {
|
||||
this.get('browserSvc').downloadFile(htmlExport, 'documize.html');
|
||||
this.showDone();
|
||||
});
|
||||
}
|
||||
}
|
||||
export default Controller.extend({
|
||||
});
|
||||
|
|
|
@ -9,33 +9,17 @@
|
|||
//
|
||||
// https://documize.com
|
||||
|
||||
import { inject as service } from '@ember/service';
|
||||
import Route from '@ember/routing/route';
|
||||
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend(AuthenticatedRouteMixin, {
|
||||
folderService: service('folder'),
|
||||
|
||||
beforeModel() {
|
||||
if (!this.session.isAdmin) {
|
||||
this.transitionTo('auth.login');
|
||||
}
|
||||
},
|
||||
|
||||
model() {
|
||||
return this.get('folderService').adminList();
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
let nonPrivateFolders = model.rejectBy('spaceType', 2);
|
||||
if (is.empty(nonPrivateFolders) || is.null(model) || is.undefined(model)) {
|
||||
nonPrivateFolders = [];
|
||||
}
|
||||
|
||||
controller.set('folders', nonPrivateFolders);
|
||||
},
|
||||
|
||||
activate() {
|
||||
this.get('browser').setTitle('Spaces');
|
||||
this.get('browser').setTitle('Manage Spaces');
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,50 +1 @@
|
|||
{{#if folders}}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="view-customize">
|
||||
<h1 class="admin-heading">{{folders.length}} shared {{label}}</h1>
|
||||
<button type="button" class="btn btn-success" onclick={{action 'onExport'}}>Export as HTML</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="view-customize">
|
||||
<div class="space-list">
|
||||
{{#each folders as |folder|}}
|
||||
<div class="space row">
|
||||
<div class="col-12 col-sm-8">
|
||||
{{#link-to 'folder' folder.id folder.slug class="alt"}}{{folder.name}}{{/link-to}}
|
||||
</div>
|
||||
<div class="col-12 col-sm-4 text-right">
|
||||
<div id="space-delete-button-{{folder.id}}" class="button-icon-danger align-middle" data-toggle="tooltip" data-placement="top" title="Delete space" {{action "onShow" folder.id}}>
|
||||
<i class="material-icons" data-toggle="modal" data-target="#space-delete-modal" data-backdrop="static">delete</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="space-delete-modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">Space Deletion</div>
|
||||
<div class="modal-body">
|
||||
<form onsubmit={{action 'onDelete'}}>
|
||||
<p>Are you sure you want to delete this space and all documents?</p>
|
||||
<div class="form-group">
|
||||
<label for="delete-space-name">Please type space name to confirm</label>
|
||||
{{input type='text' id="delete-space-name" class="form-control mousetrap" placeholder="Space name" value=deleteSpace.name}}
|
||||
<small class="form-text text-muted">This will delete all documents and templates within this space!</small>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" onclick={{action 'onDelete'}}>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<p>There are no spaces to maintain</p>
|
||||
{{/if}}
|
||||
{{customize/space-admin}}
|
|
@ -84,7 +84,7 @@ export default Controller.extend(Tooltips, Notifier, {
|
|||
let constants = this.get('constants');
|
||||
|
||||
// if document approval mode is locked return
|
||||
if (document.get('protection') == constants.ProtectionType.Lock) {
|
||||
if (document.get('protection') === constants.ProtectionType.Lock) {
|
||||
// should not really happen
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -335,7 +335,7 @@ export default Service.extend({
|
|||
} else {
|
||||
let id = this.get('storageSvc').getSessionItem('anonId');
|
||||
|
||||
if (is.not.null(id) && is.not.undefined(id) && id.length === 16) {
|
||||
if (is.not.null(id) && is.not.undefined(id) && id.length >= 16) {
|
||||
userId = id;
|
||||
} else {
|
||||
userId = stringUtil.anonUserId();
|
||||
|
|
|
@ -174,8 +174,9 @@ export default BaseService.extend({
|
|||
});
|
||||
},
|
||||
|
||||
// returns all spaces -- for use by documize admin user
|
||||
adminList() {
|
||||
// Returns all shared spaces and spaces without an owner.
|
||||
// Administrator only method.
|
||||
manage() {
|
||||
return this.get('ajax').request(`space/manage`, {
|
||||
method: "GET"
|
||||
}).then((response) => {
|
||||
|
@ -189,5 +190,12 @@ export default BaseService.extend({
|
|||
|
||||
return data;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Add admin as space owner.
|
||||
grantOwnerPermission(folderId) {
|
||||
return this.get('ajax').request(`space/manage/owner/${folderId}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -236,10 +236,12 @@ export default Service.extend({
|
|||
});
|
||||
},
|
||||
|
||||
deactivate() {
|
||||
deactivate(comment) {
|
||||
if(this.get('sessionService.isAdmin')) {
|
||||
return this.get('ajax').request(`deactivate`, {
|
||||
method: 'POST',
|
||||
contentType: 'text',
|
||||
data: comment,
|
||||
}).then(() => {
|
||||
return;
|
||||
});
|
||||
|
|
|
@ -65,13 +65,11 @@
|
|||
|
||||
> .space-list {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin: 3rem 0;
|
||||
|
||||
> .space {
|
||||
margin: 15px 0;
|
||||
padding: 15px;
|
||||
@include card();
|
||||
@include ease-in();
|
||||
padding: 15px 0;
|
||||
font-size: 1.2rem;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
|
|
@ -143,7 +143,7 @@
|
|||
<p>Requests can take up to 24 hours to process.</p>
|
||||
{{#link-to 'customize.backup' class="btn btn-success"}}PERFORM BACKUP{{/link-to}}
|
||||
<div class="button-gap" />
|
||||
<button class="btn btn-danger" {{action 'onDeactivate'}}>REQUEST ACCOUNT CLOSURE</button>
|
||||
<button class="btn btn-danger" {{action 'onRequestClosure'}}>CLOSE ACCOUNT</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -151,16 +151,37 @@
|
|||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<div id="deactivation-modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div id="deactivation-request-modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">Request Account Closure</div>
|
||||
<div class="modal-body">
|
||||
<form {{action "onEditComment" on="submit"}}>
|
||||
<div class="form-group">
|
||||
<label for="the-comment">Comment</label>
|
||||
{{focus-textarea id="close-comment" class="form-control" rows="5" value=comment}}
|
||||
<small class="form-text text-muted">We always welcome product feedback</small>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" onclick={{action 'onDeactivate'}}>Close Account</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="deactivation-confirmation-modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">Deactivation Requested</div>
|
||||
<div class="modal-body">
|
||||
<p>Your request has been sent and will be processed shortly.</p>
|
||||
<p>If you haven't already, perform a backup to download all your data.</p>
|
||||
<p>If you haven't already, please run a backup to download all your data.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
58
gui/app/templates/components/customize/space-admin.hbs
Normal file
58
gui/app/templates/components/customize/space-admin.hbs
Normal file
|
@ -0,0 +1,58 @@
|
|||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="view-customize">
|
||||
<h1 class="admin-heading">Manage Spaces</h1>
|
||||
<h2 class="sub-heading">Delete spaces, take ownership of shared spaces and orphaned spaces</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="view-customize my-5">
|
||||
{{#if spaces}}
|
||||
<button type="button" class="btn btn-success" onclick={{action 'onExport'}}>Export content</button>
|
||||
<div class="space-list">
|
||||
{{#each spaces as |space|}}
|
||||
<div class="space row">
|
||||
<div class="col-12 col-sm-8">
|
||||
{{#link-to 'folder' space.id space.slug class="alt"}}{{space.name}}{{/link-to}}
|
||||
</div>
|
||||
<div class="col-12 col-sm-4 text-right">
|
||||
<div id="space-ownership-button-{{space.id}}" class="button-icon-gray align-middle" data-toggle="tooltip" data-placement="top" title="Add myself as space owner" {{action "onOwner" space.id}}>
|
||||
<i class="material-icons" data-toggle="modal">person_add</i>
|
||||
</div>
|
||||
<div class="button-icon-gap" />
|
||||
<div id="space-delete-button-{{space.id}}" class="button-icon-danger align-middle" data-toggle="tooltip" data-placement="top" title="Delete space and all content" {{action "onShow" space.id}}>
|
||||
<i class="material-icons" data-toggle="modal" data-target="#space-delete-modal" data-backdrop="static">delete</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
<div id="space-delete-modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">Space Deletion</div>
|
||||
<div class="modal-body">
|
||||
<form onsubmit={{action 'onDelete'}}>
|
||||
<p>Are you sure you want to delete this space and all documents?</p>
|
||||
<div class="form-group">
|
||||
<label for="delete-space-name">Please type space name to confirm</label>
|
||||
{{input type='text' id="delete-space-name" class="form-control mousetrap" placeholder="Space name" value=deleteSpace.name}}
|
||||
<small class="form-text text-muted">This will delete all documents and templates within this space!</small>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" onclick={{action 'onDelete'}}>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<p>There are no shared spaces to manage</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
|
|
@ -19,7 +19,10 @@
|
|||
{{else}}
|
||||
<div class="category col-8">
|
||||
<div class="name">{{cat.category}}</div>
|
||||
<div class="info">{{cat.documents}} {{if (eq cat.documents 1) 'document' 'documents' }} · {{cat.users}} users/groups</div>
|
||||
<div class="info">
|
||||
{{cat.documents}} {{if (eq cat.documents 1) 'document' 'documents' }} · {{cat.users}} users/groups
|
||||
{{#unless cat.access}}<span class="text-danger">(you have no view permission)</span>{{/unless}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="col-4 buttons text-right">
|
||||
|
|
|
@ -74,7 +74,7 @@
|
|||
</td>
|
||||
<td>{{x-toggle value=permission.spaceView onToggle=(action (mut permission.spaceView))}}</td>
|
||||
<td>{{x-toggle value=permission.spaceManage onToggle=(action (mut permission.spaceManage))}}</td>
|
||||
<td>{{x-toggle value=permission.spaceOwner onToggle=(action (mut permission.spaceOwner))}}</td>
|
||||
<td>{{x-toggle value=permission.spaceOwner onToggle=(action (mut permission.spaceOwner)) disabled=isNotSpaceOwner}}</td>
|
||||
<td>{{x-toggle value=permission.documentAdd onToggle=(action (mut permission.documentAdd))}}</td>
|
||||
<td>{{x-toggle value=permission.documentEdit onToggle=(action (mut permission.documentEdit))}}</td>
|
||||
<td>{{x-toggle value=permission.documentDelete onToggle=(action (mut permission.documentDelete))}}</td>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "documize",
|
||||
"version": "1.76.0",
|
||||
"version": "1.76.1",
|
||||
"description": "The Document IDE",
|
||||
"private": true,
|
||||
"repository": "",
|
||||
|
|
|
@ -95,6 +95,7 @@ const (
|
|||
EventTypeWorkflowPublishRequested EventType = "requested-publication"
|
||||
EventTypeDatabaseBackup EventType = "backedup-database"
|
||||
EventTypeDatabaseRestore EventType = "restored-database"
|
||||
EventTypeAssumedSpaceOwnership EventType = "assumed-space-ownership"
|
||||
|
||||
// EventTypeVersionAdd records addition of version
|
||||
EventTypeVersionAdd EventType = "added-version"
|
||||
|
|
|
@ -11,7 +11,9 @@
|
|||
|
||||
package org
|
||||
|
||||
import "github.com/documize/community/model"
|
||||
import (
|
||||
"github.com/documize/community/model"
|
||||
)
|
||||
|
||||
// Organization defines a tenant that uses this app.
|
||||
type Organization struct {
|
||||
|
@ -28,5 +30,10 @@ type Organization struct {
|
|||
MaxTags int `json:"maxTags"`
|
||||
Serial string `json:"serial"`
|
||||
Active bool `json:"active"`
|
||||
Subscription string
|
||||
Subscription string `json:"subscription"`
|
||||
}
|
||||
|
||||
// StripSecrets removes sensitive information.
|
||||
func (o *Organization) StripSecrets() {
|
||||
o.Subscription = ""
|
||||
}
|
||||
|
|
|
@ -131,7 +131,8 @@ func RegisterEndpoints(rt *env.Runtime, s *store.Store) {
|
|||
AddPrivate(rt, "space/{spaceID}", []string{"DELETE", "OPTIONS"}, nil, space.Delete)
|
||||
AddPrivate(rt, "space/{spaceID}/move/{moveToId}", []string{"DELETE", "OPTIONS"}, nil, space.Remove)
|
||||
AddPrivate(rt, "space/{spaceID}/invitation", []string{"POST", "OPTIONS"}, nil, space.Invite)
|
||||
AddPrivate(rt, "space/manage", []string{"GET", "OPTIONS"}, nil, space.GetAll)
|
||||
AddPrivate(rt, "space/manage", []string{"GET", "OPTIONS"}, nil, space.Manage)
|
||||
AddPrivate(rt, "space/manage/owner/{spaceID}", []string{"POST", "OPTIONS"}, nil, space.ManageOwner)
|
||||
AddPrivate(rt, "space/{spaceID}", []string{"GET", "OPTIONS"}, nil, space.Get)
|
||||
AddPrivate(rt, "space", []string{"GET", "OPTIONS"}, nil, space.GetViewable)
|
||||
AddPrivate(rt, "space/{spaceID}", []string{"PUT", "OPTIONS"}, nil, space.Update)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue