From 5598cb5ecf5eeff51ecf672b23e14a848a487850 Mon Sep 17 00:00:00 2001 From: Armin Friedl Date: Sat, 25 Jul 2020 21:43:56 +0200 Subject: [PATCH] Finish fling settings --- .../security/FlingWebSecurityConfigurer.java | 6 +- .../friedl/fling/service/FlingService.java | 8 +- web/fling/package-lock.json | 8 +- web/fling/package.json | 2 +- web/fling/src/components/admin/New.jsx | 20 +-- web/fling/src/components/admin/Settings.jsx | 159 +++++++++++------- web/fling/src/redux/actions.js | 23 ++- web/fling/src/util/fc.js | 4 +- 8 files changed, 137 insertions(+), 93 deletions(-) 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 03c2da2..0149767 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 @@ -99,11 +99,15 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter { .antMatchers(HttpMethod.POST, "/api/fling/{flingId}/artifact") .access("@authorizationService.allowUpload(#flingId, authentication)") .and() - // only admin can create, delete and list flings + // only admin can create, delete, list and modify flings .authorizeRequests() .antMatchers(HttpMethod.DELETE, "/api/fling/{flingId}") .hasAnyAuthority(FLING_ADMIN.getAuthority()) .and() + .authorizeRequests() + .antMatchers(HttpMethod.PUT, "/api/fling/{flingId}") + .hasAnyAuthority(FLING_ADMIN.getAuthority()) + .and() .authorizeRequests() .antMatchers(HttpMethod.POST, "/api/fling") .hasAuthority(FLING_ADMIN.getAuthority()) diff --git a/service/fling/src/main/java/net/friedl/fling/service/FlingService.java b/service/fling/src/main/java/net/friedl/fling/service/FlingService.java index a30a9c8..4d2fb25 100644 --- a/service/fling/src/main/java/net/friedl/fling/service/FlingService.java +++ b/service/fling/src/main/java/net/friedl/fling/service/FlingService.java @@ -121,6 +121,7 @@ public class FlingService { } private String hashAuthCode(String authCode) { + if (!StringUtils.hasText(authCode)) return null; String hash = passwordEncoder.encode(authCode); log.debug("Hashed authentication code to {}", hash); return hash; @@ -154,17 +155,14 @@ public class FlingService { flingEntity.setId(id); flingEntity.setAllowUpload(flingDto.getAllowUpload()); flingEntity.setDirectDownload(flingDto.getDirectDownload()); + flingEntity.setShared(flingDto.getShared()); flingEntity.setExpirationClicks(flingDto.getExpirationClicks()); flingEntity.setExpirationTime(flingDto.getExpirationTime()); flingEntity.setName(flingDto.getName()); flingEntity.setShareId(flingDto.getShareId()); - - if(!flingEntity.getAuthCode().equals(flingDto.getAuthCode()) - && !flingEntity.getAuthCode().equals(hashAuthCode(flingDto.getAuthCode()))) { - + if (!flingDto.getAuthCode().equals(flingEntity.getAuthCode())) { flingEntity.setAuthCode(hashAuthCode(flingDto.getAuthCode())); } - return flingMapper.map(flingEntity); } } diff --git a/web/fling/package-lock.json b/web/fling/package-lock.json index d3a5315..dd87126 100644 --- a/web/fling/package-lock.json +++ b/web/fling/package-lock.json @@ -1129,7 +1129,7 @@ "@fling/flingclient": { "version": "0.1.0-snapshot", "resolved": "https://nexus.friedl.net/repository/npm-private/@fling/flingclient/-/flingclient-0.1.0-snapshot.tgz", - "integrity": "sha512-P3JWlmnaYYpj5xS5EFp94OVZXSG9lJbraKlQE4SHnTctxLv3OaR4XOaO7j/FJFygJ3KhOLqVi0x8gQQEDnlMBQ==", + "integrity": "sha512-7paRpY2dM3d2fxCeZhZUUW+KZX8xjxyrlPxbhRcdPnyAFUY13kyczG0N5D4MFKQsquND3Dp7Js2vTKnzixPkpA==", "requires": { "@babel/cli": "^7.0.0", "superagent": "3.7.0" @@ -6618,9 +6618,9 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" }, "immer": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/immer/-/immer-7.0.5.tgz", - "integrity": "sha512-TtRAKZyuqld2eYjvWgXISLJ0ZlOl1OOTzRmrmiY8SlB0dnAhZ1OiykIDL5KDFNaPHDXiLfGQFNJGtet8z8AEmg==" + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/immer/-/immer-7.0.7.tgz", + "integrity": "sha512-Q8yYwVADJXrNfp1ZUAh4XDHkcoE3wpdpb4mC5abDSajs2EbW8+cGdPyAnglMyLnm7EF6ojD2xBFX7L5i4TIytw==" }, "import-cwd": { "version": "2.1.0", diff --git a/web/fling/package.json b/web/fling/package.json index 4babb47..62946c6 100644 --- a/web/fling/package.json +++ b/web/fling/package.json @@ -10,7 +10,7 @@ "axios": "^0.19.2", "classnames": "^2.2.6", "core-js": "^3.6.5", - "immer": "^7.0.5", + "immer": "^7.0.7", "jwt-decode": "^2.2.0", "loglevel": "^1.6.8", "node-sass": "^4.14.1", diff --git a/web/fling/src/components/admin/New.jsx b/web/fling/src/components/admin/New.jsx index 2f3b43a..8c3e055 100644 --- a/web/fling/src/components/admin/New.jsx +++ b/web/fling/src/components/admin/New.jsx @@ -66,17 +66,9 @@ export default function New(props) { const flingClient = new FlingClient(); flingClient.getFlingByShareId(ev.currentTarget.value) - .then(result => { - setShareUrlUnique(false); - }).catch(error => { - if(error.status === 404) { - setShareUrlUnique(true); - } - }).finally(() => { - s.shareUrl = value; - f.sharing = s; - setFling(f); - }); + .then(result => setShareUrlUnique(false)) + .catch(error => error.status === 404 && setShareUrlUnique(true) ) + .finally(() => { s.shareUrl = value; f.sharing = s; setFling(f); }); } function setName(ev) { @@ -157,9 +149,9 @@ export default function New(props) { } } - flingClient.postFling({fling: flingEntity}) - .then(() => handleClose()) - .catch(error => log.error(error)) + flingClient.postFling({ fling: flingEntity }) + .then(() => handleClose()) + .catch(error => log.error(error)) } return ( diff --git a/web/fling/src/components/admin/Settings.jsx b/web/fling/src/components/admin/Settings.jsx index b150f3a..f4dc75f 100644 --- a/web/fling/src/components/admin/Settings.jsx +++ b/web/fling/src/components/admin/Settings.jsx @@ -1,10 +1,8 @@ import log from 'loglevel'; import React, { useState } from 'react'; import produce from 'immer'; -import _ from 'lodash'; import { useSelector, useDispatch } from 'react-redux'; -import { Fling } from '@fling/flingclient'; import { retrieveFlings } from "../../redux/actions"; import { FlingClient } from '../../util/fc'; @@ -17,7 +15,6 @@ export default function Settings(props) { * The active fling from the redux store. Treat this as immutable. */ let activeFling = useSelector(state => state.flings.activeFling); - let _clone = _.cloneDeep(_.toPlainObject(_.cloneDeep(activeFling))); /** * Deep clone the active fling from redux into a draft. Changes to the @@ -26,21 +23,25 @@ export default function Settings(props) { * * The draft, just as the activeFling, is of type Fling */ - let [draft, setDraft] = useState({ fling: _clone }); + let [draft, setDraft] = useState(produce(activeFling, draft => draft)); let [shareUrlUnique, setShareUrlUnique] = useState(true); let [authCodeChangeable, setAuthCodeChangeable] = useState(false); + let [expirationType, setExpirationType] = useState(activeFling.expirationClicks + ? "clicks" + : activeFling.expirationTime ? "time" : "never"); /** * Publishes the draft to the backend and refreshes the redux store */ function publishDraft() { - flingClient.putFling(draft).then( - success => { - log.info("Saved new settings {}", draft); - dispatch(retrieveFlings()); - }, - error => log.error("Could not save new settings for {}: {}", activeFling.id, error)); + flingClient.putFling(activeFling.id, { fling: draft }) + .then( + success => { + log.info("Saved new settings {}", draft); + dispatch(retrieveFlings()); + }) + .catch(error => log.error(`Could not save new settings for ${activeFling.id}: `, error)); } /** @@ -48,7 +49,7 @@ export default function Settings(props) { * modifications get lost. */ function resetDraft() { - setDraft(produce({}, draft => { return { fling: activeFling }; })); + setDraft(produce({}, draft => activeFling)); } /** @@ -72,29 +73,29 @@ export default function Settings(props) { switch (setting) { case "direct-download": if (enabled) { - newDraft.fling.directDownload = true; - newDraft.fling.shared = true; - newDraft.fling.allowUpload = false; + newDraft.directDownload = true; + newDraft.shared = true; + newDraft.allowUpload = false; } else { - newDraft.fling.directDownload = false; + newDraft.directDownload = false; } - return newDraft.fling; + return newDraft; case "allow-upload": if (enabled) { - newDraft.fling.allowUpload = true; - newDraft.fling.shared = true; - newDraft.fling.directDownload = false; + newDraft.allowUpload = true; + newDraft.shared = true; + newDraft.directDownload = false; } else { - newDraft.fling.allowUpload = false; + newDraft.allowUpload = false; } - return newDraft.fling; + return newDraft; case "shared": - if (enabled) { - newDraft.fling.allowUpload = false; - newDraft.fling.directDownload = false; - newDraft.fling.shared = false; + if (!enabled) { + newDraft.allowUpload = false; + newDraft.directDownload = false; + newDraft.shared = false; } else { - newDraft.fling.shared = true; + newDraft.shared = true; } return newDraft; default: @@ -103,46 +104,78 @@ export default function Settings(props) { }; }) /** Sets the Fling.name. Creates a new draft and sets it into the local state. */ - let setName = produce((newDraft, name) => { newDraft.fling.name = name; return newDraft; }); - /** Sets the Fling.expirationTime. Creates a new draft and sets it into the local state. */ - let setExpirationTime = _pproduce((newDraft, time) => newDraft.fling.expirationTime = time); - /** Sets the Fling.expirationClicks. Creates a new draft and sets it into the local state. */ - let setExpirationClicks = _pproduce((newDraft, clicks) => newDraft.fling.clicks = clicks); - /** Sets the Fling.shareId. Creates a new draft and sets it into the local state. */ - let setShareId = _pproduce((newDraft, shareId) => newDraft.fling.shareId = shareId); - /** Sets the Fling.authCode. Creates a new draft and sets it into the local state. */ - let setAuthCode = _pproduce((newDraft, authCode) => newDraft.fling.authCode = authCode); + let setName = _pproduce((newDraft, name) => { newDraft.name = name }); + /** Sets the Fling.shareId. Creates a new draft and sets it into the local + * state. Sets `setShareUrlUnique`. */ + let setShareId = _pproduce((newDraft, shareId) => { + newDraft.shareId = shareId; - let resetAuthCode = _pproduce((newDraft) => newDraft.fling.authCode = activeFling.authCode); + flingClient.getFlingByShareId(shareId) + .then(result => shareId !== activeFling.shareId && setShareUrlUnique(false)) + .catch(error => error.status === 404 && setShareUrlUnique(true)); + }); + /** Sets the Fling.authCode. Creates a new draft and sets it into the local state. */ + let setAuthCode = _pproduce((newDraft, authCode) => { + setAuthCodeChangeable(true); + if (!authCode) return newDraft; + newDraft.authCode = authCode + }); + + let resetAuthCode = _pproduce((newDraft) => { + setAuthCodeChangeable(true); + newDraft.authCode = ""; + return newDraft; + }); + + let setExpiration = _pproduce((newDraft, type, value) => { + setExpirationType(type) + switch (type) { + case "clicks": + newDraft.expirationTime = ""; + newDraft.expirationClicks = value; + break; + case "time": + newDraft.expirationClicks = ""; + newDraft.expirationTime = value; + break; + case "never": + newDraft.expirationClicks = ""; + newDraft.expirationTime = ""; + break; + default: + log.error("Unknown expiration type"); + break; + } + }); + + + let resetExpiration = (draft, type) => { + setExpiration(draft, type, ""); + }; return (
-
+ { ev.preventDefault(); publishDraft(); }}>
{ - ev.preventDefault(); - let nd = produce(draft, newDraft => { - newDraft.fling.name = ev.target.value; - return newDraft; - }) - setDraft(nd); - }} /> + value={draft.name} + onChange={(ev) => setName(draft, ev.target.value)} />
- +
- setShareId(ev.target.value)} /> + setShareId(draft, ev.target.value)} />
@@ -153,13 +186,16 @@ export default function Settings(props) {
- setAuthCode(ev.target.value)} /> + setAuthCode(draft, ev.target.value)} /> + @@ -174,25 +210,27 @@ export default function Settings(props) {
- resetExpiration(draft, ev.currentTarget.value)}>
-
+
Expire after - + setExpiration(draft, "clicks", ev.target.value)} /> Clicks
-
+
Expire after - + setExpiration(draft, "time", ev.target.valueAsNumber)} />
@@ -205,15 +243,18 @@ export default function Settings(props) {
diff --git a/web/fling/src/redux/actions.js b/web/fling/src/redux/actions.js index efb9e76..740f5df 100644 --- a/web/fling/src/redux/actions.js +++ b/web/fling/src/redux/actions.js @@ -56,13 +56,6 @@ function setActiveFling(id) { } } -function _purify(o) { - if(Array.isArray(o)) { - return o.map(e => toPlainObject(cloneDeep(e))); - } - return toPlainObject(cloneDeep(o)); -} - function retrieveFlings() { return (dispatch, getState) => { const { flings: { activeFling } } = getState(); @@ -94,4 +87,20 @@ function deleteFling(id) { } } +/** + * Purify the object or array `o` to a plain, simple javascript object. + * + * This is used for two reasons on the store: + * - The state itself should be kept simple. It is a mere structure of simple + * values. Any logic acting upon the state must be defined on other layers. + * - Complex objects interact in hard to understand ways with libraries + * like react and immer. A simple object is more versatile and predictable. + */ +function _purify(o) { + if(Array.isArray(o)) { + return o.map(e => toPlainObject(cloneDeep(e))); + } + return toPlainObject(cloneDeep(o)); +} + export { retrieveFlings, setActiveFling, deleteFling }; diff --git a/web/fling/src/util/fc.js b/web/fling/src/util/fc.js index b6d6051..7109dc9 100644 --- a/web/fling/src/util/fc.js +++ b/web/fling/src/util/fc.js @@ -12,7 +12,7 @@ let clientConfig = (token) => { config.basePath = process.env.REACT_APP_API.replace(/\/+$/, ''); token = token || sessionStorage.getItem('token'); - if(token) { config.authentications['bearer'].accessToken = token; } + if (token) { config.authentications['bearer'].accessToken = token; } return config; }; @@ -29,4 +29,4 @@ function AuthClient(token) { return new fc.AuthApi(clientConfig(token)); } -export {FlingClient, ArtifactClient, AuthClient, fc}; +export { FlingClient, ArtifactClient, AuthClient, fc };