Finish fling settings
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Armin Friedl 2020-07-25 21:43:56 +02:00
parent 232518bee4
commit 5598cb5ecf
Signed by: armin
GPG key ID: 48C726EEE7FBCBC8
8 changed files with 137 additions and 93 deletions

View file

@ -99,11 +99,15 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
.antMatchers(HttpMethod.POST, "/api/fling/{flingId}/artifact") .antMatchers(HttpMethod.POST, "/api/fling/{flingId}/artifact")
.access("@authorizationService.allowUpload(#flingId, authentication)") .access("@authorizationService.allowUpload(#flingId, authentication)")
.and() .and()
// only admin can create, delete and list flings // only admin can create, delete, list and modify flings
.authorizeRequests() .authorizeRequests()
.antMatchers(HttpMethod.DELETE, "/api/fling/{flingId}") .antMatchers(HttpMethod.DELETE, "/api/fling/{flingId}")
.hasAnyAuthority(FLING_ADMIN.getAuthority()) .hasAnyAuthority(FLING_ADMIN.getAuthority())
.and() .and()
.authorizeRequests()
.antMatchers(HttpMethod.PUT, "/api/fling/{flingId}")
.hasAnyAuthority(FLING_ADMIN.getAuthority())
.and()
.authorizeRequests() .authorizeRequests()
.antMatchers(HttpMethod.POST, "/api/fling") .antMatchers(HttpMethod.POST, "/api/fling")
.hasAuthority(FLING_ADMIN.getAuthority()) .hasAuthority(FLING_ADMIN.getAuthority())

View file

@ -121,6 +121,7 @@ public class FlingService {
} }
private String hashAuthCode(String authCode) { private String hashAuthCode(String authCode) {
if (!StringUtils.hasText(authCode)) return null;
String hash = passwordEncoder.encode(authCode); String hash = passwordEncoder.encode(authCode);
log.debug("Hashed authentication code to {}", hash); log.debug("Hashed authentication code to {}", hash);
return hash; return hash;
@ -154,17 +155,14 @@ public class FlingService {
flingEntity.setId(id); flingEntity.setId(id);
flingEntity.setAllowUpload(flingDto.getAllowUpload()); flingEntity.setAllowUpload(flingDto.getAllowUpload());
flingEntity.setDirectDownload(flingDto.getDirectDownload()); flingEntity.setDirectDownload(flingDto.getDirectDownload());
flingEntity.setShared(flingDto.getShared());
flingEntity.setExpirationClicks(flingDto.getExpirationClicks()); flingEntity.setExpirationClicks(flingDto.getExpirationClicks());
flingEntity.setExpirationTime(flingDto.getExpirationTime()); flingEntity.setExpirationTime(flingDto.getExpirationTime());
flingEntity.setName(flingDto.getName()); flingEntity.setName(flingDto.getName());
flingEntity.setShareId(flingDto.getShareId()); flingEntity.setShareId(flingDto.getShareId());
if (!flingDto.getAuthCode().equals(flingEntity.getAuthCode())) {
if(!flingEntity.getAuthCode().equals(flingDto.getAuthCode())
&& !flingEntity.getAuthCode().equals(hashAuthCode(flingDto.getAuthCode()))) {
flingEntity.setAuthCode(hashAuthCode(flingDto.getAuthCode())); flingEntity.setAuthCode(hashAuthCode(flingDto.getAuthCode()));
} }
return flingMapper.map(flingEntity); return flingMapper.map(flingEntity);
} }
} }

View file

@ -1129,7 +1129,7 @@
"@fling/flingclient": { "@fling/flingclient": {
"version": "0.1.0-snapshot", "version": "0.1.0-snapshot",
"resolved": "https://nexus.friedl.net/repository/npm-private/@fling/flingclient/-/flingclient-0.1.0-snapshot.tgz", "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": { "requires": {
"@babel/cli": "^7.0.0", "@babel/cli": "^7.0.0",
"superagent": "3.7.0" "superagent": "3.7.0"
@ -6618,9 +6618,9 @@
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg=="
}, },
"immer": { "immer": {
"version": "7.0.5", "version": "7.0.7",
"resolved": "https://registry.npmjs.org/immer/-/immer-7.0.5.tgz", "resolved": "https://registry.npmjs.org/immer/-/immer-7.0.7.tgz",
"integrity": "sha512-TtRAKZyuqld2eYjvWgXISLJ0ZlOl1OOTzRmrmiY8SlB0dnAhZ1OiykIDL5KDFNaPHDXiLfGQFNJGtet8z8AEmg==" "integrity": "sha512-Q8yYwVADJXrNfp1ZUAh4XDHkcoE3wpdpb4mC5abDSajs2EbW8+cGdPyAnglMyLnm7EF6ojD2xBFX7L5i4TIytw=="
}, },
"import-cwd": { "import-cwd": {
"version": "2.1.0", "version": "2.1.0",

View file

@ -10,7 +10,7 @@
"axios": "^0.19.2", "axios": "^0.19.2",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"immer": "^7.0.5", "immer": "^7.0.7",
"jwt-decode": "^2.2.0", "jwt-decode": "^2.2.0",
"loglevel": "^1.6.8", "loglevel": "^1.6.8",
"node-sass": "^4.14.1", "node-sass": "^4.14.1",

View file

@ -66,17 +66,9 @@ export default function New(props) {
const flingClient = new FlingClient(); const flingClient = new FlingClient();
flingClient.getFlingByShareId(ev.currentTarget.value) flingClient.getFlingByShareId(ev.currentTarget.value)
.then(result => { .then(result => setShareUrlUnique(false))
setShareUrlUnique(false); .catch(error => error.status === 404 && setShareUrlUnique(true) )
}).catch(error => { .finally(() => { s.shareUrl = value; f.sharing = s; setFling(f); });
if(error.status === 404) {
setShareUrlUnique(true);
}
}).finally(() => {
s.shareUrl = value;
f.sharing = s;
setFling(f);
});
} }
function setName(ev) { function setName(ev) {
@ -157,7 +149,7 @@ export default function New(props) {
} }
} }
flingClient.postFling({fling: flingEntity}) flingClient.postFling({ fling: flingEntity })
.then(() => handleClose()) .then(() => handleClose())
.catch(error => log.error(error)) .catch(error => log.error(error))
} }

View file

@ -1,10 +1,8 @@
import log from 'loglevel'; import log from 'loglevel';
import React, { useState } from 'react'; import React, { useState } from 'react';
import produce from 'immer'; import produce from 'immer';
import _ from 'lodash';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { Fling } from '@fling/flingclient';
import { retrieveFlings } from "../../redux/actions"; import { retrieveFlings } from "../../redux/actions";
import { FlingClient } from '../../util/fc'; 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. * The active fling from the redux store. Treat this as immutable.
*/ */
let activeFling = useSelector(state => state.flings.activeFling); 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 * 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 * 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 [shareUrlUnique, setShareUrlUnique] = useState(true);
let [authCodeChangeable, setAuthCodeChangeable] = useState(false); 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 * Publishes the draft to the backend and refreshes the redux store
*/ */
function publishDraft() { function publishDraft() {
flingClient.putFling(draft).then( flingClient.putFling(activeFling.id, { fling: draft })
.then(
success => { success => {
log.info("Saved new settings {}", draft); log.info("Saved new settings {}", draft);
dispatch(retrieveFlings()); dispatch(retrieveFlings());
}, })
error => log.error("Could not save new settings for {}: {}", activeFling.id, error)); .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. * modifications get lost.
*/ */
function resetDraft() { function resetDraft() {
setDraft(produce({}, draft => { return { fling: activeFling }; })); setDraft(produce({}, draft => activeFling));
} }
/** /**
@ -72,29 +73,29 @@ export default function Settings(props) {
switch (setting) { switch (setting) {
case "direct-download": case "direct-download":
if (enabled) { if (enabled) {
newDraft.fling.directDownload = true; newDraft.directDownload = true;
newDraft.fling.shared = true; newDraft.shared = true;
newDraft.fling.allowUpload = false; newDraft.allowUpload = false;
} else { } else {
newDraft.fling.directDownload = false; newDraft.directDownload = false;
} }
return newDraft.fling; return newDraft;
case "allow-upload": case "allow-upload":
if (enabled) { if (enabled) {
newDraft.fling.allowUpload = true; newDraft.allowUpload = true;
newDraft.fling.shared = true; newDraft.shared = true;
newDraft.fling.directDownload = false; newDraft.directDownload = false;
} else { } else {
newDraft.fling.allowUpload = false; newDraft.allowUpload = false;
} }
return newDraft.fling; return newDraft;
case "shared": case "shared":
if (enabled) { if (!enabled) {
newDraft.fling.allowUpload = false; newDraft.allowUpload = false;
newDraft.fling.directDownload = false; newDraft.directDownload = false;
newDraft.fling.shared = false; newDraft.shared = false;
} else { } else {
newDraft.fling.shared = true; newDraft.shared = true;
} }
return newDraft; return newDraft;
default: 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. */ /** 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; }); let setName = _pproduce((newDraft, name) => { newDraft.name = name });
/** Sets the Fling.expirationTime. Creates a new draft and sets it into the local state. */ /** Sets the Fling.shareId. Creates a new draft and sets it into the local
let setExpirationTime = _pproduce((newDraft, time) => newDraft.fling.expirationTime = time); * state. Sets `setShareUrlUnique`. */
/** Sets the Fling.expirationClicks. Creates a new draft and sets it into the local state. */ let setShareId = _pproduce((newDraft, shareId) => {
let setExpirationClicks = _pproduce((newDraft, clicks) => newDraft.fling.clicks = clicks); newDraft.shareId = shareId;
/** 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 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 ( return (
<div className="container"> <div className="container">
<div className="columns"> <div className="columns">
<div className="p-centered column col-xl-9 col-sm-12 col-6"> <div className="p-centered column col-xl-9 col-sm-12 col-6">
<form className="form-horizontal" onSubmit={publishDraft}> <form className="form-horizontal" onSubmit={(ev) => { ev.preventDefault(); publishDraft(); }}>
<div className="form-group"> <div className="form-group">
<div className="col-3 col-sm-12"> <div className="col-3 col-sm-12">
<label className="form-label" htmlFor="input-name">Name</label> <label className="form-label" htmlFor="input-name">Name</label>
</div> </div>
<div className="col-9 col-sm-12"> <div className="col-9 col-sm-12">
<input className="form-input" type="text" id="input-name" <input className="form-input" type="text" id="input-name"
value={draft.fling.name} value={draft.name}
onChange={(ev) => { onChange={(ev) => setName(draft, ev.target.value)} />
ev.preventDefault();
let nd = produce(draft, newDraft => {
newDraft.fling.name = ev.target.value;
return newDraft;
})
setDraft(nd);
}} />
</div> </div>
</div> </div>
<div className="form-group"> <div className="form-group">
<div className="col-3 col-sm-12"> <div className="col-3 col-sm-12">
<label className="form-label" htmlFor="input-share-url">Share URL</label> <label className="form-label" htmlFor="input-share-url">Share Id</label>
</div> </div>
<div className="col-9 col-sm-12"> <div className="col-9 col-sm-12">
<input className="form-input" type="text" id="input-share-url" onChange={ev => setShareId(ev.target.value)} /> <input className="form-input" type="text" id="input-share-url"
value={draft.shareId}
onChange={ev => setShareId(draft, ev.target.value)} />
<i className={`icon icon-cross text-error ${shareUrlUnique ? "d-hide" : "d-visible"}`} /> <i className={`icon icon-cross text-error ${shareUrlUnique ? "d-hide" : "d-visible"}`} />
</div> </div>
</div> </div>
@ -153,13 +186,16 @@ export default function Settings(props) {
</div> </div>
<div className="col-9 col-sm-12"> <div className="col-9 col-sm-12">
<div className="input-group"> <div className="input-group">
<input className={`form-input ${authCodeChangeable ? "d-visible" : "d-hide"}`} type="text" readOnly={!authCodeChangeable} onChange={ev => setAuthCode(ev.target.value)} /> <input className={`form-input ${!draft.authCode || authCodeChangeable ? "d-visible" : "d-hide"}`} type="text"
value={draft.authCode}
onChange={ev => setAuthCode(draft, ev.target.value)} />
<label className="form-switch ml-2 popover popover-bottom"> <label className="form-switch ml-2 popover popover-bottom">
<input type="checkbox" checked={!!draft.fling.authCode} onChange={resetAuthCode} /> <input type="checkbox" checked={!!draft.authCode} onChange={ev => resetAuthCode(draft)} />
<i className="form-icon" /> Protected <i className="form-icon" /> Protected
<div className="popover-container card"> <div className="popover-container card">
<div className="card-body"> <div className="card-body">
{draft.fling.authCode ? "Click to reset passcode" : "Set passcode to enable protection"} {draft.authCode ? "Click to reset passcode" : "Set passcode to enable protection"}
</div> </div>
</div> </div>
</label> </label>
@ -174,25 +210,27 @@ export default function Settings(props) {
</div> </div>
<div className="col-9 col-sm-12"> <div className="col-9 col-sm-12">
<div className="form-group"> <div className="form-group">
<select className="form-select" > <select className="form-select" value={expirationType} onChange={ev => resetExpiration(draft, ev.currentTarget.value)}>
<option value="never">Never</option> <option value="never">Never</option>
<option value="time">Date</option> <option value="time">Date</option>
<option value="clicks">Clicks</option> <option value="clicks">Clicks</option>
</select> </select>
</div> </div>
<div className={"clicks" === "clicks" ? "d-visible" : "d-hide"}> <div className={expirationType === "clicks" ? "d-visible" : "d-hide"}>
<div className="input-group"> <div className="input-group">
<span className="input-group-addon">Expire after</span> <span className="input-group-addon">Expire after</span>
<input className="form-input" type="number" /> <input className="form-input" type="number" value={draft.expirationClicks} onChange={ev => setExpiration(draft, "clicks", ev.target.value)} />
<span className="input-group-addon">Clicks</span> <span className="input-group-addon">Clicks</span>
</div> </div>
</div> </div>
<div className={"clicks" === "time" ? "d-visible" : "d-hide"}> <div className={expirationType === "time" ? "d-visible" : "d-hide"}>
<div className="input-group"> <div className="input-group">
<span className="input-group-addon">Expire after</span> <span className="input-group-addon">Expire after</span>
<input className="form-input" type="date" /> <input className="form-input" type="date"
value={draft.expirationTime ? (new Date(draft.expirationTime)).toISOString().split('T')[0]: ""}
onChange={ev => setExpiration(draft, "time", ev.target.valueAsNumber)} />
</div> </div>
</div> </div>
</div> </div>
@ -205,15 +243,18 @@ export default function Settings(props) {
<div className="col-9 col-sm-12"> <div className="col-9 col-sm-12">
<label className="form-switch form-inline"> <label className="form-switch form-inline">
<input type="checkbox" id="shared" checked={draft.fling.shared} onChange={toggleSharing} /> <input type="checkbox" id="shared" checked={draft.shared}
onChange={ev => toggleSharing(draft, ev.target.id, ev.target.checked)} />
<i className="form-icon" /> Shared <i className="form-icon" /> Shared
</label> </label>
<label className="form-switch form-inline"> <label className="form-switch form-inline">
<input type="checkbox" id="allow-upload" checked={draft.fling.allowUpload} onChange={toggleSharing} /> <input type="checkbox" id="allow-upload" checked={draft.allowUpload}
onChange={ev => toggleSharing(draft, ev.target.id, ev.target.checked)} />
<i className="form-icon" /> Uploads <i className="form-icon" /> Uploads
</label> </label>
<label className="form-switch form-inline"> <label className="form-switch form-inline">
<input type="checkbox" id="direct-download" checked={draft.fling.directDownload} onChange={toggleSharing} /> <input type="checkbox" id="direct-download" checked={draft.directDownload}
onChange={ev => toggleSharing(draft, ev.target.id, ev.target.checked)} />
<i className="form-icon" /> Direct Download <i className="form-icon" /> Direct Download
</label> </label>
</div> </div>

View file

@ -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() { function retrieveFlings() {
return (dispatch, getState) => { return (dispatch, getState) => {
const { flings: { activeFling } } = 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 }; export { retrieveFlings, setActiveFling, deleteFling };

View file

@ -12,7 +12,7 @@ let clientConfig = (token) => {
config.basePath = process.env.REACT_APP_API.replace(/\/+$/, ''); config.basePath = process.env.REACT_APP_API.replace(/\/+$/, '');
token = token || sessionStorage.getItem('token'); token = token || sessionStorage.getItem('token');
if(token) { config.authentications['bearer'].accessToken = token; } if (token) { config.authentications['bearer'].accessToken = token; }
return config; return config;
}; };
@ -29,4 +29,4 @@ function AuthClient(token) {
return new fc.AuthApi(clientConfig(token)); return new fc.AuthApi(clientConfig(token));
} }
export {FlingClient, ArtifactClient, AuthClient, fc}; export { FlingClient, ArtifactClient, AuthClient, fc };