This commit is contained in:
parent
232518bee4
commit
5598cb5ecf
8 changed files with 137 additions and 93 deletions
|
@ -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())
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
8
web/fling/package-lock.json
generated
8
web/fling/package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 };
|
||||
|
|
Loading…
Reference in a new issue