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 Long id;
|
||||||
|
|
||||||
|
private Instant creationTime;
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private Boolean directDownload;
|
private Boolean directDownload;
|
||||||
|
|
||||||
|
@ -91,4 +93,9 @@ public class FlingDto {
|
||||||
|
|
||||||
return expiration;
|
return expiration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonProperty("creationTime")
|
||||||
|
public Long getJsonUploadTime() {
|
||||||
|
return creationTime.toEpochMilli();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
package net.friedl.fling.security;
|
package net.friedl.fling.security;
|
||||||
|
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import net.friedl.fling.security.authentication.FlingToken;
|
import net.friedl.fling.security.authentication.FlingToken;
|
||||||
import net.friedl.fling.security.authentication.dto.UserAuthDto;
|
import net.friedl.fling.security.authentication.dto.UserAuthDto;
|
||||||
import net.friedl.fling.service.FlingService;
|
import net.friedl.fling.service.FlingService;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class AuthorizationService {
|
public class AuthorizationService {
|
||||||
private FlingService flingService;
|
private FlingService flingService;
|
||||||
|
@ -18,11 +22,13 @@ public class AuthorizationService {
|
||||||
this.flingService = flingService;
|
this.flingService = flingService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean allowUpload(Long flingId) {
|
public boolean allowUpload(Long flingId, FlingToken authentication) {
|
||||||
return flingService
|
if (authentication.getGrantedFlingAuthority().getAuthority().equals(FlingAuthority.FLING_OWNER.name())) {
|
||||||
.findFlingById(flingId)
|
return true;
|
||||||
.orElseThrow()
|
}
|
||||||
.getAllowUpload();
|
|
||||||
|
return flingService.findFlingById(flingId).orElseThrow().getAllowUpload()
|
||||||
|
&& authentication.getGrantedFlingAuthority().getFlingId().equals(flingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean allowFlingAccess(UserAuthDto userAuth, String shareUrl) {
|
public boolean allowFlingAccess(UserAuthDto userAuth, String shareUrl) {
|
||||||
|
@ -43,9 +49,17 @@ public class AuthorizationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
var shareId = request.getParameter("shareId");
|
var shareId = request.getParameter("shareId");
|
||||||
var flingId = shareId != null
|
|
||||||
|
Long flingId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
flingId = shareId != null
|
||||||
? flingService.findFlingByShareId(shareId).orElseThrow().getId()
|
? flingService.findFlingByShareId(shareId).orElseThrow().getId()
|
||||||
: request.getParameter("flingId");
|
: 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);
|
return authentication.getGrantedFlingAuthority().getFlingId().equals(flingId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,7 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
|
||||||
.and()
|
.and()
|
||||||
.authorizeRequests()
|
.authorizeRequests()
|
||||||
.antMatchers(HttpMethod.POST, "/api/artifacts/{flingId}/**")
|
.antMatchers(HttpMethod.POST, "/api/artifacts/{flingId}/**")
|
||||||
.access("hasAuthority('"+FlingAuthority.FLING_USER.name()+"') and @authorizationService.allowUpload(#flingId)")
|
.access("@authorizationService.allowUpload(#flingId, authentication)")
|
||||||
.and()
|
.and()
|
||||||
.authorizeRequests()
|
.authorizeRequests()
|
||||||
// TODO: This is still insecure since URLs are not encrypted
|
// 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]+")
|
.regexMatchers(HttpMethod.GET, "\\/api\\/fling\\?(shareId=|flingId=)[a-zA-Z0-9]+")
|
||||||
.access("@authorizationService.allowFlingAccess(authentication, request)")
|
.access("@authorizationService.allowFlingAccess(authentication, request)")
|
||||||
.and()
|
.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()
|
.authorizeRequests()
|
||||||
.antMatchers(HttpMethod.GET, "/api/fling/{flingId}/**")
|
.antMatchers(HttpMethod.GET, "/api/fling/{flingId}/**")
|
||||||
.access("@authorizationService.allowFlingAccess(#flingId, authentication)")
|
.access("@authorizationService.allowFlingAccess(#flingId, authentication)")
|
||||||
|
|
|
@ -25,6 +25,8 @@ fling:
|
||||||
allowed-origins:
|
allowed-origins:
|
||||||
- "https://friedl.net"
|
- "https://friedl.net"
|
||||||
- "http://localhost:3000"
|
- "http://localhost:3000"
|
||||||
|
- "http://localhost:5000"
|
||||||
|
- "http://10.0.2.2:5000"
|
||||||
admin-user: "${FLING_ADMIN_USER:admin}"
|
admin-user: "${FLING_ADMIN_USER:admin}"
|
||||||
admin-password: "${FLING_ADMIN_PASSWORD:123}"
|
admin-password: "${FLING_ADMIN_PASSWORD:123}"
|
||||||
signing-key: "${FLING_SIGNING_KEY:changeitchangeitchangeitchangeit}"
|
signing-key: "${FLING_SIGNING_KEY:changeitchangeitchangeitchangeit}"
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
"@testing-library/user-event": "^7.2.1",
|
"@testing-library/user-event": "^7.2.1",
|
||||||
"axios": "^0.19.2",
|
"axios": "^0.19.2",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
|
"core-js": "^3.6.5",
|
||||||
"jwt-decode": "^2.2.0",
|
"jwt-decode": "^2.2.0",
|
||||||
"loglevel": "^1.6.8",
|
"loglevel": "^1.6.8",
|
||||||
"node-sass": "^4.14.0",
|
"node-sass": "^4.14.0",
|
||||||
|
|
|
@ -44,8 +44,12 @@ export default function LandingPage() {
|
||||||
<h5>I got a code...</h5>
|
<h5>I got a code...</h5>
|
||||||
<img src={view_fling} alt="Fling view" />
|
<img src={view_fling} alt="Fling view" />
|
||||||
<div className="input-group mt-2">
|
<div className="input-group mt-2">
|
||||||
|
<form onSubmit={openFling}>
|
||||||
|
<div className="input-group">
|
||||||
<input type="text" className="form-input input-sm" value={shareId} onChange={changeShareId} placeholder="My code" />
|
<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>
|
<input type="submit" className="btn btn-secondary input-group-btn btn-sm" onClick={openFling} value="Open"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {useParams, BrowserRouter} from 'react-router-dom';
|
||||||
import {flingClient} from '../../util/flingclient';
|
import {flingClient} from '../../util/flingclient';
|
||||||
|
|
||||||
import DirectDownload from './DirectDownload';
|
import DirectDownload from './DirectDownload';
|
||||||
|
import FlingUserList from './FlingUserList';
|
||||||
|
|
||||||
export default function FlingUser() {
|
export default function FlingUser() {
|
||||||
let { shareId } = useParams();
|
let { shareId } = useParams();
|
||||||
|
@ -18,7 +19,7 @@ export default function FlingUser() {
|
||||||
|
|
||||||
return(
|
return(
|
||||||
<div>
|
<div>
|
||||||
{fling.sharing && fling.sharing.directDownload ? <DirectDownload fling={fling} />: ""}
|
{fling.sharing && fling.sharing.directDownload ? <DirectDownload fling={fling} />: <FlingUserList fling={fling} />}
|
||||||
</div>
|
</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;
|
background-color: $canvas-base-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
@include shadow;
|
||||||
|
}
|
||||||
|
|
||||||
/*********\
|
/*********\
|
||||||
| Login |
|
| Login |
|
||||||
\*********/
|
\*********/
|
||||||
|
@ -312,3 +316,40 @@ tbody td {
|
||||||
width: 12rem;
|
width: 12rem;
|
||||||
text-align: center;
|
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