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")
.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())

View file

@ -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);
}
}

View file

@ -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",

View file

@ -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",

View file

@ -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) {

View file

@ -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(
flingClient.putFling(activeFling.id, { fling: draft })
.then(
success => {
log.info("Saved new settings {}", draft);
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.
*/
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 (
<div className="container">
<div className="columns">
<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="col-3 col-sm-12">
<label className="form-label" htmlFor="input-name">Name</label>
</div>
<div className="col-9 col-sm-12">
<input className="form-input" type="text" id="input-name"
value={draft.fling.name}
onChange={(ev) => {
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)} />
</div>
</div>
<div className="form-group">
<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 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"}`} />
</div>
</div>
@ -153,13 +186,16 @@ export default function Settings(props) {
</div>
<div className="col-9 col-sm-12">
<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">
<input type="checkbox" checked={!!draft.fling.authCode} onChange={resetAuthCode} />
<input type="checkbox" checked={!!draft.authCode} onChange={ev => resetAuthCode(draft)} />
<i className="form-icon" /> Protected
<div className="popover-container card">
<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>
</label>
@ -174,25 +210,27 @@ export default function Settings(props) {
</div>
<div className="col-9 col-sm-12">
<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="time">Date</option>
<option value="clicks">Clicks</option>
</select>
</div>
<div className={"clicks" === "clicks" ? "d-visible" : "d-hide"}>
<div className={expirationType === "clicks" ? "d-visible" : "d-hide"}>
<div className="input-group">
<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>
</div>
</div>
<div className={"clicks" === "time" ? "d-visible" : "d-hide"}>
<div className={expirationType === "time" ? "d-visible" : "d-hide"}>
<div className="input-group">
<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>
@ -205,15 +243,18 @@ export default function Settings(props) {
<div className="col-9 col-sm-12">
<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
</label>
<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
</label>
<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
</label>
</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() {
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 };