Add import/export functionality for apps, including client-side file handling and backend encryption/decryption logic
This commit is contained in:
parent
1788a7ef1c
commit
6add528c02
7 changed files with 287 additions and 14 deletions
|
|
@ -1,10 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="app-list-item">
|
<div class="app-list-item">
|
||||||
<div class="app-name-container"><div class="app-name-name">{{ app.name }}</div><div class="app-name-id">{{ app.client_id}}</div></div>
|
<div class="app-name-container">
|
||||||
|
<div class="app-name-name">{{ app.name }}</div>
|
||||||
|
<div class="app-name-id">{{ app.client_id }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="badge-list">
|
||||||
|
<basic-badge variant="secondary" icon="lock" v-for="group in groups" :key="group">{{ group }}</basic-badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-container">
|
||||||
|
<icon-button icon="download" @click="exportClick"></icon-button>
|
||||||
|
<icon-button icon="trash" @click="deleteClick"></icon-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="badge-list"> <basic-badge variant="secondary" icon="lock" v-for="group in groups" :key="group">{{group}}</basic-badge></div>
|
|
||||||
<div class="action-container"> <icon-button icon="trash" @click="deleteClick"></icon-button></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -18,7 +28,7 @@ import BasicBadge from "@/components/UI/BasicBadge.vue";
|
||||||
export default {
|
export default {
|
||||||
name: "AppListItem",
|
name: "AppListItem",
|
||||||
components: {BasicBadge, IconButton, Box},
|
components: {BasicBadge, IconButton, Box},
|
||||||
emits: ["deleteApp"],
|
emits: ["deleteApp", "exportApp"],
|
||||||
props: {
|
props: {
|
||||||
app: {
|
app: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|
@ -33,6 +43,9 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
deleteClick() {
|
deleteClick() {
|
||||||
this.$emit("deleteApp", this.app.id);
|
this.$emit("deleteApp", this.app.id);
|
||||||
|
},
|
||||||
|
exportClick() {
|
||||||
|
this.$emit("exportApp", this.app.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -76,7 +89,10 @@ export default {
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-container{
|
.action-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1.2rem;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="apps-container">
|
<div class="apps-container">
|
||||||
|
<div class="app-list-actions">
|
||||||
|
|
||||||
|
</div>
|
||||||
<div class="app-list-header">
|
<div class="app-list-header">
|
||||||
<div>App</div>
|
<div>App</div>
|
||||||
<div>Groups</div>
|
<div>Groups</div>
|
||||||
|
|
@ -8,13 +10,20 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="app-list">
|
<div class="app-list">
|
||||||
|
|
||||||
<app-list-item v-for="app in apps" :app="app" @delete-app="deleteApp"></app-list-item>
|
<app-list-item v-for="app in apps" :app="app" @delete-app="deleteApp" @export-app="exportApp"></app-list-item>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modal :state="modalState">
|
<modal :state="modalState">
|
||||||
<add-app @close="closeModal"></add-app>
|
<add-app @close="closeModal"></add-app>
|
||||||
</modal>
|
</modal>
|
||||||
<basic-button icon="Plus" @click="modalState = true">New App</basic-button>
|
|
||||||
|
|
||||||
|
<div class="app-list-actions">
|
||||||
|
|
||||||
|
<basic-button icon="Upload" @click="importApp">Import</basic-button>
|
||||||
|
<basic-button icon="Plus" @click="modalState = true">New App</basic-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -27,6 +36,8 @@ import {mapStores} from "pinia";
|
||||||
import {useAppsStore} from "@/store/apps.js";
|
import {useAppsStore} from "@/store/apps.js";
|
||||||
import Modal from "@/components/UI/Modal.vue";
|
import Modal from "@/components/UI/Modal.vue";
|
||||||
import AddApp from "@/components/layout/config/AddApp.vue";
|
import AddApp from "@/components/layout/config/AddApp.vue";
|
||||||
|
import Dropdown from "@/components/UI/Dropdown.vue";
|
||||||
|
import IconButton from "@/components/UI/IconButton.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Apps",
|
name: "Apps",
|
||||||
|
|
@ -36,7 +47,7 @@ export default {
|
||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {AddApp, Modal, AppListItem, BasicButton},
|
components: {IconButton, Dropdown, AddApp, Modal, AppListItem, BasicButton},
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(useAppsStore),
|
...mapStores(useAppsStore),
|
||||||
apps() {
|
apps() {
|
||||||
|
|
@ -45,7 +56,8 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
modalState: false
|
modalState: false,
|
||||||
|
exportedApp: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -57,6 +69,62 @@ export default {
|
||||||
},
|
},
|
||||||
deleteApp(id) {
|
deleteApp(id) {
|
||||||
this.appsStore.deleteApp(id);
|
this.appsStore.deleteApp(id);
|
||||||
|
},
|
||||||
|
async exportApp(id) {
|
||||||
|
const response = await this.appsStore.exportApp(id);
|
||||||
|
const app = this.appsStore.getById(id);
|
||||||
|
|
||||||
|
if(response?.data) {
|
||||||
|
const base64String = response.data;
|
||||||
|
|
||||||
|
const blob = new Blob([base64String], { type: 'text/plain' });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `${app.name}.app`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
async importApp() {
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = '.app';
|
||||||
|
|
||||||
|
input.onchange = async (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
try {
|
||||||
|
const fileContent = await this.readFileContent(file);
|
||||||
|
await this.appsStore.importApp(fileContent);
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// File Dialog öffnen
|
||||||
|
input.click();
|
||||||
|
},
|
||||||
|
|
||||||
|
readFileContent(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = (e) => {
|
||||||
|
resolve(e.target.result);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onerror = (error) => {
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsText(file);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
|
|
@ -85,6 +153,13 @@ export default {
|
||||||
border-bottom: 0.1rem solid #E3EDFF;
|
border-bottom: 0.1rem solid #E3EDFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-list-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 2rem;
|
||||||
|
gap: 1.6rem
|
||||||
|
}
|
||||||
|
|
||||||
.app-list {
|
.app-list {
|
||||||
margin-bottom: 2.4rem;
|
margin-bottom: 2.4rem;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,18 @@ export const useAppsStore = defineStore('apps', {
|
||||||
this.apps = resp.data;
|
this.apps = resp.data;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
},
|
},
|
||||||
|
async exportApp(id) {
|
||||||
|
const url = `${config.backendUrl}/apps/export/${id}`;
|
||||||
|
const resp = await performRequest(this, 'GET', url, null);
|
||||||
|
return resp.data;
|
||||||
|
},
|
||||||
|
async importApp(app) {
|
||||||
|
const url = `${config.backendUrl}/apps/import`;
|
||||||
|
const resp = await performRequest(this, 'POST', url, { data: app },true);
|
||||||
|
|
||||||
|
if(resp.data)
|
||||||
|
await this.loadApps();
|
||||||
|
},
|
||||||
async addApp(appName, appGroups) {
|
async addApp(appName, appGroups) {
|
||||||
const url = `${config.backendUrl}/apps`;
|
const url = `${config.backendUrl}/apps`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package de.avatic.lcc.controller.configuration;
|
||||||
|
|
||||||
import com.azure.core.annotation.BodyParam;
|
import com.azure.core.annotation.BodyParam;
|
||||||
import de.avatic.lcc.dto.configuration.apps.AppDTO;
|
import de.avatic.lcc.dto.configuration.apps.AppDTO;
|
||||||
|
import de.avatic.lcc.dto.configuration.apps.AppExchangeDTO;
|
||||||
import de.avatic.lcc.service.apps.AppsService;
|
import de.avatic.lcc.service.apps.AppsService;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
|
@ -32,6 +33,18 @@ public class AppsController {
|
||||||
return ResponseEntity.ok(appsService.updateApp(dto));
|
return ResponseEntity.ok(appsService.updateApp(dto));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping({"/export/{id}", "/export/{id}/"})
|
||||||
|
@PreAuthorize("hasRole('SERVICE')")
|
||||||
|
public ResponseEntity<AppExchangeDTO> exportApp(@PathVariable Integer id) {
|
||||||
|
return ResponseEntity.ok(appsService.exportApp(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping({"/import/", "/import"})
|
||||||
|
@PreAuthorize("hasRole('SERVICE')")
|
||||||
|
public ResponseEntity<Boolean> importApp(@RequestBody AppExchangeDTO dto) {
|
||||||
|
return ResponseEntity.ok(appsService.importApp(dto));
|
||||||
|
}
|
||||||
|
|
||||||
@DeleteMapping({"/{id}", "/{id}/"})
|
@DeleteMapping({"/{id}", "/{id}/"})
|
||||||
@PreAuthorize("hasRole('SERVICE')")
|
@PreAuthorize("hasRole('SERVICE')")
|
||||||
public ResponseEntity<Void> deleteApp(@PathVariable Integer id) {
|
public ResponseEntity<Void> deleteApp(@PathVariable Integer id) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,14 @@
|
||||||
package de.avatic.lcc.dto.configuration.apps;
|
package de.avatic.lcc.dto.configuration.apps;
|
||||||
|
|
||||||
public class AppExchangeDTO {
|
public class AppExchangeDTO {
|
||||||
|
|
||||||
|
private String data;
|
||||||
|
|
||||||
|
public void setData(String data) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getData() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,23 @@
|
||||||
package de.avatic.lcc.service.apps;
|
package de.avatic.lcc.service.apps;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import de.avatic.lcc.dto.configuration.apps.AppDTO;
|
import de.avatic.lcc.dto.configuration.apps.AppDTO;
|
||||||
|
import de.avatic.lcc.dto.configuration.apps.AppExchangeDTO;
|
||||||
import de.avatic.lcc.model.db.users.App;
|
import de.avatic.lcc.model.db.users.App;
|
||||||
import de.avatic.lcc.repositories.users.AppRepository;
|
import de.avatic.lcc.repositories.users.AppRepository;
|
||||||
import de.avatic.lcc.service.transformer.apps.AppTransformer;
|
import de.avatic.lcc.service.transformer.apps.AppTransformer;
|
||||||
|
import de.avatic.lcc.util.exception.base.InternalErrorException;
|
||||||
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.security.SecureRandom;
|
import javax.crypto.*;
|
||||||
|
import javax.crypto.spec.GCMParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.*;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
@ -16,14 +26,31 @@ import java.util.UUID;
|
||||||
@Service
|
@Service
|
||||||
public class AppsService {
|
public class AppsService {
|
||||||
|
|
||||||
|
|
||||||
|
private static final String HMAC_ALGORITHM = "HmacSHA256";
|
||||||
|
private static final String ENCRYPTION_ALGORITHM = "AES/GCM/NoPadding";
|
||||||
|
private static final int GCM_TAG_LENGTH = 128;
|
||||||
|
private static final int GCM_IV_LENGTH = 12;
|
||||||
private final AppRepository appRepository;
|
private final AppRepository appRepository;
|
||||||
private final AppTransformer appTransformer;
|
private final AppTransformer appTransformer;
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final Key signingKey;
|
||||||
|
private final SecretKeySpec encryptionKey;
|
||||||
|
|
||||||
public AppsService(AppRepository appRepository, AppTransformer appTransformer, PasswordEncoder passwordEncoder) {
|
|
||||||
|
public AppsService(@Value("${jwt.secret}") String secret, AppRepository appRepository, AppTransformer appTransformer, PasswordEncoder passwordEncoder, ObjectMapper objectMapper) {
|
||||||
this.appRepository = appRepository;
|
this.appRepository = appRepository;
|
||||||
this.appTransformer = appTransformer;
|
this.appTransformer = appTransformer;
|
||||||
this.passwordEncoder = passwordEncoder;
|
this.passwordEncoder = passwordEncoder;
|
||||||
|
this.signingKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
|
||||||
|
// AES-256 Key aus JWT Secret ableiten
|
||||||
|
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] aesKey = new byte[32]; // 256 bit
|
||||||
|
System.arraycopy(keyBytes, 0, aesKey, 0, Math.min(keyBytes.length, 32));
|
||||||
|
this.encryptionKey = new SecretKeySpec(aesKey, "AES");
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<AppDTO> listApps() {
|
public List<AppDTO> listApps() {
|
||||||
|
|
@ -35,7 +62,7 @@ public class AppsService {
|
||||||
var newApp = dto.getId() == null;
|
var newApp = dto.getId() == null;
|
||||||
String appSecret = null;
|
String appSecret = null;
|
||||||
|
|
||||||
if(newApp) {
|
if (newApp) {
|
||||||
dto.setClientId(generateAppId());
|
dto.setClientId(generateAppId());
|
||||||
appSecret = generateAppSecret();
|
appSecret = generateAppSecret();
|
||||||
dto.setClientSecret(passwordEncoder.encode(appSecret));
|
dto.setClientSecret(passwordEncoder.encode(appSecret));
|
||||||
|
|
@ -43,7 +70,7 @@ public class AppsService {
|
||||||
|
|
||||||
var id = appRepository.update(appTransformer.toAppEntity(dto));
|
var id = appRepository.update(appTransformer.toAppEntity(dto));
|
||||||
|
|
||||||
if(newApp) {
|
if (newApp) {
|
||||||
dto.setId(id);
|
dto.setId(id);
|
||||||
dto.setClientSecret(appSecret);
|
dto.setClientSecret(appSecret);
|
||||||
}
|
}
|
||||||
|
|
@ -79,4 +106,103 @@ public class AppsService {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AppExchangeDTO exportApp(Integer id) {
|
||||||
|
var app = appRepository.getById(id).map(appTransformer::toAppDTOWithHashedSecret);
|
||||||
|
|
||||||
|
if (app.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("App mit ID " + id + " nicht gefunden");
|
||||||
|
}
|
||||||
|
AppExchangeDTO exchangeDTO = new AppExchangeDTO();
|
||||||
|
try {
|
||||||
|
|
||||||
|
String json = objectMapper.writeValueAsString(app);
|
||||||
|
byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM);
|
||||||
|
byte[] iv = new byte[GCM_IV_LENGTH];
|
||||||
|
new SecureRandom().nextBytes(iv);
|
||||||
|
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, gcmSpec);
|
||||||
|
byte[] encryptedData = cipher.doFinal(jsonBytes);
|
||||||
|
|
||||||
|
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
|
||||||
|
mac.init(new SecretKeySpec(signingKey.getEncoded(), HMAC_ALGORITHM));
|
||||||
|
mac.update(iv);
|
||||||
|
mac.update(encryptedData);
|
||||||
|
byte[] signature = mac.doFinal();
|
||||||
|
|
||||||
|
byte[] bundle = new byte[iv.length + encryptedData.length + signature.length];
|
||||||
|
System.arraycopy(iv, 0, bundle, 0, iv.length);
|
||||||
|
System.arraycopy(encryptedData, 0, bundle, iv.length, encryptedData.length);
|
||||||
|
System.arraycopy(signature, 0, bundle, iv.length + encryptedData.length, signature.length);
|
||||||
|
|
||||||
|
|
||||||
|
exchangeDTO.setData(Base64.getEncoder().encodeToString(bundle));
|
||||||
|
} catch (JsonProcessingException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException |
|
||||||
|
InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException _) {
|
||||||
|
throw new InternalErrorException("Fehler beim Exportieren der App");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return exchangeDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean importApp(AppExchangeDTO exchangeDTO) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
byte[] bundle = Base64.getDecoder().decode(exchangeDTO.getData());
|
||||||
|
|
||||||
|
// 2. Bundle aufteilen
|
||||||
|
if (bundle.length < GCM_IV_LENGTH + 32) {
|
||||||
|
throw new IllegalArgumentException("Ungültiges Export-Bundle");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] iv = new byte[GCM_IV_LENGTH];
|
||||||
|
byte[] signature = new byte[32];
|
||||||
|
byte[] encryptedData = new byte[bundle.length - GCM_IV_LENGTH - 32];
|
||||||
|
|
||||||
|
System.arraycopy(bundle, 0, iv, 0, GCM_IV_LENGTH);
|
||||||
|
System.arraycopy(bundle, GCM_IV_LENGTH, encryptedData, 0, encryptedData.length);
|
||||||
|
System.arraycopy(bundle, GCM_IV_LENGTH + encryptedData.length, signature, 0, 32);
|
||||||
|
|
||||||
|
// 3. Signatur verifizieren
|
||||||
|
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
|
||||||
|
mac.init(new SecretKeySpec(signingKey.getEncoded(), HMAC_ALGORITHM));
|
||||||
|
mac.update(iv);
|
||||||
|
mac.update(encryptedData);
|
||||||
|
byte[] expectedSignature = mac.doFinal();
|
||||||
|
|
||||||
|
if (!java.security.MessageDigest.isEqual(signature, expectedSignature)) {
|
||||||
|
throw new SecurityException("Ungültige Signatur - Daten wurden manipuliert oder stammen nicht von dieser Instanz");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Entschlüsseln
|
||||||
|
Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM);
|
||||||
|
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, encryptionKey, gcmSpec);
|
||||||
|
byte[] decryptedData = cipher.doFinal(encryptedData);
|
||||||
|
|
||||||
|
// 5. JSON deserialisieren
|
||||||
|
String json = new String(decryptedData, StandardCharsets.UTF_8);
|
||||||
|
AppDTO dto = objectMapper.readValue(json, AppDTO.class);
|
||||||
|
|
||||||
|
// 6. Prüfen ob App mit dieser Client-ID bereits existiert
|
||||||
|
var existingApp = appRepository.getByClientId(dto.getClientId());
|
||||||
|
if (existingApp.isPresent()) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"App mit Client-ID '" + dto.getClientId() + "' existiert bereits"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
App app = appTransformer.toAppEntityWithHashedSecret(dto);
|
||||||
|
appRepository.update(app);
|
||||||
|
} catch (JsonProcessingException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException |
|
||||||
|
InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException _) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ public class AppTransformer {
|
||||||
|
|
||||||
dto.setId(entity.getId());
|
dto.setId(entity.getId());
|
||||||
dto.setName(entity.getName());
|
dto.setName(entity.getName());
|
||||||
dto.setClientSecret(entity.getClientSecret());
|
|
||||||
dto.setClientId(entity.getClientId());
|
dto.setClientId(entity.getClientId());
|
||||||
dto.setGroups(entity.getGroups().stream().map(Group::getName).toList());
|
dto.setGroups(entity.getGroups().stream().map(Group::getName).toList());
|
||||||
|
|
||||||
|
|
@ -38,4 +37,26 @@ public class AppTransformer {
|
||||||
group.setName(name);
|
group.setName(name);
|
||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AppDTO toAppDTOWithHashedSecret(App entity) {
|
||||||
|
AppDTO dto = new AppDTO();
|
||||||
|
|
||||||
|
dto.setName(entity.getName());
|
||||||
|
dto.setClientSecret(entity.getClientSecret());
|
||||||
|
dto.setClientId(entity.getClientId());
|
||||||
|
dto.setGroups(entity.getGroups().stream().map(Group::getName).toList());
|
||||||
|
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
public App toAppEntityWithHashedSecret(AppDTO dto) {
|
||||||
|
App entity = new App();
|
||||||
|
|
||||||
|
entity.setName(dto.getName());
|
||||||
|
entity.setClientSecret(dto.getClientSecret());
|
||||||
|
entity.setClientId(dto.getClientId());
|
||||||
|
entity.setGroups(dto.getGroups().stream().map(this::fromGroupDTO).toList());
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue