Implement auth code setting and reset of settings
AuthCode to protect a fling can now be set in settings. AuthCode was moved to the Fling table. Only one auth code per fling is possible now. AuthCode is stored hashed in the database. Additionally, implement reset of settings (Cancel button) by reloading the fling from the database.
This commit is contained in:
parent
4ca4fe5ffe
commit
7f4bc536b9
9 changed files with 71 additions and 90 deletions
|
@ -1,5 +1,8 @@
|
||||||
package net.friedl.fling;
|
package net.friedl.fling;
|
||||||
|
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@ -8,6 +11,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class FlingConfiguration {
|
public class FlingConfiguration {
|
||||||
|
@Bean
|
||||||
|
public MessageDigest keyHashDigest() throws NoSuchAlgorithmException {
|
||||||
|
return MessageDigest.getInstance("SHA-512");
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public ObjectMapper objectMapper() {
|
public ObjectMapper objectMapper() {
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
|
@ -14,7 +14,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import net.friedl.fling.model.dto.AuthCodeDto;
|
|
||||||
import net.friedl.fling.model.dto.FlingDto;
|
import net.friedl.fling.model.dto.FlingDto;
|
||||||
import net.friedl.fling.service.FlingService;
|
import net.friedl.fling.service.FlingService;
|
||||||
|
|
||||||
|
@ -63,9 +62,4 @@ public class FlingController {
|
||||||
public void deleteFling(@PathVariable Long flingId) {
|
public void deleteFling(@PathVariable Long flingId) {
|
||||||
flingService.deleteFlingById(flingId);
|
flingService.deleteFlingById(flingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/fling/{flingId}/protect")
|
|
||||||
public void protectFling(@PathVariable Long flingId, @RequestBody AuthCodeDto protectCode) {
|
|
||||||
flingService.protect(flingId, protectCode);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
package net.friedl.fling.model.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class AuthCodeDto {
|
|
||||||
String authCode;
|
|
||||||
}
|
|
|
@ -33,6 +33,8 @@ public class FlingDto {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private Instant expirationTime;
|
private Instant expirationTime;
|
||||||
|
|
||||||
|
private String authCode;
|
||||||
|
|
||||||
@JsonProperty("sharing")
|
@JsonProperty("sharing")
|
||||||
private void unpackSharing(Map<String, Object> sharing) {
|
private void unpackSharing(Map<String, Object> sharing) {
|
||||||
this.directDownload = (Boolean) sharing.getOrDefault("directDownload", false);
|
this.directDownload = (Boolean) sharing.getOrDefault("directDownload", false);
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
package net.friedl.fling.model.mapper;
|
|
||||||
|
|
||||||
import org.mapstruct.Mapper;
|
|
||||||
|
|
||||||
import net.friedl.fling.model.dto.AuthCodeDto;
|
|
||||||
import net.friedl.fling.persistence.entities.AuthCodeEntity;
|
|
||||||
|
|
||||||
@Mapper(componentModel = "spring")
|
|
||||||
public interface AuthCodeMapper {
|
|
||||||
AuthCodeEntity map(AuthCodeDto authCodeDto);
|
|
||||||
AuthCodeDto map(AuthCodeEntity authCodeEntity);
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
package net.friedl.fling.persistence.entities;
|
|
||||||
|
|
||||||
import javax.persistence.Column;
|
|
||||||
import javax.persistence.Entity;
|
|
||||||
import javax.persistence.GeneratedValue;
|
|
||||||
import javax.persistence.Id;
|
|
||||||
import javax.persistence.ManyToOne;
|
|
||||||
import javax.persistence.Table;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "AuthCode")
|
|
||||||
@Getter @Setter
|
|
||||||
public class AuthCodeEntity {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private String authCode;
|
|
||||||
|
|
||||||
@ManyToOne(optional = false)
|
|
||||||
private FlingEntity fling;
|
|
||||||
|
|
||||||
public void setFling(FlingEntity fling) {
|
|
||||||
this.fling = fling;
|
|
||||||
fling.getAuthCodes().add(this);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -45,11 +45,10 @@ public class FlingEntity {
|
||||||
@Column(unique = true, nullable = false)
|
@Column(unique = true, nullable = false)
|
||||||
private String shareUrl;
|
private String shareUrl;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "fling", cascade = CascadeType.ALL, orphanRemoval = true)
|
private String authCode;
|
||||||
private Set<ArtifactEntity> artifacts;
|
|
||||||
|
|
||||||
@OneToMany(mappedBy = "fling", cascade = CascadeType.ALL, orphanRemoval = true)
|
@OneToMany(mappedBy = "fling", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
private Set<AuthCodeEntity> authCodes;
|
private Set<ArtifactEntity> artifacts;
|
||||||
|
|
||||||
@PrePersist
|
@PrePersist
|
||||||
private void prePersist() {
|
private void prePersist() {
|
||||||
|
|
|
@ -1,24 +1,22 @@
|
||||||
package net.friedl.fling.service;
|
package net.friedl.fling.service;
|
||||||
|
|
||||||
|
import java.security.MessageDigest;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import javax.persistence.EntityManager;
|
|
||||||
import javax.persistence.PersistenceContext;
|
|
||||||
import javax.transaction.Transactional;
|
import javax.transaction.Transactional;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.crypto.codec.Hex;
|
||||||
import org.springframework.security.crypto.keygen.KeyGenerators;
|
import org.springframework.security.crypto.keygen.KeyGenerators;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import net.friedl.fling.model.dto.AuthCodeDto;
|
|
||||||
import net.friedl.fling.model.dto.FlingDto;
|
import net.friedl.fling.model.dto.FlingDto;
|
||||||
import net.friedl.fling.model.mapper.AuthCodeMapper;
|
|
||||||
import net.friedl.fling.model.mapper.FlingMapper;
|
import net.friedl.fling.model.mapper.FlingMapper;
|
||||||
import net.friedl.fling.persistence.entities.FlingEntity;
|
import net.friedl.fling.persistence.entities.FlingEntity;
|
||||||
import net.friedl.fling.persistence.repositories.FlingRepository;
|
import net.friedl.fling.persistence.repositories.FlingRepository;
|
||||||
|
@ -31,16 +29,13 @@ public class FlingService {
|
||||||
|
|
||||||
private FlingMapper flingMapper;
|
private FlingMapper flingMapper;
|
||||||
|
|
||||||
private AuthCodeMapper authCodeMapper;
|
private MessageDigest keyHashDigest;
|
||||||
|
|
||||||
@PersistenceContext
|
|
||||||
private EntityManager entityManager;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public FlingService(FlingRepository flingRepository, FlingMapper flingMapper, AuthCodeMapper authCodeMapper) {
|
public FlingService(FlingRepository flingRepository, FlingMapper flingMapper, MessageDigest keyHashDigest) {
|
||||||
this.flingRepository = flingRepository;
|
this.flingRepository = flingRepository;
|
||||||
this.flingMapper = flingMapper;
|
this.flingMapper = flingMapper;
|
||||||
this.authCodeMapper = authCodeMapper;
|
this.keyHashDigest = keyHashDigest;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FlingDto> findAll() {
|
public List<FlingDto> findAll() {
|
||||||
|
@ -70,6 +65,7 @@ public class FlingService {
|
||||||
mergeNonEmpty(flingDto::getName, flingEntity::setName);
|
mergeNonEmpty(flingDto::getName, flingEntity::setName);
|
||||||
mergeNonEmpty(flingDto::getShared, flingEntity::setShared);
|
mergeNonEmpty(flingDto::getShared, flingEntity::setShared);
|
||||||
mergeNonEmpty(flingDto::getShareUrl, flingEntity::setShareUrl);
|
mergeNonEmpty(flingDto::getShareUrl, flingEntity::setShareUrl);
|
||||||
|
mergeWithEmpty(() -> hashKey(flingDto.getAuthCode()), flingEntity::setAuthCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<FlingDto> findFlingById(Long flingId) {
|
public Optional<FlingDto> findFlingById(Long flingId) {
|
||||||
|
@ -85,15 +81,11 @@ public class FlingService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasAuthCode(Long flingId, String authCode) {
|
public boolean hasAuthCode(Long flingId, String authCode) {
|
||||||
return flingRepository.getOne(flingId).getAuthCodes()
|
|
||||||
.stream().anyMatch(ae -> ae.getAuthCode().equals(authCode));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void protect(Long flingId, AuthCodeDto authCodeDto) {
|
|
||||||
var fling = flingRepository.getOne(flingId);
|
var fling = flingRepository.getOne(flingId);
|
||||||
var authCode = authCodeMapper.map(authCodeDto);
|
|
||||||
|
|
||||||
authCode.setFling(fling);
|
if(!StringUtils.hasText(fling.getAuthCode())) return true;
|
||||||
|
|
||||||
|
return fling.getAuthCode().equals(hashKey(authCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getShareName(String shareUrl) {
|
public String getShareName(String shareUrl) {
|
||||||
|
@ -112,7 +104,7 @@ public class FlingService {
|
||||||
return flingRepository.countArtifactsById(flingId);
|
return flingRepository.countArtifactsById(flingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String generateShareUrl() {
|
public String generateShareUrl() {
|
||||||
var key = KeyGenerators
|
var key = KeyGenerators
|
||||||
.secureRandom(16)
|
.secureRandom(16)
|
||||||
.generateKey();
|
.generateKey();
|
||||||
|
@ -127,6 +119,12 @@ public class FlingService {
|
||||||
.replace('-', 'd');
|
.replace('-', 'd');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String hashKey(String key) {
|
||||||
|
if(!StringUtils.hasText(key)) return null;
|
||||||
|
|
||||||
|
return new String(Hex.encode(keyHashDigest.digest(key.getBytes())));
|
||||||
|
}
|
||||||
|
|
||||||
private <T> void mergeNonEmpty(Supplier<T> sup, Consumer<T> con) {
|
private <T> void mergeNonEmpty(Supplier<T> sup, Consumer<T> con) {
|
||||||
T r = sup.get();
|
T r = sup.get();
|
||||||
if(r != null) con.accept(r);
|
if(r != null) con.accept(r);
|
||||||
|
|
|
@ -1,28 +1,58 @@
|
||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
import React, {useState, useEffect, useRef} from 'react';
|
import React, {useState, useEffect, useRef} from 'react';
|
||||||
|
import {useLocation, useHistory} from 'react-router-dom';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import {flingClient} from '../../util/flingclient';
|
import {flingClient} from '../../util/flingclient';
|
||||||
|
|
||||||
export default function Settings(props) {
|
export default function Settings(props) {
|
||||||
let [fling, setFling] = useState({name: "", sharing: {directDownload: false, allowUpload: true, shared: true, shareUrl: "", authCode: ""},
|
let defaultState = () => ({name: "", authCode: "",
|
||||||
expiration: {type: "clicks", value: 0}});
|
sharing: {directDownload: false, allowUpload: true, shared: true, shareUrl: ""},
|
||||||
let [shareUrlUnique, setShareUrlUnique] = useState(true);
|
expiration: {}});
|
||||||
|
|
||||||
useEffect(() => {
|
let [fling, setFling] = useState(defaultState());
|
||||||
|
let [shareUrlUnique, setShareUrlUnique] = useState(true);
|
||||||
|
let [authCodeChangeable, setAuthCodeChangeable] = useState(false);
|
||||||
|
let location = useLocation();
|
||||||
|
let history = useHistory();
|
||||||
|
|
||||||
|
useEffect(() => loadSettings(), [props.activeFling]);
|
||||||
|
|
||||||
|
function loadSettings() {
|
||||||
if(props.activeFling) {
|
if(props.activeFling) {
|
||||||
flingClient.getFling(props.activeFling)
|
flingClient.getFling(props.activeFling)
|
||||||
.then(result => {
|
.then(result => {
|
||||||
let f = {...fling, ...result};
|
let f = {...fling, ...result};
|
||||||
let s = {...fling.sharing, ...result.sharing};
|
let s = {...fling.sharing, ...result.sharing};
|
||||||
let e = {...fling.expiration, ...result.expiration};
|
let e = {...fling.expiration, ...result.expiration};
|
||||||
|
|
||||||
f.sharing = s;
|
f.sharing = s;
|
||||||
f.expiration = e;
|
f.expiration = e;
|
||||||
setFling(f);
|
setFling(f);
|
||||||
|
|
||||||
|
setAuthCodeChangeable(!f.authCode);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [props.activeFling]);
|
}
|
||||||
|
|
||||||
|
function reloadSettings(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
setFling(defaultState());
|
||||||
|
loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetAuthCode(ev) {
|
||||||
|
if(fling.authCode) {
|
||||||
|
let f = {...fling};
|
||||||
|
f.authCode = "";
|
||||||
|
setFling(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!ev.currentTarget.checked) {
|
||||||
|
setAuthCodeChangeable(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleSharing(ev) {
|
function toggleSharing(ev) {
|
||||||
let f = {...fling};
|
let f = {...fling};
|
||||||
|
@ -133,11 +163,9 @@ export default function Settings(props) {
|
||||||
|
|
||||||
function setAuthCode(ev) {
|
function setAuthCode(ev) {
|
||||||
let f = {...fling};
|
let f = {...fling};
|
||||||
let s = {...fling.sharing};
|
|
||||||
let value = ev.currentTarget.value;
|
let value = ev.currentTarget.value;
|
||||||
|
|
||||||
s.authCode = value;
|
f.authCode = value;
|
||||||
f.sharing = s;
|
|
||||||
setFling(f);
|
setFling(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,13 +204,13 @@ 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" type="text" value={fling.sharing.authCode} onChange={setAuthCode} />
|
<input className={`form-input ${authCodeChangeable ? "d-visible":"d-hide"}`} type="text" readOnly={!authCodeChangeable} value={fling.authCode} onChange={setAuthCode} />
|
||||||
<label className="form-switch ml-2 popover popover-bottom">
|
<label className="form-switch ml-2 popover popover-bottom">
|
||||||
<input type="checkbox" checked={!!fling.sharing.authCode} readOnly />
|
<input type="checkbox" checked={!!fling.authCode} onChange={resetAuthCode} />
|
||||||
<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">
|
||||||
{!!fling.sharing.authCode ? "Delete the passcode to disable protection": "Set a passcode to enable protection"}
|
{fling.authCode ? "Click to reset passcode": "Set passcode to enable protection"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
@ -242,7 +270,10 @@ export default function Settings(props) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="submit" className="btn btn-primary float-right" value="Save" />
|
<div className="float-right">
|
||||||
|
<button className="btn btn-secondary mr-2" onClick={reloadSettings}>Cancel</button>
|
||||||
|
<input type="submit" className="btn btn-primary" value="Save" />
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue