Introduced app management functionality:
- Added Vue components (`AppListItem`, `AddApp`, `Apps`) for app management. - Implemented Vuex stores for apps and groups. - Enhanced backend: - CRUD operations for apps and group mappings. - Introduced a new service for app-related logic. - Updated database schema and DTO structure. - Adjusted security and CORS configurations. - Updated docker-compose to align service dependencies.
This commit is contained in:
parent
f75b20830c
commit
6acfbe1602
22 changed files with 604 additions and 65 deletions
|
|
@ -1,54 +1,50 @@
|
|||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: lcc-mysql
|
||||
container_name: lcc-mysql-local
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
|
||||
MYSQL_DATABASE: ${DB_DATABASE}
|
||||
MYSQL_USER: ${DB_USER}
|
||||
MYSQL_PASSWORD: ${DB_PASSWORD}
|
||||
MYSQL_DATABASE: lcc
|
||||
MYSQL_USER: ${SPRING_DATASOURCE_USERNAME}
|
||||
MYSQL_PASSWORD: ${SPRING_DATASOURCE_PASSWORD}
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
- mysql-data-local:/var/lib/mysql
|
||||
ports:
|
||||
- "3306:3306"
|
||||
networks:
|
||||
- lcc-network
|
||||
- lcc-network-local
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
lcc-app:
|
||||
#image: git.avatic.de/avatic/lcc:latest
|
||||
# Oder für lokales Bauen:
|
||||
build: .
|
||||
container_name: lcc-app
|
||||
container_name: lcc-app-local
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
DB_DATABASE: ${DB_DATABASE}
|
||||
DB_USER: ${DB_USER}
|
||||
DB_PASSWORD: ${DB_PASSWORD}
|
||||
ALLOWED_CORS_DOMAIN: ${ALLOWED_CORS_DOMAIN}
|
||||
LCC_BASE_URL: ${LCC_BASE_URL}
|
||||
AZURE_MAPS_CLIENT_ID: ${AZURE_MAPS_CLIENT_ID}
|
||||
AZURE_MAPS_SUBSCRIPTION_KEY: ${AZURE_MAPS_SUBSCRIPTION_KEY}
|
||||
AZURE_TENANT_ID: ${AZURE_TENANT_ID}
|
||||
AZURE_CLIENT_ID: ${AZURE_CLIENT_ID}
|
||||
AZURE_CLIENT_SECRET: ${AZURE_CLIENT_SECRET}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/${DB_DATABASE}
|
||||
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE}
|
||||
# Überschreibe die Datasource URL für Docker-Netzwerk
|
||||
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/lcc
|
||||
ports:
|
||||
- "8080:8080"
|
||||
networks:
|
||||
- lcc-network
|
||||
- lcc-network-local
|
||||
dns:
|
||||
- 8.8.8.8
|
||||
- 8.8.4.4
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
mysql-data:
|
||||
mysql-data-local:
|
||||
|
||||
networks:
|
||||
lcc-network:
|
||||
lcc-network-local:
|
||||
driver: bridge
|
||||
226
src/frontend/src/components/UI/AddApp.vue
Normal file
226
src/frontend/src/components/UI/AddApp.vue
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
<template>
|
||||
<div class="stage-container">
|
||||
<transition name="slide-fade" mode="out-in">
|
||||
<div v-if="stage1" key="stage1">
|
||||
<div class="add-app">
|
||||
<div>App name</div>
|
||||
<div>
|
||||
<div class="text-container"><input class="input-field" v-model="appName" @input="checkChange"/></div>
|
||||
</div>
|
||||
<div class="add-app-group-header">App groups</div>
|
||||
<div>
|
||||
<app-group-item v-for="group in groups" :group-obj="group" :key="group.group_name"
|
||||
:selected="isSelected(group.group_name)"
|
||||
@checkbox-changed="updateSelected"></app-group-item>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="add-app-footer">
|
||||
<basic-button :show-icon="false" @click="addApp" :disabled="disableButton">Add app</basic-button>
|
||||
<basic-button :show-icon="false" @click="closeModal(false)" variant="secondary">Cancel</basic-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else key="stage2">
|
||||
<div class="secret-warning">
|
||||
<ph-warning size="18px"></ph-warning>
|
||||
This is your app's client secret. Please keep it safe. It will not be shown again.
|
||||
</div>
|
||||
<box variant="border" class="add-app-secret">
|
||||
<div class="secret-header">App</div>
|
||||
<div class="secret">{{ appName }}</div>
|
||||
<div class="secret-header">Client Id</div>
|
||||
<div class="secret">{{ clientId }}</div>
|
||||
<div class="secret-header">Client secret</div>
|
||||
<div class="secret">{{ clientSecret }}</div>
|
||||
</box>
|
||||
<div class="add-app-footer">
|
||||
<basic-button :show-icon="false" @click="closeModal(true)">Close</basic-button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import InputField from "@/components/UI/InputField.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {useGroupStore} from "@/store/group.js";
|
||||
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||
import AppGroupItem from "@/components/UI/AppGroupItem.vue";
|
||||
import {useAppsStore} from "@/store/apps.js";
|
||||
import checkbox from "@/components/UI/Checkbox.vue";
|
||||
import Box from "@/components/UI/Box.vue";
|
||||
|
||||
export default {
|
||||
name: "AddApp",
|
||||
components: {Box, AppGroupItem, BasicButton, Checkbox, InputField},
|
||||
computed: {
|
||||
checkbox() {
|
||||
return checkbox
|
||||
},
|
||||
...mapStores(useGroupStore, useAppsStore),
|
||||
groups() {
|
||||
return this.groupStore.groups;
|
||||
},
|
||||
disableButton() {
|
||||
return this.appName === null || this.appName.length === 0 || Object.values(this.selectedGroups).every(value => !value);
|
||||
}
|
||||
|
||||
},
|
||||
async created() {
|
||||
await this.groupStore.loadGroups()
|
||||
this.groupStore.groups?.forEach(group => {
|
||||
this.selectedGroups[group.group_name] = false;
|
||||
})
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
stage1: true,
|
||||
appName: '',
|
||||
selectedGroups: {},
|
||||
clientSecret: '',
|
||||
clientId: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isSelected(groupName) {
|
||||
if (groupName === null || this.selectedGroups == null || Object.keys(this.selectedGroups).length === 0)
|
||||
return false;
|
||||
|
||||
const group = this.selectedGroups[groupName];
|
||||
if ((group ?? null) === null)
|
||||
return false;
|
||||
return group;
|
||||
},
|
||||
async addApp() {
|
||||
if (!this.disableButton) {
|
||||
const app = await this.appsStore.addApp(this.appName, this.selectedGroups);
|
||||
this.stage1 = false;
|
||||
console.log("app in add app", app);
|
||||
this.clientSecret = app.client_secret;
|
||||
this.clientId = app.client_id;
|
||||
}
|
||||
},
|
||||
updateSelected(checked, groupName) {
|
||||
this.selectedGroups[groupName] = checked;
|
||||
|
||||
},
|
||||
closeModal() {
|
||||
this.$emit('close');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.stage-container {
|
||||
position: relative;
|
||||
min-height: 20rem;
|
||||
margin: 1.6rem;
|
||||
}
|
||||
|
||||
/* Transition animations */
|
||||
.slide-fade-enter-active {
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.slide-fade-leave-active {
|
||||
transition: all 0.3s ease-in;
|
||||
}
|
||||
|
||||
.slide-fade-enter-from {
|
||||
transform: translateX(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-fade-leave-to {
|
||||
transform: translateX(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.add-app {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-gap: 10px;
|
||||
font-weight: 500;
|
||||
font-size: 1.4rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.add-app-group-header {
|
||||
align-self: start;
|
||||
padding-top: 2.4rem;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);*/
|
||||
border: 0.2rem solid #E3EDFF;
|
||||
transition: all 0.1s ease;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
|
||||
.input-field {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: none;
|
||||
resize: none;
|
||||
font-family: inherit;
|
||||
font-size: 1.4rem;
|
||||
color: #002F54;
|
||||
width: 100%;
|
||||
min-width: 5rem;
|
||||
}
|
||||
|
||||
.add-app-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.secret-warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.4rem;
|
||||
gap: 1.6rem;
|
||||
background-color: #c3cfdf;
|
||||
color: #002F54;
|
||||
border-radius: 0.8rem;
|
||||
padding: 1.6rem;
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
|
||||
.secret-header {
|
||||
font-weight: 500;
|
||||
font-size: 1.4rem;
|
||||
color: #002F54;
|
||||
}
|
||||
|
||||
.secret {
|
||||
font-weight: 400;
|
||||
font-size: 1.4rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.add-app-secret {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-gap: 2.4rem;
|
||||
font-weight: 500;
|
||||
font-size: 1.4rem;
|
||||
align-items: center;
|
||||
margin-bottom: 2.4rem;
|
||||
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
83
src/frontend/src/components/UI/AppListItem.vue
Normal file
83
src/frontend/src/components/UI/AppListItem.vue
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<template>
|
||||
<div>
|
||||
<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="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>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Box from "@/components/UI/Box.vue";
|
||||
import IconButton from "@/components/UI/IconButton.vue";
|
||||
import BasicBadge from "@/components/UI/BasicBadge.vue";
|
||||
|
||||
export default {
|
||||
name: "AppListItem",
|
||||
components: {BasicBadge, IconButton, Box},
|
||||
emits: ["deleteApp"],
|
||||
props: {
|
||||
app: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
groups() {
|
||||
return this.app.groups;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
deleteClick() {
|
||||
this.$emit("deleteApp", this.app.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-list-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr 0.5fr;
|
||||
grid-gap: 2.4rem;
|
||||
padding: 1.6rem 0;
|
||||
font-weight: 400;
|
||||
font-size: 1.4rem;
|
||||
border-bottom: 0.1rem solid #E3EDFF;
|
||||
align-items: center;
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
.app-name-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.badge-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
|
||||
.app-name-name {
|
||||
font-weight: 500;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.app-name-id {
|
||||
font-weight: 400;
|
||||
font-size: 1.4rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.action-container{
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
86
src/frontend/src/components/layout/config/Apps.vue
Normal file
86
src/frontend/src/components/layout/config/Apps.vue
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
|
||||
|
||||
<div class="app-list-header">
|
||||
<div>App</div>
|
||||
<div>Groups</div>
|
||||
<div>Action</div>
|
||||
</div>
|
||||
<div class="app-list">
|
||||
|
||||
<app-list-item v-for="app in apps" :app="app" @delete-app="deleteApp"></app-list-item>
|
||||
</div>
|
||||
|
||||
<modal :state="modalState">
|
||||
<add-app @close="closeModal"></add-app>
|
||||
</modal>
|
||||
<basic-button icon="Plus" @click="modalState = true">New App</basic-button>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||
import AppListItem from "@/components/UI/AppListItem.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {useAppsStore} from "@/store/apps.js";
|
||||
import Modal from "@/components/UI/Modal.vue";
|
||||
import AddApp from "@/components/UI/AddApp.vue";
|
||||
|
||||
export default {
|
||||
name: "Apps",
|
||||
props: {
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
components: {AddApp, Modal, AppListItem, BasicButton},
|
||||
computed: {
|
||||
...mapStores(useAppsStore),
|
||||
apps() {
|
||||
return this.appsStore.apps;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modalState: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async closeModal(success) {
|
||||
this.modalState = false;
|
||||
if (success) {
|
||||
await this.appsStore.loadApps();
|
||||
}
|
||||
},
|
||||
deleteApp(id) {
|
||||
this.appsStore.deleteApp(id);
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
await this.appsStore.loadApps();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.app-list-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr 0.5fr;
|
||||
grid-gap: 1rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 500;
|
||||
font-size: 1.4rem;
|
||||
color: #6B869C;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08rem;
|
||||
border-bottom: 0.1rem solid #E3EDFF;
|
||||
}
|
||||
|
||||
.app-list {
|
||||
margin-bottom: 2.4rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -27,6 +27,7 @@ import Materials from "@/components/layout/config/Materials.vue";
|
|||
import ErrorLog from "@/pages/ErrorLog.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {useActiveUserStore} from "@/store/activeuser.js";
|
||||
import Apps from "@/components/layout/config/Apps.vue";
|
||||
|
||||
export default {
|
||||
name: "Config",
|
||||
|
|
@ -39,6 +40,11 @@ export default {
|
|||
component: markRaw(Properties),
|
||||
props: {isSelected: false},
|
||||
},
|
||||
appsTab: {
|
||||
title: 'Apps',
|
||||
component: markRaw(Apps),
|
||||
props: {isSelected: false},
|
||||
},
|
||||
systemLogTab: {
|
||||
title: 'System log',
|
||||
component: markRaw(ErrorLog),
|
||||
|
|
@ -76,6 +82,10 @@ export default {
|
|||
tabs.push(this.systemLogTab);
|
||||
}
|
||||
|
||||
if(this.activeUserStore.isService) {
|
||||
tabs.push(this.appsTab);
|
||||
}
|
||||
|
||||
tabs.push(this.materialsTab);
|
||||
tabs.push(this.nodesTab);
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export const useActiveUserStore = defineStore('activeUser', {
|
|||
allowConfiguration(state) {
|
||||
if (state.user === null)
|
||||
return false;
|
||||
return state.user.groups?.includes("super") || state.user.groups?.includes("freight") || state.user.groups?.includes("packaging");
|
||||
return state.user.groups?.includes("super") || state.user.groups?.includes("freight") || state.user.groups?.includes("packaging") || state.user.groups?.includes("service");
|
||||
},
|
||||
allowReporting(state) {
|
||||
if (state.user === null)
|
||||
|
|
@ -30,6 +30,11 @@ export const useActiveUserStore = defineStore('activeUser', {
|
|||
return false;
|
||||
return state.user.groups?.includes("super");
|
||||
},
|
||||
isService(state) {
|
||||
if (state.user === null)
|
||||
return false;
|
||||
return state.user.groups?.includes("service");
|
||||
},
|
||||
isPackaging(state) {
|
||||
if (state.user === null)
|
||||
return false;
|
||||
|
|
|
|||
72
src/frontend/src/store/apps.js
Normal file
72
src/frontend/src/store/apps.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import {config} from '@/config'
|
||||
import {useErrorStore} from "@/store/error.js";
|
||||
import performRequest from "@/backend.js";
|
||||
import logger from "@/logger.js";
|
||||
|
||||
|
||||
export const useAppsStore = defineStore('apps', {
|
||||
state: () => ({
|
||||
apps: [],
|
||||
loading: false,
|
||||
|
||||
}),
|
||||
getters: {
|
||||
getById: (state) => {
|
||||
return (id) => state.apps.find(p => p.id === id)
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async loadApps() {
|
||||
this.loading = true;
|
||||
const url = `${config.backendUrl}/apps`;
|
||||
const resp = await performRequest(this, 'GET', url, null);
|
||||
this.apps = resp.data;
|
||||
this.loading = false;
|
||||
},
|
||||
async addApp(appName, appGroups) {
|
||||
const url = `${config.backendUrl}/apps`;
|
||||
|
||||
const app = {
|
||||
name: appName,
|
||||
groups: [],
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(appGroups)) {
|
||||
if (value) {
|
||||
app.groups.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const resp = await performRequest(this, 'POST', url, app);
|
||||
this.apps.push(resp.data);
|
||||
return resp.data;
|
||||
},
|
||||
async updateApp(appId, appName, appGroups) {
|
||||
const url = `${config.backendUrl}/apps/${appId}`;
|
||||
|
||||
const app = {
|
||||
id: appId,
|
||||
name: appName,
|
||||
groups: [],
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(appGroups)) {
|
||||
if (value) {
|
||||
app.groups.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
const resp = await performRequest(this, 'POST', url, app);
|
||||
const index = this.apps.findIndex(a => a.id === app.id);
|
||||
this.apps[index] = resp.data;
|
||||
},
|
||||
async deleteApp(appId) {
|
||||
const url = `${config.backendUrl}/apps/${appId}`;
|
||||
await performRequest(this, 'DELETE', url, null, false);
|
||||
this.apps = this.apps.filter(a => a.id !== appId);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
25
src/frontend/src/store/group.js
Normal file
25
src/frontend/src/store/group.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import {defineStore} from 'pinia'
|
||||
import {config} from '@/config'
|
||||
import performRequest from "@/backend.js";
|
||||
|
||||
|
||||
export const useGroupStore = defineStore('group', {
|
||||
state: () => ({
|
||||
groups: [],
|
||||
loading: false,
|
||||
}),
|
||||
getters: {
|
||||
getById: (state) => {
|
||||
return (id) => state.group.find(p => p.id === id)
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async loadGroups() {
|
||||
this.loading = true;
|
||||
const url = `${config.backendUrl}/groups`;
|
||||
const resp = await performRequest(this,'GET', url, null);
|
||||
this.groups = resp.data;
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -20,12 +20,15 @@ import java.util.Arrays;
|
|||
@Profile("dev | test")
|
||||
public class CorsConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
private Environment environment;
|
||||
private final Environment environment;
|
||||
|
||||
@Value("${lcc.allowed_cors}")
|
||||
private String allowedCors;
|
||||
|
||||
public CorsConfig(Environment environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(@NotNull CorsRegistry registry) {
|
||||
String[] activeProfiles = environment.getActiveProfiles();
|
||||
|
|
|
|||
|
|
@ -23,11 +23,11 @@ public class LccOidcUser extends DefaultOidcUser {
|
|||
this.userId = userId;
|
||||
}
|
||||
|
||||
public static User createDatabaseUser(String email, String firstName, String lastName, String workdayId) {
|
||||
public static User createDatabaseUser(String email, String firstName, String lastName, String workdayId, boolean isFirstUser) {
|
||||
User user = new User();
|
||||
|
||||
Group group = new Group();
|
||||
group.setName("none");
|
||||
group.setName(isFirstUser ? "service" : "none");
|
||||
|
||||
user.setEmail(email);
|
||||
user.setFirstName(firstName == null ? "" : firstName);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package de.avatic.lcc.config;
|
|||
|
||||
import de.avatic.lcc.model.db.users.User;
|
||||
import de.avatic.lcc.repositories.users.GroupRepository;
|
||||
import de.avatic.lcc.repositories.users.JwtTokenService;
|
||||
import de.avatic.lcc.service.apps.JwtTokenService;
|
||||
import de.avatic.lcc.repositories.users.UserRepository;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
|
|
@ -48,9 +48,6 @@ import java.util.function.Supplier;
|
|||
@EnableMethodSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Value("${lcc.base.url}")
|
||||
private String baseUrl;
|
||||
|
||||
@Bean
|
||||
@Profile("!dev & !test") // Only active when NOT in dev profile
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtTokenService jwtTokenService) throws Exception {
|
||||
|
|
@ -63,10 +60,6 @@ public class SecurityConfig {
|
|||
)
|
||||
.oauth2Login(oauth2 -> oauth2
|
||||
.defaultSuccessUrl("/", true)
|
||||
// Redirect-URI explizit setzen:
|
||||
// .redirectionEndpoint(redirection -> redirection
|
||||
// .baseUri(baseUrl + "/login/oauth2/code/*")
|
||||
// )
|
||||
)
|
||||
.oauth2ResourceServer(oauth2 -> oauth2
|
||||
.jwt(jwt -> jwt
|
||||
|
|
@ -178,8 +171,9 @@ public class SecurityConfig {
|
|||
}
|
||||
|
||||
if (user == null) {
|
||||
userRepository.update(LccOidcUser.createDatabaseUser(email, oidcUser.getGivenName(), oidcUser.getFamilyName(), workdayId));
|
||||
mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_NONE"));
|
||||
var isFirstUser = userRepository.count() == 0;
|
||||
userRepository.update(LccOidcUser.createDatabaseUser(email, oidcUser.getGivenName(), oidcUser.getFamilyName(), workdayId, isFirstUser));
|
||||
mappedAuthorities.add(new SimpleGrantedAuthority(isFirstUser ? "ROLE_SERVICE" : "ROLE_NONE"));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
package de.avatic.lcc.config;
|
||||
import de.avatic.lcc.repositories.users.JwtTokenService;
|
||||
import de.avatic.lcc.service.apps.JwtTokenService;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
package de.avatic.lcc.dto.configuration.apps;
|
||||
package de.avatic.lcc.controller.configuration;
|
||||
|
||||
import de.avatic.lcc.dto.users.AppDTO;
|
||||
import de.avatic.lcc.repositories.users.AppRepository;
|
||||
import com.azure.core.annotation.BodyParam;
|
||||
import de.avatic.lcc.dto.configuration.apps.AppDTO;
|
||||
import de.avatic.lcc.service.apps.AppsService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
|
@ -29,9 +26,15 @@ public class AppsController {
|
|||
}
|
||||
|
||||
@PostMapping({"", "/"})
|
||||
public ResponseEntity<AppDTO> updateApp(AppDTO dto) {
|
||||
public ResponseEntity<AppDTO> updateApp(@RequestBody AppDTO dto) {
|
||||
return ResponseEntity.ok(appsService.updateApp(dto));
|
||||
}
|
||||
|
||||
@DeleteMapping({"/{id}", "/{id}/"})
|
||||
public ResponseEntity<Void> deleteApp(@PathVariable Integer id) {
|
||||
appsService.deleteApp(id);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package de.avatic.lcc.controller.token;
|
||||
|
||||
import de.avatic.lcc.model.db.users.App;
|
||||
import de.avatic.lcc.repositories.users.JwtTokenService;
|
||||
import de.avatic.lcc.service.apps.JwtTokenService;
|
||||
import de.avatic.lcc.service.apps.AppsService;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ public class GroupController {
|
|||
* @return A ResponseEntity containing the list of groups and pagination headers.
|
||||
*/
|
||||
@GetMapping({"/", ""})
|
||||
@PreAuthorize("hasRole('RIGHT-MANAGMENT')")
|
||||
@PreAuthorize("hasAnyRole('RIGHT-MANAGMENT', 'SERVICE')")
|
||||
public ResponseEntity<List<GroupDTO>> listGroups(@RequestParam(defaultValue = "20") @Min(1) int limit,
|
||||
@RequestParam(defaultValue = "1") @Min(1) int page) {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package de.avatic.lcc.dto.users;
|
||||
package de.avatic.lcc.dto.configuration.apps;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
|
|
@ -86,11 +86,14 @@ public class AppRepository {
|
|||
List<Integer> groupIds = groupRepository.findGroupIds(app.getGroups().stream().map(Group::getName).toList());
|
||||
|
||||
if (appId == null) {
|
||||
String sql = """
|
||||
INSERT INTO sys_app (name, client_id, client_secret)
|
||||
VALUES (?, ?, ?)""";
|
||||
|
||||
KeyHolder keyHolder = new GeneratedKeyHolder();
|
||||
jdbcTemplate.update(connection -> {
|
||||
PreparedStatement ps = connection.prepareStatement(
|
||||
"INSERT INTO sys_app (name, client_id, client_secret) " +
|
||||
"VALUES (?, ?, ?)",
|
||||
sql,
|
||||
Statement.RETURN_GENERATED_KEYS
|
||||
);
|
||||
ps.setString(1, app.getName());
|
||||
|
|
@ -104,7 +107,7 @@ public class AppRepository {
|
|||
String query = """
|
||||
UPDATE sys_app SET name = ? WHERE id = ?""";
|
||||
|
||||
jdbcTemplate.update(query, app.getName());
|
||||
jdbcTemplate.update(query, app.getName(), appId);
|
||||
}
|
||||
|
||||
updateAppGroupMappings(appId, groupIds);
|
||||
|
|
@ -150,13 +153,19 @@ public class AppRepository {
|
|||
|
||||
/**
|
||||
* Deletes an app by id.
|
||||
* Also removes all associated group mappings.
|
||||
*
|
||||
* @param id id of the app to delete
|
||||
*/
|
||||
@Transactional
|
||||
public void delete(Integer id) {
|
||||
String sql = "DELETE FROM sys_app WHERE id = ?";
|
||||
jdbcTemplate.update(sql, id);
|
||||
// First delete all group mappings for this app
|
||||
String deleteMappingsSql = "DELETE FROM sys_app_group_mapping WHERE app_id = ?";
|
||||
jdbcTemplate.update(deleteMappingsSql, id);
|
||||
|
||||
// Then delete the app itself
|
||||
String deleteAppSql = "DELETE FROM sys_app WHERE id = ?";
|
||||
jdbcTemplate.update(deleteAppSql, id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -96,8 +96,10 @@ public class UserRepository {
|
|||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Transactional
|
||||
public Integer count() {
|
||||
return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM sys_user", Integer.class);
|
||||
}
|
||||
|
||||
private void updateUserGroupMappings(Integer userId, List<Integer> groups) {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
package de.avatic.lcc.service.apps;
|
||||
|
||||
import de.avatic.lcc.dto.users.AppDTO;
|
||||
import de.avatic.lcc.dto.configuration.apps.AppDTO;
|
||||
import de.avatic.lcc.model.db.users.App;
|
||||
import de.avatic.lcc.repositories.users.AppRepository;
|
||||
import de.avatic.lcc.service.transformer.apps.AppTransformer;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class AppsService {
|
||||
|
|
@ -30,11 +33,35 @@ public class AppsService {
|
|||
public AppDTO updateApp(AppDTO dto) {
|
||||
|
||||
var newApp = dto.getId() == null;
|
||||
var id = !newApp ? dto.getId() : appRepository.update(appTransformer.toAppEntity(dto));
|
||||
String appSecret = null;
|
||||
|
||||
if(newApp) {
|
||||
dto.setClientId(generateAppId());
|
||||
appSecret = generateAppSecret();
|
||||
dto.setClientSecret(passwordEncoder.encode(appSecret));
|
||||
}
|
||||
|
||||
var id = appRepository.update(appTransformer.toAppEntity(dto));
|
||||
|
||||
if(newApp) {
|
||||
dto.setId(id);
|
||||
dto.setClientSecret(appSecret);
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
private String generateAppId() {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
private String generateAppSecret() {
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] bytes = new byte[32];
|
||||
random.nextBytes(bytes);
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
|
||||
}
|
||||
|
||||
public void deleteApp(Integer id) {
|
||||
appRepository.delete(id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package de.avatic.lcc.repositories.users;
|
||||
package de.avatic.lcc.service.apps;
|
||||
|
||||
import de.avatic.lcc.model.db.users.App;
|
||||
import de.avatic.lcc.model.db.users.Group;
|
||||
|
|
@ -12,7 +12,6 @@ import javax.crypto.SecretKey;
|
|||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Key;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class JwtTokenService {
|
||||
|
|
@ -28,7 +27,6 @@ public class JwtTokenService {
|
|||
|
||||
public String createApplicationToken(App app, long expiration) {
|
||||
|
||||
|
||||
return Jwts.builder()
|
||||
.issuer(baseUrl)
|
||||
.subject(app.getClientId())
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package de.avatic.lcc.service.transformer.apps;
|
||||
|
||||
import de.avatic.lcc.dto.users.AppDTO;
|
||||
import de.avatic.lcc.dto.configuration.apps.AppDTO;
|
||||
import de.avatic.lcc.model.db.users.App;
|
||||
import de.avatic.lcc.model.db.users.Group;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ CREATE TABLE IF NOT EXISTS `sys_user_node`
|
|||
FOREIGN KEY (`country_id`) REFERENCES `country` (`id`)
|
||||
) COMMENT 'Contains user generated logistic nodes';
|
||||
|
||||
-- Main table for user information
|
||||
-- Main table for app information
|
||||
CREATE TABLE IF NOT EXISTS `sys_app`
|
||||
(
|
||||
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
|
|
@ -147,7 +147,7 @@ CREATE TABLE IF NOT EXISTS `sys_app`
|
|||
) COMMENT 'Stores basic information about external applications';
|
||||
|
||||
|
||||
-- Junction table for user-group assignments
|
||||
-- Junction table for app-group assignments
|
||||
CREATE TABLE IF NOT EXISTS `sys_app_group_mapping`
|
||||
(
|
||||
`app_id` INT NOT NULL,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue