mirror of
https://github.com/documize/community.git
synced 2025-07-19 13:19:43 +02:00
Migrate document attachments view to new UI framework
This commit is contained in:
parent
7cdf97aa86
commit
3d2060ca60
9 changed files with 148 additions and 9 deletions
|
@ -15,9 +15,12 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/documize/community/domain/auth"
|
||||||
|
"github.com/documize/community/model/space"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/documize/community/core/env"
|
"github.com/documize/community/core/env"
|
||||||
"github.com/documize/community/core/request"
|
"github.com/documize/community/core/request"
|
||||||
|
@ -47,9 +50,22 @@ func (h *Handler) Download(w http.ResponseWriter, r *http.Request) {
|
||||||
method := "attachment.Download"
|
method := "attachment.Download"
|
||||||
ctx := domain.GetRequestContext(r)
|
ctx := domain.GetRequestContext(r)
|
||||||
ctx.Subdomain = organization.GetSubdomainFromHost(r)
|
ctx.Subdomain = organization.GetSubdomainFromHost(r)
|
||||||
|
ctx.OrgID = request.Param(r, "orgID")
|
||||||
|
|
||||||
a, err := h.Store.Attachment.GetAttachment(ctx, request.Param(r, "orgID"), request.Param(r, "attachmentID"))
|
// Is caller permitted to download this attachment?
|
||||||
|
canDownload := false
|
||||||
|
|
||||||
|
// Do e have user authentication token?
|
||||||
|
authToken := strings.TrimSpace(request.Query(r, "token"))
|
||||||
|
|
||||||
|
// Do we have secure sharing token (for external users)?
|
||||||
|
secureToken := strings.TrimSpace(request.Query(r, "secure"))
|
||||||
|
|
||||||
|
// We now fetch attachment, the document and space it lives inside.
|
||||||
|
// Any data loading issue spells the end of this request.
|
||||||
|
|
||||||
|
// Get attachment being requested.
|
||||||
|
a, err := h.Store.Attachment.GetAttachment(ctx, ctx.OrgID, request.Param(r, "attachmentID"))
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
response.WriteNotFoundError(w, method, request.Param(r, "fileID"))
|
response.WriteNotFoundError(w, method, request.Param(r, "fileID"))
|
||||||
return
|
return
|
||||||
|
@ -60,6 +76,86 @@ func (h *Handler) Download(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the document for this attachment
|
||||||
|
doc, err := h.Store.Document.Get(ctx, a.DocumentID)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
response.WriteNotFoundError(w, method, a.DocumentID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
h.Runtime.Log.Error("get attachment document", err)
|
||||||
|
response.WriteServerError(w, method, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the space for this attachment
|
||||||
|
sp, err := h.Store.Space.Get(ctx, doc.SpaceID)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
response.WriteNotFoundError(w, method, a.DocumentID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
h.Runtime.Log.Error("get attachment document", err)
|
||||||
|
response.WriteServerError(w, method, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, all data associated data is loaded.
|
||||||
|
// We now begin security checks based upon the request.
|
||||||
|
|
||||||
|
// If attachment is in public space then anyone can download
|
||||||
|
if sp.Type == space.ScopePublic {
|
||||||
|
canDownload = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If an user authentication token was provided we check to see
|
||||||
|
// if user can view document.
|
||||||
|
// This check only applies to attachments NOT in public spaces.
|
||||||
|
if sp.Type != space.ScopePublic && len(authToken) > 0 {
|
||||||
|
// Decode and check incoming token
|
||||||
|
creds, _, err := auth.DecodeJWT(h.Runtime, authToken)
|
||||||
|
if err != nil {
|
||||||
|
h.Runtime.Log.Error("get attachment decode auth token", err)
|
||||||
|
response.WriteForbiddenError(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Check for tampering.
|
||||||
|
if ctx.OrgID != creds.OrgID {
|
||||||
|
h.Runtime.Log.Error("get attachment org ID mismatch", err)
|
||||||
|
response.WriteForbiddenError(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use token-based user ID for subsequent processing.
|
||||||
|
ctx.UserID = creds.UserID
|
||||||
|
|
||||||
|
// Check to see if user can view BOTH space and document.
|
||||||
|
if !permission.CanViewSpace(ctx, *h.Store, sp.RefID) || !permission.CanViewDocument(ctx, *h.Store, a.DocumentID) {
|
||||||
|
h.Runtime.Log.Error("get attachment cannot view document", err)
|
||||||
|
response.WriteServerError(w, method, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticated user can view attachment.
|
||||||
|
canDownload = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// External users can be sent secure document viewing links.
|
||||||
|
// Those documents may contain attachments that external viewers
|
||||||
|
// can download as required.
|
||||||
|
// Such secure document viewing links can have expiry dates.
|
||||||
|
if len(authToken) == 0 && len(secureToken) > 0 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send back error if caller unable view attachment
|
||||||
|
if !canDownload {
|
||||||
|
h.Runtime.Log.Error("get attachment refused", err)
|
||||||
|
response.WriteForbiddenError(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, user can view attachment so we send it back!
|
||||||
typ := mime.TypeByExtension("." + a.Extension)
|
typ := mime.TypeByExtension("." + a.Extension)
|
||||||
if typ == "" {
|
if typ == "" {
|
||||||
typ = "application/octet-stream"
|
typ = "application/octet-stream"
|
||||||
|
|
|
@ -112,6 +112,10 @@ Disallow: /auth/*
|
||||||
Disallow: /auth/**
|
Disallow: /auth/**
|
||||||
Disallow: /share
|
Disallow: /share
|
||||||
Disallow: /share/*
|
Disallow: /share/*
|
||||||
|
Disallow: /attachments
|
||||||
|
Disallow: /attachments/*
|
||||||
|
Disallow: /attachment
|
||||||
|
Disallow: /attachment/*
|
||||||
Sitemap: %s`, sitemap)
|
Sitemap: %s`, sitemap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,6 @@ export default Component.extend(Notifier, {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.set('model.general.maxTags', this.get('maxTags'));
|
this.set('model.general.maxTags', this.get('maxTags'));
|
||||||
this.model.general.set('allowAnonymousAccess', $("#allowAnonymousAccess").prop('checked'));
|
|
||||||
|
|
||||||
this.get('save')().then(() => {
|
this.get('save')().then(() => {
|
||||||
this.notifySuccess('Saved');
|
this.notifySuccess('Saved');
|
||||||
|
|
|
@ -20,11 +20,13 @@ export default Component.extend(Modals, Notifier, {
|
||||||
documentService: service('document'),
|
documentService: service('document'),
|
||||||
browserSvc: service('browser'),
|
browserSvc: service('browser'),
|
||||||
appMeta: service(),
|
appMeta: service(),
|
||||||
|
session: service(),
|
||||||
hasAttachments: notEmpty('files'),
|
hasAttachments: notEmpty('files'),
|
||||||
canEdit: computed('permissions.documentEdit', 'document.protection', function() {
|
canEdit: computed('permissions.documentEdit', 'document.protection', function() {
|
||||||
return this.get('document.protection') !== this.get('constants').ProtectionType.Lock && this.get('permissions.documentEdit');
|
return this.get('document.protection') !== this.get('constants').ProtectionType.Lock && this.get('permissions.documentEdit');
|
||||||
}),
|
}),
|
||||||
showDialog: false,
|
showDialog: false,
|
||||||
|
downloadQuery: '',
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
@ -50,7 +52,7 @@ export default Component.extend(Modals, Notifier, {
|
||||||
|
|
||||||
let dzone = new Dropzone("#upload-document-files", {
|
let dzone = new Dropzone("#upload-document-files", {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': 'Bearer ' + self.get('session.session.content.authenticated.token')
|
'Authorization': 'Bearer ' + self.get('session.authToken')
|
||||||
},
|
},
|
||||||
url: uploadUrl,
|
url: uploadUrl,
|
||||||
method: "post",
|
method: "post",
|
||||||
|
@ -67,16 +69,16 @@ export default Component.extend(Modals, Notifier, {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.on("queuecomplete", function () {
|
this.on("queuecomplete", function () {
|
||||||
self.notifySuccess('Saved');
|
self.notifySuccess('Uploaded file');
|
||||||
self.getAttachments();
|
self.getAttachments();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.on("addedfile", function ( /*file*/ ) {
|
this.on("addedfile", function ( /*file*/ ) {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.on("error", function (error, msg) { // // eslint-disable-line no-unused-vars
|
this.on("error", function (error, msg) {
|
||||||
self.notifyError(msg);
|
self.notifyError(msg);
|
||||||
console.log(msg); // eslint-disable-line no-console
|
self.notifyError(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -86,6 +88,13 @@ export default Component.extend(Modals, Notifier, {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.set('drop', dzone);
|
this.set('drop', dzone);
|
||||||
|
|
||||||
|
// For authenticated users we send server auth token.
|
||||||
|
let qry = '';
|
||||||
|
if (this.get('session.authenticated')) {
|
||||||
|
qry = '?token=' + this.get('session.authToken');
|
||||||
|
}
|
||||||
|
this.set('downloadQuery', qry);
|
||||||
},
|
},
|
||||||
|
|
||||||
getAttachments() {
|
getAttachments() {
|
||||||
|
|
|
@ -356,7 +356,7 @@ export default Service.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
//**************************************************
|
//**************************************************
|
||||||
// Export
|
// Export content to HTML
|
||||||
//**************************************************
|
//**************************************************
|
||||||
|
|
||||||
export(spec) {
|
export(spec) {
|
||||||
|
@ -367,6 +367,14 @@ export default Service.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
//**************************************************
|
||||||
|
// Secure document attachment download
|
||||||
|
//**************************************************
|
||||||
|
|
||||||
|
downloadAttachment(fileId) {
|
||||||
|
return this.get('ajax').get(`attachment/${fileId}`, {});
|
||||||
|
},
|
||||||
|
|
||||||
//**************************************************
|
//**************************************************
|
||||||
// Fetch bulk data
|
// Fetch bulk data
|
||||||
//**************************************************
|
//**************************************************
|
||||||
|
|
|
@ -85,6 +85,16 @@ export default SimpleAuthSession.extend({
|
||||||
this.get('session.content.authenticated.user.viewUsers') === true;
|
this.get('session.content.authenticated.user.viewUsers') === true;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
authToken: computed('session.content.authenticated.user', function () {
|
||||||
|
if (is.null(this.get('session.authenticator')) ||
|
||||||
|
this.get('appMeta.secureMode')) return '';
|
||||||
|
|
||||||
|
if (this.get('session.authenticator') === 'authenticator:anonymous' ||
|
||||||
|
this.get('session.content.authenticated.user.id') === '0') return '';
|
||||||
|
|
||||||
|
return this.get('session.content.authenticated.token');
|
||||||
|
}),
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
|
||||||
|
|
|
@ -84,4 +84,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.empty-label {
|
||||||
|
margin: 10px 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: map-get($gray-shades, 700);
|
||||||
|
font-style: italic;
|
||||||
|
padding: 0 7px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<ul class="files">
|
<ul class="files">
|
||||||
{{#each files key="id" as |file|}}
|
{{#each files key="id" as |file|}}
|
||||||
<li class="file">
|
<li class="file">
|
||||||
<a href="{{appMeta.endpoint}}/public/attachments/{{appMeta.orgId}}/{{file.id}}">
|
<a href="{{appMeta.endpoint}}/public/attachment/{{appMeta.orgId}}/{{file.id}}{{downloadQuery}}">
|
||||||
{{file.filename}}
|
{{file.filename}}
|
||||||
</a>
|
</a>
|
||||||
{{#if canEdit}}
|
{{#if canEdit}}
|
||||||
|
@ -27,6 +27,8 @@
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<p class="empty-label">No attachments</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#ui/ui-dialog title="Delete Attachment" confirmCaption="Delete" buttonColor=constants.Color.Red show=showDialog onAction=(action "onDelete")}}
|
{{#ui/ui-dialog title="Delete Attachment" confirmCaption="Delete" buttonColor=constants.Color.Red show=showDialog onAction=(action "onDelete")}}
|
||||||
|
|
|
@ -92,7 +92,7 @@ func RegisterEndpoints(rt *env.Runtime, s *store.Store) {
|
||||||
AddPublic(rt, "forgot", []string{"POST", "OPTIONS"}, nil, user.ForgotPassword)
|
AddPublic(rt, "forgot", []string{"POST", "OPTIONS"}, nil, user.ForgotPassword)
|
||||||
AddPublic(rt, "reset/{token}", []string{"POST", "OPTIONS"}, nil, user.ResetPassword)
|
AddPublic(rt, "reset/{token}", []string{"POST", "OPTIONS"}, nil, user.ResetPassword)
|
||||||
AddPublic(rt, "share/{spaceID}", []string{"POST", "OPTIONS"}, nil, space.AcceptInvitation)
|
AddPublic(rt, "share/{spaceID}", []string{"POST", "OPTIONS"}, nil, space.AcceptInvitation)
|
||||||
AddPublic(rt, "attachments/{orgID}/{attachmentID}", []string{"GET", "OPTIONS"}, nil, attachment.Download)
|
AddPublic(rt, "attachment/{orgID}/{attachmentID}", []string{"GET", "OPTIONS"}, nil, attachment.Download)
|
||||||
|
|
||||||
//**************************************************
|
//**************************************************
|
||||||
// Secured private routes (require authentication)
|
// Secured private routes (require authentication)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue