WebClient: load shares using an async request
instead of rendering them directly within the template Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
364c9c8162
commit
c23d779280
5 changed files with 70 additions and 51 deletions
|
@ -7014,7 +7014,7 @@ func TestProviderErrors(t *testing.T) {
|
|||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), util.I18nErrorGetUser)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, webClientSharesPath, nil)
|
||||
req, err = http.NewRequest(http.MethodGet, webClientSharesPath+jsonAPISuffix, nil)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, userWebToken)
|
||||
rr = executeRequest(req)
|
||||
|
@ -18518,14 +18518,14 @@ func TestWebUserShare(t *testing.T) {
|
|||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, webClientSharesPath+"?qlimit=aa", nil)
|
||||
req, err = http.NewRequest(http.MethodGet, webClientSharesPath, nil)
|
||||
assert.NoError(t, err)
|
||||
req.RemoteAddr = defaultRemoteAddr
|
||||
setJWTCookieForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, webClientSharesPath+"?qlimit=1", nil) //nolint:goconst
|
||||
req, err = http.NewRequest(http.MethodGet, webClientSharesPath+jsonAPISuffix, nil) //nolint:goconst
|
||||
assert.NoError(t, err)
|
||||
req.RemoteAddr = defaultRemoteAddr
|
||||
setJWTCookieForReq(req, token)
|
||||
|
|
|
@ -2645,9 +2645,9 @@ func TestWebUserInvalidClaims(t *testing.T) {
|
|||
assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest(http.MethodGet, webClientSharesPath, nil)
|
||||
req, _ = http.NewRequest(http.MethodGet, webClientSharesPath+jsonAPISuffix, nil)
|
||||
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
|
||||
server.handleClientGetShares(rr, req)
|
||||
getAllShares(rr, req)
|
||||
assert.Equal(t, http.StatusForbidden, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken)
|
||||
|
||||
|
|
|
@ -1613,6 +1613,8 @@ func (s *httpdServer) setupWebClientRoutes() {
|
|||
Get(webClientRecoveryCodesPath, getRecoveryCodes)
|
||||
router.With(s.checkHTTPUserPerm(sdk.WebClientMFADisabled), verifyCSRFHeader).
|
||||
Post(webClientRecoveryCodesPath, generateRecoveryCodes)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled), compressor.Handler, s.refreshCookie).
|
||||
Get(webClientSharesPath+jsonAPISuffix, getAllShares)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled), s.refreshCookie).
|
||||
Get(webClientSharesPath, s.handleClientGetShares)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled), s.refreshCookie).
|
||||
|
|
|
@ -197,7 +197,6 @@ type clientMFAPage struct {
|
|||
|
||||
type clientSharesPage struct {
|
||||
baseClientPage
|
||||
Shares []dataprovider.Share
|
||||
BasePublicSharesURL string
|
||||
}
|
||||
|
||||
|
@ -1515,36 +1514,33 @@ func (s *httpdServer) handleClientUpdateSharePost(w http.ResponseWriter, r *http
|
|||
}
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleClientGetShares(w http.ResponseWriter, r *http.Request) {
|
||||
func getAllShares(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
claims, err := getTokenClaims(r)
|
||||
if err != nil || claims.Username == "" {
|
||||
s.renderClientForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
|
||||
sendAPIResponse(w, r, nil, util.I18nErrorInvalidToken, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
limit := defaultQueryLimit
|
||||
if _, ok := r.URL.Query()["qlimit"]; ok {
|
||||
var err error
|
||||
limit, err = strconv.Atoi(r.URL.Query().Get("qlimit"))
|
||||
if err != nil {
|
||||
limit = defaultQueryLimit
|
||||
}
|
||||
}
|
||||
shares := make([]dataprovider.Share, 0, limit)
|
||||
shares := make([]dataprovider.Share, 0, 10)
|
||||
for {
|
||||
sh, err := dataprovider.GetShares(limit, len(shares), dataprovider.OrderASC, claims.Username)
|
||||
sh, err := dataprovider.GetShares(defaultQueryLimit, len(shares), dataprovider.OrderASC, claims.Username)
|
||||
if err != nil {
|
||||
s.renderClientInternalServerErrorPage(w, r, err)
|
||||
sendAPIResponse(w, r, err, getI18NErrorString(err, util.I18nError500Message), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
shares = append(shares, sh...)
|
||||
if len(sh) < limit {
|
||||
if len(sh) < defaultQueryLimit {
|
||||
break
|
||||
}
|
||||
}
|
||||
render.JSON(w, r, shares)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleClientGetShares(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
|
||||
data := clientSharesPage{
|
||||
baseClientPage: s.getBaseClientPageData(util.I18nSharesTitle, webClientSharesPath, r),
|
||||
Shares: shares,
|
||||
BasePublicSharesURL: webClientPubSharesPath,
|
||||
}
|
||||
renderClientTemplate(w, templateClientShares, data)
|
||||
|
|
|
@ -20,6 +20,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
{{- end}}
|
||||
|
||||
{{- define "page_body"}}
|
||||
{{- template "errmsg" ""}}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h3 data-i18n="share.view_manage" class="card-title section-title">View and manage shares</h3>
|
||||
|
@ -73,7 +74,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
</div>
|
||||
</div>
|
||||
<div class="modal-body fs-5">
|
||||
<div id="readShare">
|
||||
<div id="readShare" class="mb-5">
|
||||
<div class="mb-3">
|
||||
<h4 data-i18n="share.link_single_title">Single zip file</h4>
|
||||
<p data-i18n="share.link_single_desc">You can download shared content as a single zip file</p>
|
||||
|
@ -133,7 +134,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="writeShare">
|
||||
<div id="writeShare" class="mb-5">
|
||||
<p data-i18n="share.upload_desc">You can upload one or more files to the shared directory</p>
|
||||
<button id="writePageLinkCopy" data-clipboard-target="#writePageLink" type="button" class="btn btn-flex btn-light-primary btn-clipboard-copy me-3">
|
||||
<i class="ki-duotone ki-fasten fs-2">
|
||||
|
@ -150,7 +151,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
<span data-i18n="fs.upload.text">Upload</span>
|
||||
</a>
|
||||
</div>
|
||||
<div data-i18n="share.expired_desc" id="expiredShare" class="fw-semibold">
|
||||
<div data-i18n="share.expired_desc" id="expiredShare" class="fw-semibold fs-4 mb-5">
|
||||
This share is no longer accessible because it has expired
|
||||
</div>
|
||||
</div>
|
||||
|
@ -222,8 +223,8 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
window.location.replace('{{.ShareURL}}' + "/" + encodeURIComponent(shareID));
|
||||
}
|
||||
|
||||
function showShareLink(shareID, shareScope, isExpired) {
|
||||
if (isExpired == "1") {
|
||||
function showShareLink(shareID, shareScope, expiresAt) {
|
||||
if (expiresAt < Date.now()) {
|
||||
$('#expiredShare').show();
|
||||
$('#writeShare').hide();
|
||||
$('#readShare').hide();
|
||||
|
@ -254,20 +255,35 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
$('#link_modal').modal('show');
|
||||
}
|
||||
|
||||
const tableData = [];
|
||||
{{- range .Shares}}
|
||||
tableData.push(['{{.Name}}','{{.Scope}}','{{- if .Password}}1{{- else}}0{{- end}}','{{.ShareID}}','{{- if .IsExpired}}1{{- else}}0{{- end}}', '{{.ExpiresAt}}', '{{.LastUseAt}}', '{{.UsedTokens}}', '{{.MaxTokens}}']);
|
||||
{{- end}}
|
||||
|
||||
var sharesDatatable = function(){
|
||||
var dt;
|
||||
|
||||
var initDatatable = function () {
|
||||
dt = $('#dataTable').DataTable({
|
||||
data: tableData,
|
||||
columnDefs: [
|
||||
ajax: {
|
||||
url: "{{.SharesURL}}/json",
|
||||
dataSrc: "",
|
||||
error: function ($xhr, textStatus, errorThrown) {
|
||||
$(".dataTables_processing").hide();
|
||||
let txt = "";
|
||||
if ($xhr) {
|
||||
let json = $xhr.responseJSON;
|
||||
if (json) {
|
||||
if (json.message){
|
||||
txt = json.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!txt){
|
||||
txt = "general.error500";
|
||||
}
|
||||
setI18NData($('#errorTxt'), txt);
|
||||
$('#errorMsg').removeClass("d-none");
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
target: 0,
|
||||
data: "name",
|
||||
render: function(data, type, row) {
|
||||
if (type === 'display') {
|
||||
return escapeHTML(data);
|
||||
|
@ -276,13 +292,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
}
|
||||
},
|
||||
{
|
||||
target: 1,
|
||||
data: "scope",
|
||||
render: function (data, type, row) {
|
||||
if (type === 'display') {
|
||||
switch (data){
|
||||
case "2":
|
||||
case 2:
|
||||
return $.t('share.scope_write');
|
||||
case "3":
|
||||
case 3:
|
||||
return $.t('share.scope_read_write');
|
||||
default:
|
||||
return $.t('share.scope_read');
|
||||
|
@ -292,34 +308,39 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
}
|
||||
},
|
||||
{
|
||||
target: 2,
|
||||
data: "expires_at",
|
||||
defaultContent: 0,
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
render: function (data, type, row) {
|
||||
if (type === 'display') {
|
||||
let info = "";
|
||||
if (row[5] > 0){
|
||||
if (row.expires_at && row.expires_at > 0){
|
||||
info+= $.t('share.expiration_date', {
|
||||
val: parseInt(row[5], 10),
|
||||
val: row.expires_at,
|
||||
formatParams: {
|
||||
val: { year: 'numeric', month: 'numeric', day: 'numeric' },
|
||||
}
|
||||
});
|
||||
}
|
||||
if (row[6] > 0){
|
||||
if (row.last_use_at && row.last_use_at > 0){
|
||||
info+= $.t('share.last_use', {
|
||||
val: parseInt(row[6], 10),
|
||||
val: row.last_use_at,
|
||||
formatParams: {
|
||||
val: { year: 'numeric', month: 'numeric', day: 'numeric' },
|
||||
}
|
||||
});
|
||||
}
|
||||
if (row[8] > 0){
|
||||
info+= $.t('share.usage', {used: row[7], total: row[8]})
|
||||
} else {
|
||||
info+= $.t('share.used_tokens', {used: row[7]})
|
||||
let used_tokens = 0;
|
||||
if (row.used_tokens && row.used_tokens > 0){
|
||||
used_tokens = row.used_tokens;
|
||||
}
|
||||
if (data == "1"){
|
||||
if (row.max_tokens && row.max_tokens > 0){
|
||||
info+= $.t('share.usage', {used: used_tokens, total: row.max_tokens});
|
||||
} else {
|
||||
info+= $.t('share.used_tokens', {used: used_tokens});
|
||||
}
|
||||
if (row.password){
|
||||
info+= $.t('share.password_protected')
|
||||
}
|
||||
return info;
|
||||
|
@ -328,7 +349,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
}
|
||||
},
|
||||
{
|
||||
targets: 3,
|
||||
data: "id",
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
className: 'text-end',
|
||||
|
@ -423,7 +444,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
el.on("click", function(e){
|
||||
e.preventDefault();
|
||||
const parent = e.target.closest('tr');
|
||||
editAction(dt.row(parent).data()[3]);
|
||||
editAction(dt.row(parent).data()["id"]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -435,7 +456,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
el.on("click", function(e){
|
||||
e.preventDefault();
|
||||
const parent = e.target.closest('tr');
|
||||
deleteAction(dt.row(parent).data()[3]);
|
||||
deleteAction(dt.row(parent).data()["id"]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -447,7 +468,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
el.on("click", function(e){
|
||||
e.preventDefault();
|
||||
let rowData = dt.row(e.target.closest('tr')).data();
|
||||
showShareLink(rowData[3], rowData[1], rowData[4]);
|
||||
showShareLink(rowData["id"], rowData["scope"], rowData["expires_at"]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue