From 4d9b3d0d87fe139c94c4b465a1f6e5d066fdcbf5 Mon Sep 17 00:00:00 2001 From: Armin Friedl Date: Thu, 4 Jun 2020 22:28:30 +0200 Subject: [PATCH] Fling User Download File listing and dynamic download button --- .../net/friedl/fling/model/dto/FlingDto.java | 7 + .../fling/security/AuthorizationService.java | 34 ++-- .../security/FlingWebSecurityConfigurer.java | 8 +- .../src/main/resources/application-local.yml | 2 + web/fling/package.json | 3 +- web/fling/src/components/LandingPage.jsx | 8 +- web/fling/src/components/user/FlingUser.jsx | 3 +- .../src/components/user/FlingUserList.jsx | 163 ++++++++++++++++++ .../src/components/user/FlingUserUpload.jsx | 25 +++ web/fling/src/style/fling.scss | 41 +++++ 10 files changed, 279 insertions(+), 15 deletions(-) create mode 100644 web/fling/src/components/user/FlingUserList.jsx create mode 100644 web/fling/src/components/user/FlingUserUpload.jsx diff --git a/service/fling/src/main/java/net/friedl/fling/model/dto/FlingDto.java b/service/fling/src/main/java/net/friedl/fling/model/dto/FlingDto.java index 9c437af..0be3fc4 100644 --- a/service/fling/src/main/java/net/friedl/fling/model/dto/FlingDto.java +++ b/service/fling/src/main/java/net/friedl/fling/model/dto/FlingDto.java @@ -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(); + } } diff --git a/service/fling/src/main/java/net/friedl/fling/security/AuthorizationService.java b/service/fling/src/main/java/net/friedl/fling/security/AuthorizationService.java index ab98525..6c06ab4 100644 --- a/service/fling/src/main/java/net/friedl/fling/security/AuthorizationService.java +++ b/service/fling/src/main/java/net/friedl/fling/security/AuthorizationService.java @@ -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); } diff --git a/service/fling/src/main/java/net/friedl/fling/security/FlingWebSecurityConfigurer.java b/service/fling/src/main/java/net/friedl/fling/security/FlingWebSecurityConfigurer.java index 9a5b5df..892139c 100644 --- a/service/fling/src/main/java/net/friedl/fling/security/FlingWebSecurityConfigurer.java +++ b/service/fling/src/main/java/net/friedl/fling/security/FlingWebSecurityConfigurer.java @@ -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)") diff --git a/service/fling/src/main/resources/application-local.yml b/service/fling/src/main/resources/application-local.yml index 0a62b0d..ffaf6ee 100644 --- a/service/fling/src/main/resources/application-local.yml +++ b/service/fling/src/main/resources/application-local.yml @@ -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}" diff --git a/web/fling/package.json b/web/fling/package.json index b7e4bd3..20e6ba4 100644 --- a/web/fling/package.json +++ b/web/fling/package.json @@ -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": [ diff --git a/web/fling/src/components/LandingPage.jsx b/web/fling/src/components/LandingPage.jsx index d59e82e..4f2d774 100644 --- a/web/fling/src/components/LandingPage.jsx +++ b/web/fling/src/components/LandingPage.jsx @@ -44,8 +44,12 @@ export default function LandingPage() {
I got a code...
Fling view
- - +
+
+ + +
+
diff --git a/web/fling/src/components/user/FlingUser.jsx b/web/fling/src/components/user/FlingUser.jsx index 7a118fc..874efca 100644 --- a/web/fling/src/components/user/FlingUser.jsx +++ b/web/fling/src/components/user/FlingUser.jsx @@ -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(
- {fling.sharing && fling.sharing.directDownload ? : ""} + {fling.sharing && fling.sharing.directDownload ? : }
); } diff --git a/web/fling/src/components/user/FlingUserList.jsx b/web/fling/src/components/user/FlingUserList.jsx new file mode 100644 index 0000000..bfde251 --- /dev/null +++ b/web/fling/src/components/user/FlingUserList.jsx @@ -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( +
+
+
+
+ {artifact.name}
+
+
+
{readableBytes(artifact.size)}
+
+
+
{localizedDate(artifact.uploadTime)}
+
+
+
+
+ ); + } + + return ( +
+ {artifacts.map(renderArtifact)} +
+ ); +} + +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( + <> + +
+ +
+ +

{props.fling.name}

+
{infoText}
+ +
+
    +
  • + Files +
  • +
  • + Upload +
  • + +
  • +
    + {inProgress + ? + : + } +
    +
  • +
+ +
+ +
+
+ +
+ +
+
+ + ); +} diff --git a/web/fling/src/components/user/FlingUserUpload.jsx b/web/fling/src/components/user/FlingUserUpload.jsx new file mode 100644 index 0000000..6f7c18a --- /dev/null +++ b/web/fling/src/components/user/FlingUserUpload.jsx @@ -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( +
+ {fling.sharing && fling.sharing.directDownload ? : ""} +
+ ); +} + diff --git a/web/fling/src/style/fling.scss b/web/fling/src/style/fling.scss index f1710c5..4dad858 100644 --- a/web/fling/src/style/fling.scss +++ b/web/fling/src/style/fling.scss @@ -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); +}