Fling User Download
File listing and dynamic download button
This commit is contained in:
parent
c1171e8376
commit
4d9b3d0d87
10 changed files with 279 additions and 15 deletions
|
@ -15,6 +15,8 @@ public class FlingDto {
|
|||
|
||||
private Long id;
|
||||
|
||||
private Instant creationTime;
|
||||
|
||||
@JsonIgnore
|
||||
private Boolean directDownload;
|
||||
|
||||
|
@ -91,4 +93,9 @@ public class FlingDto {
|
|||
|
||||
return expiration;
|
||||
}
|
||||
|
||||
@JsonProperty("creationTime")
|
||||
public Long getJsonUploadTime() {
|
||||
return creationTime.toEpochMilli();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
package net.friedl.fling.security;
|
||||
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.friedl.fling.security.authentication.FlingToken;
|
||||
import net.friedl.fling.security.authentication.dto.UserAuthDto;
|
||||
import net.friedl.fling.service.FlingService;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AuthorizationService {
|
||||
private FlingService flingService;
|
||||
|
@ -18,11 +22,13 @@ public class AuthorizationService {
|
|||
this.flingService = flingService;
|
||||
}
|
||||
|
||||
public boolean allowUpload(Long flingId) {
|
||||
return flingService
|
||||
.findFlingById(flingId)
|
||||
.orElseThrow()
|
||||
.getAllowUpload();
|
||||
public boolean allowUpload(Long flingId, FlingToken authentication) {
|
||||
if (authentication.getGrantedFlingAuthority().getAuthority().equals(FlingAuthority.FLING_OWNER.name())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return flingService.findFlingById(flingId).orElseThrow().getAllowUpload()
|
||||
&& authentication.getGrantedFlingAuthority().getFlingId().equals(flingId);
|
||||
}
|
||||
|
||||
public boolean allowFlingAccess(UserAuthDto userAuth, String shareUrl) {
|
||||
|
@ -30,7 +36,7 @@ public class AuthorizationService {
|
|||
}
|
||||
|
||||
public boolean allowFlingAccess(Long flingId, FlingToken authentication) {
|
||||
if(authentication.getGrantedFlingAuthority().getAuthority().equals(FlingAuthority.FLING_OWNER.name())) {
|
||||
if (authentication.getGrantedFlingAuthority().getAuthority().equals(FlingAuthority.FLING_OWNER.name())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -38,14 +44,22 @@ public class AuthorizationService {
|
|||
}
|
||||
|
||||
public boolean allowFlingAccess(FlingToken authentication, HttpServletRequest request) {
|
||||
if(authentication.getGrantedFlingAuthority().getAuthority().equals(FlingAuthority.FLING_OWNER.name())) {
|
||||
if (authentication.getGrantedFlingAuthority().getAuthority().equals(FlingAuthority.FLING_OWNER.name())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var shareId = request.getParameter("shareId");
|
||||
var flingId = shareId != null
|
||||
? flingService.findFlingByShareId(shareId).orElseThrow().getId()
|
||||
: request.getParameter("flingId");
|
||||
|
||||
Long flingId;
|
||||
|
||||
try {
|
||||
flingId = shareId != null
|
||||
? flingService.findFlingByShareId(shareId).orElseThrow().getId()
|
||||
: Long.parseLong(request.getParameter("flingId"));
|
||||
} catch (NumberFormatException | NoSuchElementException e) {
|
||||
log.warn("Invalid shareId [shareId=\"{}\"] or flingId [flingId=\"{}\"] found", request.getParameter("shareId"), request.getParameter("flingId"));
|
||||
flingId = null;
|
||||
}
|
||||
|
||||
return authentication.getGrantedFlingAuthority().getFlingId().equals(flingId);
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
|
|||
.and()
|
||||
.authorizeRequests()
|
||||
.antMatchers(HttpMethod.POST, "/api/artifacts/{flingId}/**")
|
||||
.access("hasAuthority('"+FlingAuthority.FLING_USER.name()+"') and @authorizationService.allowUpload(#flingId)")
|
||||
.access("@authorizationService.allowUpload(#flingId, authentication)")
|
||||
.and()
|
||||
.authorizeRequests()
|
||||
// TODO: This is still insecure since URLs are not encrypted
|
||||
|
@ -82,6 +82,12 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
|
|||
.regexMatchers(HttpMethod.GET, "\\/api\\/fling\\?(shareId=|flingId=)[a-zA-Z0-9]+")
|
||||
.access("@authorizationService.allowFlingAccess(authentication, request)")
|
||||
.and()
|
||||
.authorizeRequests()
|
||||
// TODO: Security by request parameters is just not well supported with spring security
|
||||
// TODO: Change API
|
||||
.regexMatchers(HttpMethod.GET, "\\/api\\/artifacts\\?(shareId=|flingId=)[a-zA-Z0-9]+")
|
||||
.access("@authorizationService.allowFlingAccess(authentication, request)")
|
||||
.and()
|
||||
.authorizeRequests()
|
||||
.antMatchers(HttpMethod.GET, "/api/fling/{flingId}/**")
|
||||
.access("@authorizationService.allowFlingAccess(#flingId, authentication)")
|
||||
|
|
|
@ -25,6 +25,8 @@ fling:
|
|||
allowed-origins:
|
||||
- "https://friedl.net"
|
||||
- "http://localhost:3000"
|
||||
- "http://localhost:5000"
|
||||
- "http://10.0.2.2:5000"
|
||||
admin-user: "${FLING_ADMIN_USER:admin}"
|
||||
admin-password: "${FLING_ADMIN_PASSWORD:123}"
|
||||
signing-key: "${FLING_SIGNING_KEY:changeitchangeitchangeitchangeit}"
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"@testing-library/user-event": "^7.2.1",
|
||||
"axios": "^0.19.2",
|
||||
"classnames": "^2.2.6",
|
||||
"core-js": "^3.6.5",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"loglevel": "^1.6.8",
|
||||
"node-sass": "^4.14.0",
|
||||
|
@ -27,7 +28,7 @@
|
|||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
|
|
@ -44,8 +44,12 @@ export default function LandingPage() {
|
|||
<h5>I got a code...</h5>
|
||||
<img src={view_fling} alt="Fling view" />
|
||||
<div className="input-group mt-2">
|
||||
<input type="text" className="form-input input-sm" value={shareId} onChange={changeShareId} placeholder="My code" />
|
||||
<button className="btn btn-secondary input-group-btn btn-sm" onClick={openFling}>Open</button>
|
||||
<form onSubmit={openFling}>
|
||||
<div className="input-group">
|
||||
<input type="text" className="form-input input-sm" value={shareId} onChange={changeShareId} placeholder="My code" />
|
||||
<input type="submit" className="btn btn-secondary input-group-btn btn-sm" onClick={openFling} value="Open"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -6,6 +6,7 @@ import {useParams, BrowserRouter} from 'react-router-dom';
|
|||
import {flingClient} from '../../util/flingclient';
|
||||
|
||||
import DirectDownload from './DirectDownload';
|
||||
import FlingUserList from './FlingUserList';
|
||||
|
||||
export default function FlingUser() {
|
||||
let { shareId } = useParams();
|
||||
|
@ -18,7 +19,7 @@ export default function FlingUser() {
|
|||
|
||||
return(
|
||||
<div>
|
||||
{fling.sharing && fling.sharing.directDownload ? <DirectDownload fling={fling} />: ""}
|
||||
{fling.sharing && fling.sharing.directDownload ? <DirectDownload fling={fling} />: <FlingUserList fling={fling} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
163
web/fling/src/components/user/FlingUserList.jsx
Normal file
163
web/fling/src/components/user/FlingUserList.jsx
Normal file
|
@ -0,0 +1,163 @@
|
|||
import log from 'loglevel';
|
||||
import React, {useState, useEffect, useRef} from 'react';
|
||||
|
||||
import {useParams, BrowserRouter} from 'react-router-dom';
|
||||
|
||||
import {flingClient, artifactClient} from '../../util/flingclient';
|
||||
|
||||
function Artifacts(props) {
|
||||
let [artifacts, setArtifacts] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if(!props.fling) return;
|
||||
|
||||
artifactClient.getArtifacts(props.fling.id)
|
||||
.then((artifacts) => setArtifacts(artifacts));
|
||||
}, [props.fling]);
|
||||
|
||||
function renderArtifact(artifact) {
|
||||
function readableBytes(bytes) {
|
||||
if(bytes <= 0) return "0 KB";
|
||||
|
||||
var i = Math.floor(Math.log(bytes) / Math.log(1024)),
|
||||
sizes = ['Byte', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function localizedDate(t) {
|
||||
let d = new Date(t);
|
||||
return d.toLocaleDateString();
|
||||
}
|
||||
|
||||
function getArtifactInfo() {
|
||||
return `${readableBytes(artifact.size)}, ${localizedDate(artifact.uploadTime)}`;
|
||||
}
|
||||
|
||||
return(
|
||||
<div className="user-list-artifact">
|
||||
<div className="container">
|
||||
<div className="columns">
|
||||
<div className="column col-8 col-sm-12">
|
||||
{artifact.name}<br />
|
||||
</div>
|
||||
<div className="column col-2 col-sm-6">
|
||||
<div className="text-gray">{readableBytes(artifact.size)}</div>
|
||||
</div>
|
||||
<div className="column col-2 col-sm-6">
|
||||
<div className="text-gray float-right">{localizedDate(artifact.uploadTime)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{artifacts.map(renderArtifact)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FlingUserList(props) {
|
||||
let iframeContainer = useRef(null);
|
||||
let [infoText, setInfoText] = useState("");
|
||||
let [inProgress, setInProgress] = useState(false);
|
||||
let [downloadUrl, setDownloadUrl] = useState("");
|
||||
|
||||
useEffect(() => flingInfo(props.fling.id), [props.fling.id]);
|
||||
|
||||
function flingInfo(flingId) {
|
||||
if(!flingId) return;
|
||||
|
||||
function readableBytes(bytes) {
|
||||
if(bytes <= 0) return "0 KB";
|
||||
|
||||
var i = Math.floor(Math.log(bytes) / Math.log(1024)),
|
||||
sizes = ['Byte', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function localizedDate(t) {
|
||||
let d = new Date(t);
|
||||
return d.toLocaleDateString();
|
||||
}
|
||||
|
||||
artifactClient.getArtifacts(flingId)
|
||||
.then((artifacts) => {
|
||||
let totalSize = 0;
|
||||
let countArtifacts = 0;
|
||||
|
||||
for(let artifact of artifacts) {
|
||||
totalSize += artifact.size;
|
||||
countArtifacts++;
|
||||
}
|
||||
|
||||
setInfoText(`${localizedDate(props.fling.creationTime)} - ${countArtifacts} files - ${readableBytes(totalSize)}`);
|
||||
});
|
||||
}
|
||||
|
||||
function handleDownload(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
setInProgress(true);
|
||||
|
||||
flingClient.packageFling(props.fling.id)
|
||||
.then(downloadUrl => {
|
||||
// We need this iframe hack because with a regular href, while
|
||||
// the browser downloads the file fine, it also reloads the page, hence
|
||||
// loosing all logs and state
|
||||
let frame = document.createElement("iframe");
|
||||
frame.src = downloadUrl;
|
||||
iframeContainer.current.appendChild(frame);
|
||||
setDownloadUrl(downloadUrl);
|
||||
setInProgress(false);
|
||||
});
|
||||
}
|
||||
|
||||
return(
|
||||
<>
|
||||
|
||||
<div className="container-center">
|
||||
|
||||
<div className="col-6 col-xl-8 col-lg-10 col-md-12">
|
||||
|
||||
<h3 className="ml-2"> {props.fling.name} </h3>
|
||||
<div className="ml-2 text-gray">{infoText}</div>
|
||||
|
||||
<div className="card">
|
||||
<ul className="tab mx-2">
|
||||
<li className="tab-item active">
|
||||
<a>Files</a>
|
||||
</li>
|
||||
<li className="tab-item">
|
||||
<a>Upload</a>
|
||||
</li>
|
||||
|
||||
<li className="tab-item tab-action">
|
||||
<div className="card-title">
|
||||
{inProgress
|
||||
? <button className="m-2 btn btn-xs btn-secondary float-right user-list-loading" disabled="true"
|
||||
onClick={(ev) => ev.preventDefault()}>
|
||||
<div className="loading" /> Packaging
|
||||
</button>
|
||||
: <button className="m-2 btn btn-xs btn-secondary float-right" onClick={handleDownload}>Download</button>
|
||||
}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="card-body">
|
||||
<Artifacts fling={props.fling} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-hide" ref={iframeContainer} />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
25
web/fling/src/components/user/FlingUserUpload.jsx
Normal file
25
web/fling/src/components/user/FlingUserUpload.jsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import log from 'loglevel';
|
||||
import React, {useState, useEffect} from 'react';
|
||||
|
||||
import {useParams, BrowserRouter} from 'react-router-dom';
|
||||
|
||||
import {flingClient} from '../../util/flingclient';
|
||||
|
||||
import DirectDownload from './DirectDownload';
|
||||
|
||||
export default function FlingUserUpload(props) {
|
||||
let { shareId } = useParams();
|
||||
let [fling, setFling] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
flingClient.getFlingByShareId(shareId)
|
||||
.then(f => setFling(f));
|
||||
}, [shareId]);
|
||||
|
||||
return(
|
||||
<div>
|
||||
{fling.sharing && fling.sharing.directDownload ? <DirectDownload fling={fling} />: ""}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -11,6 +11,10 @@ body {
|
|||
background-color: $canvas-base-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
@include shadow;
|
||||
}
|
||||
|
||||
/*********\
|
||||
| Login |
|
||||
\*********/
|
||||
|
@ -312,3 +316,40 @@ tbody td {
|
|||
width: 12rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/*****************\
|
||||
| FlingUserList |
|
||||
\*****************/
|
||||
|
||||
.user-list-loading {
|
||||
.loading {
|
||||
display: inline-block;
|
||||
|
||||
&::after {
|
||||
position: relative;
|
||||
left: unset;
|
||||
top: unset;
|
||||
display: inline-block;
|
||||
margin: unset;
|
||||
animation-duration: 1s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-list-artifact {
|
||||
word-break: break-all;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
&:not(:last-child)::after {
|
||||
@extend .user-list-divider;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
.user-list-divider {
|
||||
@extend .divider;
|
||||
transform: scale(0.98, 1);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue