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:
Armin Friedl 2020-05-22 21:44:28 +02:00
parent 4ca4fe5ffe
commit 7f4bc536b9
Signed by: armin
GPG key ID: 48C726EEE7FBCBC8
9 changed files with 71 additions and 90 deletions

View file

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

View file

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

View file

@ -1,8 +0,0 @@
package net.friedl.fling.model.dto;
import lombok.Data;
@Data
public class AuthCodeDto {
String authCode;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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