Compare commits
No commits in common. "master" and "remise-projet" have entirely different histories.
master
...
remise-pro
@ -1,11 +0,0 @@
|
||||
.gradle
|
||||
.idea
|
||||
**/build
|
||||
**/data
|
||||
**/gradle
|
||||
**/logs
|
||||
.gitignore
|
||||
.gitlab-ci.yml
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
gradlew**
|
125
.drone.yml
125
.drone.yml
@ -1,125 +0,0 @@
|
||||
---
|
||||
global-variables:
|
||||
release: &release ${DRONE_TAG}
|
||||
environment: &environment
|
||||
JAVA_VERSION: 17
|
||||
GRADLE_VERSION: 7.3
|
||||
CRE_VERSION: dev-${DRONE_BUILD_NUMBER}
|
||||
CRE_ARTIFACT_NAME: ColorRecipesExplorer
|
||||
CRE_REGISTRY_IMAGE: registry.fyloz.dev/colorrecipesexplorer/backend
|
||||
CRE_PORT: 9101
|
||||
CRE_RELEASE: *release
|
||||
gradle-image: &gradle-image gradle:7.3-jdk17
|
||||
alpine-image: &alpine-image alpine:latest
|
||||
docker-registry: &docker-registry registry.fyloz.dev
|
||||
docker-registry-repo: &docker-registry-repo registry.fyloz.dev/colorrecipesexplorer/backend
|
||||
|
||||
kind: pipeline
|
||||
name: default
|
||||
type: docker
|
||||
|
||||
steps:
|
||||
- name: gradle-test
|
||||
image: *gradle-image
|
||||
commands:
|
||||
- gradle test
|
||||
when:
|
||||
branch: develop
|
||||
|
||||
- name: set-docker-tags-latest
|
||||
image: *alpine-image
|
||||
environment:
|
||||
<<: *environment
|
||||
commands:
|
||||
- echo -n "latest" > .tags
|
||||
when:
|
||||
branch: develop
|
||||
event:
|
||||
exclude:
|
||||
- pull_request
|
||||
|
||||
- name: set-docker-tags-release
|
||||
image: *alpine-image
|
||||
environment:
|
||||
<<: *environment
|
||||
commands:
|
||||
- echo -n "latest-release,$CRE_RELEASE" > .tags
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: containerize-dev
|
||||
image: plugins/docker
|
||||
environment:
|
||||
<<: *environment
|
||||
settings:
|
||||
build_args_from_env:
|
||||
- GRADLE_VERSION
|
||||
- JAVA_VERSION
|
||||
- CRE_VERSION
|
||||
registry: *docker-registry
|
||||
repo: *docker-registry-repo
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
when:
|
||||
branch: develop
|
||||
event:
|
||||
exclude:
|
||||
- pull_request
|
||||
|
||||
- name: containerize-release
|
||||
image: plugins/docker
|
||||
environment:
|
||||
<<: *environment
|
||||
settings:
|
||||
build_args_from_env:
|
||||
- GRADLE_VERSION
|
||||
- JAVA_VERSION
|
||||
build_args:
|
||||
- CRE_VERSION=${DRONE_TAG}
|
||||
registry: *docker-registry
|
||||
repo: *docker-registry-repo
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: deploy
|
||||
image: alpine:latest
|
||||
environment:
|
||||
<<: *environment
|
||||
CRE_REGISTRY_IMAGE: *docker-registry-repo
|
||||
DEPLOY_SERVER:
|
||||
from_secret: deploy_server
|
||||
DEPLOY_SERVER_USERNAME:
|
||||
from_secret: deploy_server_username
|
||||
DEPLOY_SERVER_SSH_PORT:
|
||||
from_secret: deploy_server_ssh_port
|
||||
DEPLOY_SERVER_SSH_KEY:
|
||||
from_secret: deploy_server_ssh_key
|
||||
DEPLOY_CONTAINER_NAME: cre_backend
|
||||
DEPLOY_SPRING_PROFILES: mysql,rest
|
||||
DEPLOY_DATA_VOLUME: /var/cre/data
|
||||
DEPLOY_CONFIG_VOLUME: /var/cre/config
|
||||
DEPLOY_LOGS_VOLUME: /var/cre/logs
|
||||
commands:
|
||||
- apk update
|
||||
- apk add --no-cache openssh-client
|
||||
- mkdir -p ~/.ssh
|
||||
- echo "$DEPLOY_SERVER_SSH_KEY" | tr -d '\r' > ~/.ssh/id_rsa
|
||||
- chmod 700 ~/.ssh/id_rsa
|
||||
- eval $(ssh-agent -s)
|
||||
- ssh-add ~/.ssh/id_rsa
|
||||
- ssh-keyscan -p $DEPLOY_SERVER_SSH_PORT -H $DEPLOY_SERVER >> ~/.ssh/known_hosts
|
||||
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
|
||||
- ssh -p $DEPLOY_SERVER_SSH_PORT $DEPLOY_SERVER_USERNAME@$DEPLOY_SERVER "docker stop $DEPLOY_CONTAINER_NAME || true && docker rm $DEPLOY_CONTAINER_NAME || true"
|
||||
- ssh -p $DEPLOY_SERVER_SSH_PORT $DEPLOY_SERVER_USERNAME@$DEPLOY_SERVER "docker pull $CRE_REGISTRY_IMAGE:$CRE_RELEASE"
|
||||
- ssh -p $DEPLOY_SERVER_SSH_PORT $DEPLOY_SERVER_USERNAME@$DEPLOY_SERVER "docker run -d -p $CRE_PORT:9090 --name=$DEPLOY_CONTAINER_NAME -v $DEPLOY_DATA_VOLUME:/usr/bin/data -v $DEPLOY_CONFIG_VOLUME:/usr/bin/config -v $DEPLOY_LOGS_VOLUME:/usr/bin/logs -e spring_profiles_active=$DEPLOY_SPRING_PROFILES $CRE_REGISTRY_IMAGE:$CRE_RELEASE"
|
||||
when:
|
||||
event:
|
||||
- tag
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -4,8 +4,8 @@
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
## Other directories
|
||||
.gradle/
|
||||
gradle/
|
||||
build/
|
||||
logs/
|
||||
data/
|
||||
@ -13,8 +13,4 @@ dokka/
|
||||
dist/
|
||||
out/
|
||||
|
||||
## Generated configuration file
|
||||
config.properties
|
||||
|
||||
## Prevent ignoring the gradle wrapper
|
||||
!gradle-wrapper.jar
|
||||
/src/main/resources/angular/static/*
|
||||
|
@ -48,8 +48,8 @@ package:
|
||||
ARTIFACT_NAME: "ColorRecipesExplorer-backend-$CI_PIPELINE_IID"
|
||||
script:
|
||||
- docker rm $PACKAGE_CONTAINER_NAME || true
|
||||
- docker run --name $PACKAGE_CONTAINER_NAME $CI_REGISTRY_IMAGE_GRADLE gradle bootJar -Pversion=$CI_PIPELINE_IID
|
||||
- docker cp $PACKAGE_CONTAINER_NAME:/usr/src/cre/build/libs/ColorRecipesExplorer-$CI_PIPELINE_IID.jar $ARTIFACT_NAME.jar
|
||||
- docker run --name $PACKAGE_CONTAINER_NAME $CI_REGISTRY_IMAGE_GRADLE gradle bootJar
|
||||
- docker cp $PACKAGE_CONTAINER_NAME:/usr/src/cre/build/libs/ColorRecipesExplorer.jar $ARTIFACT_NAME.jar
|
||||
- docker build -t $CI_REGISTRY_IMAGE_BACKEND --build-arg JDK_VERSION=$JDK_VERSION --build-arg PORT=$PORT --build-arg ARTIFACT_NAME=$ARTIFACT_NAME .
|
||||
- docker push $CI_REGISTRY_IMAGE_BACKEND
|
||||
after_script:
|
||||
@ -81,4 +81,4 @@ deploy:
|
||||
script:
|
||||
- ssh -p $DEPLOYMENT_SERVER_SSH_PORT $DEPLOYMENT_SERVER_USERNAME@$DEPLOYMENT_SERVER "docker stop $DEPLOYED_CONTAINER_NAME || true && docker rm $DEPLOYED_CONTAINER_NAME || true"
|
||||
- ssh -p $DEPLOYMENT_SERVER_SSH_PORT $DEPLOYMENT_SERVER_USERNAME@$DEPLOYMENT_SERVER "docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY && docker pull $CI_REGISTRY_IMAGE_BACKEND"
|
||||
- ssh -p $DEPLOYMENT_SERVER_SSH_PORT $DEPLOYMENT_SERVER_USERNAME@$DEPLOYMENT_SERVER "docker run -d -p $PORT:$PORT --name=$DEPLOYED_CONTAINER_NAME -v $DATA_VOLUME:/usr/bin/cre/data -v $CONFIG_VOLUME:/usr/bin/cre/config -e spring_profiles_active=$SPRING_PROFILES $CI_REGISTRY_IMAGE_BACKEND"
|
||||
- ssh -p $DEPLOYMENT_SERVER_SSH_PORT $DEPLOYMENT_SERVER_USERNAME@$DEPLOYMENT_SERVER "docker run -d -p $PORT:$PORT --name=$DEPLOYED_CONTAINER_NAME -v $DATA_VOLUME:/usr/bin/cre/data -e spring_profiles_active=$SPRING_PROFILES -e spring_datasource_username=$DB_USERNAME -e spring_datasource_password=$DB_PASSWORD -e spring_datasource_url=$DB_URL -e cre_server_deployment_url=$DEPLOYMENT_URL -e databaseupdater_username=$DB_UPDATE_USERNAME -e databaseupdater_password=$DB_UPDATE_PASSWORD $CI_REGISTRY_IMAGE_BACKEND"
|
||||
|
30
Dockerfile
30
Dockerfile
@ -1,33 +1,21 @@
|
||||
ARG GRADLE_VERSION=7.3
|
||||
ARG JAVA_VERSION=17
|
||||
ARG JAVA_VERSION=11
|
||||
|
||||
FROM gradle:$GRADLE_VERSION-jdk$JAVA_VERSION AS build
|
||||
WORKDIR /usr/src
|
||||
COPY . .
|
||||
FROM openjdk:$JAVA_VERSION
|
||||
|
||||
ARG CRE_VERSION=dev
|
||||
RUN gradle bootJar -Pversion=$CRE_VERSION
|
||||
WORKDIR /usr/bin/cre/
|
||||
|
||||
FROM alpine:latest
|
||||
WORKDIR /usr/bin
|
||||
ARG ARTIFACT_NAME=ColorRecipesExplorer
|
||||
COPY $ARTIFACT_NAME.jar ColorRecipesExplorer.jar
|
||||
|
||||
ARG JAVA_VERSION
|
||||
RUN apk add --no-cache openjdk$JAVA_VERSION
|
||||
|
||||
ARG CRE_VERSION
|
||||
COPY --from=build /usr/src/build/libs/ColorRecipesExplorer-$CRE_VERSION.jar ColorRecipesExplorer.jar
|
||||
|
||||
ARG CRE_PORT=9090
|
||||
EXPOSE $CRE_PORT
|
||||
ARG PORT=9090
|
||||
EXPOSE $PORT
|
||||
|
||||
ENV spring_profiles_active=h2,rest
|
||||
ENV server_port=$CRE_PORT
|
||||
ENV server_port=$PORT
|
||||
ENV spring_datasource_url=jdbc:h2:mem:cre
|
||||
ENV spring_datasource_username=root
|
||||
ENV spring_datasource_password=pass
|
||||
|
||||
VOLUME /usr/bin/data
|
||||
VOLUME /usr/bin/config
|
||||
VOLUME /usr/bin/logs
|
||||
VOLUME /usr/bin/cre/data
|
||||
|
||||
ENTRYPOINT ["java", "-jar", "ColorRecipesExplorer.jar"]
|
||||
|
@ -2,17 +2,17 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
group = "dev.fyloz.colorrecipesexplorer"
|
||||
|
||||
val kotlinVersion = "1.6.20"
|
||||
val springBootVersion = "2.6.1"
|
||||
val kotlinVersion = "1.5.0"
|
||||
val springBootVersion = "2.3.4.RELEASE"
|
||||
|
||||
plugins {
|
||||
// Outer scope variables can't be accessed in the plugins section, so we have to redefine them here
|
||||
val kotlinVersion = "1.6.20"
|
||||
val springBootVersion = "2.6.1"
|
||||
val kotlinVersion = "1.5.0"
|
||||
val springBootVersion = "2.3.4.RELEASE"
|
||||
|
||||
id("java")
|
||||
id("org.jetbrains.kotlin.jvm") version kotlinVersion
|
||||
id("org.jetbrains.dokka") version "1.6.10"
|
||||
id("org.jetbrains.dokka") version "1.4.32"
|
||||
id("org.springframework.boot") version springBootVersion
|
||||
id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion
|
||||
id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion
|
||||
@ -22,25 +22,20 @@ repositories {
|
||||
mavenCentral()
|
||||
|
||||
maven {
|
||||
url = uri("https://archiva.fyloz.dev/repository/internal")
|
||||
url = uri("https://git.fyloz.dev/api/v4/projects/40/packages/maven")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(platform("org.jetbrains.kotlin:kotlin-bom:${kotlinVersion}"))
|
||||
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
|
||||
implementation("dev.fyloz.colorrecipesexplorer:database-manager:6.2")
|
||||
implementation("dev.fyloz:memorycache:1.0")
|
||||
implementation("io.github.microutils:kotlin-logging-jvm:2.1.21")
|
||||
implementation("io.jsonwebtoken:jjwt-api:0.11.2")
|
||||
implementation("io.jsonwebtoken:jjwt-impl:0.11.2")
|
||||
implementation("io.jsonwebtoken:jjwt-jackson:0.11.2")
|
||||
implementation("javax.xml.bind:jaxb-api:2.3.0")
|
||||
implementation("org.apache.poi:poi-ooxml:4.1.0")
|
||||
implementation("org.apache.pdfbox:pdfbox:2.0.4")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion}")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.11.3")
|
||||
implementation("javax.xml.bind:jaxb-api:2.3.0")
|
||||
implementation("io.jsonwebtoken:jjwt:0.9.1")
|
||||
implementation("org.apache.poi:poi-ooxml:4.1.0")
|
||||
implementation("org.apache.pdfbox:pdfbox:2.0.4")
|
||||
implementation("dev.fyloz.colorrecipesexplorer:database-manager:5.1")
|
||||
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa:${springBootVersion}")
|
||||
implementation("org.springframework.boot:spring-boot-starter-jdbc:${springBootVersion}")
|
||||
@ -50,29 +45,24 @@ dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-configuration-processor:${springBootVersion}")
|
||||
implementation("org.springframework.boot:spring-boot-devtools:${springBootVersion}")
|
||||
|
||||
testImplementation("org.springframework:spring-test:5.1.6.RELEASE")
|
||||
testImplementation("org.mockito:mockito-inline:3.6.0")
|
||||
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
||||
testImplementation("io.mockk:mockk:1.12.1")
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test:${kotlinVersion}")
|
||||
testImplementation("org.mockito:mockito-inline:3.11.2")
|
||||
testImplementation("org.springframework:spring-test:5.3.13")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-api:5.3.2")
|
||||
testImplementation("io.mockk:mockk:1.10.6")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test:${springBootVersion}")
|
||||
testImplementation("org.springframework.boot:spring-boot-test-autoconfigure:${springBootVersion}")
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test:${kotlinVersion}")
|
||||
|
||||
runtimeOnly("com.h2database:h2:1.4.199")
|
||||
runtimeOnly("mysql:mysql-connector-java:8.0.22")
|
||||
runtimeOnly("org.postgresql:postgresql:42.2.16")
|
||||
runtimeOnly("com.microsoft.sqlserver:mssql-jdbc:9.2.1.jre11")
|
||||
|
||||
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor:${springBootVersion}")
|
||||
}
|
||||
|
||||
springBoot {
|
||||
buildInfo()
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
@ -86,26 +76,24 @@ sourceSets {
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
|
||||
jvmArgs("-XX:+ShowCodeDetailsInExceptionMessages")
|
||||
testLogging {
|
||||
events("skipped", "failed")
|
||||
setExceptionFormat("full")
|
||||
reports {
|
||||
junitXml.isEnabled = true
|
||||
html.isEnabled = false
|
||||
}
|
||||
|
||||
reports {
|
||||
junitXml.required.set(true)
|
||||
html.required.set(false)
|
||||
useJUnitPlatform()
|
||||
testLogging {
|
||||
events("skipped", "failed")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<JavaCompile>() {
|
||||
options.compilerArgs.addAll(arrayOf("--release", "17"))
|
||||
options.compilerArgs.addAll(arrayOf("--release", "11"))
|
||||
}
|
||||
tasks.withType<KotlinCompile>().all {
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
useIR = true
|
||||
freeCompilerArgs = listOf(
|
||||
"-Xopt-in=kotlin.contracts.ExperimentalContracts",
|
||||
"-Xinline-classes"
|
||||
|
@ -1,16 +0,0 @@
|
||||
version: "3.1"
|
||||
|
||||
services:
|
||||
cre.frontend:
|
||||
image: fyloz.dev:5443/color-recipes-explorer/frontend:latest
|
||||
ports:
|
||||
- "4200:80"
|
||||
cre.database:
|
||||
image: mysql
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: "pass"
|
||||
MYSQL_DATABASE: "cre"
|
||||
ports:
|
||||
- "3306:3306"
|
||||
|
10
gradle.Dockerfile
Normal file
10
gradle.Dockerfile
Normal file
@ -0,0 +1,10 @@
|
||||
ARG JDK_VERSION=11
|
||||
ARG GRADLE_VERSION=6.8
|
||||
|
||||
FROM gradle:$GRADLE_VERSION-jdk$JDK_VERSION
|
||||
WORKDIR /usr/src/cre/
|
||||
|
||||
COPY build.gradle.kts build.gradle.kts
|
||||
COPY settings.gradle.kts settings.gradle.kts
|
||||
COPY src src
|
||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
5
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +0,0 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
257
gradlew
vendored
257
gradlew
vendored
@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -17,101 +17,67 @@
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
@ -121,9 +87,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
@ -132,7 +98,7 @@ Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
@ -140,95 +106,80 @@ location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
@ -1,12 +1,10 @@
|
||||
package dev.fyloz.colorrecipesexplorer.logic.files;
|
||||
package dev.fyloz.colorrecipesexplorer.service.files;
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeDto;
|
||||
import dev.fyloz.colorrecipesexplorer.logic.RecipeLogic;
|
||||
import dev.fyloz.colorrecipesexplorer.model.Recipe;
|
||||
import dev.fyloz.colorrecipesexplorer.service.RecipeService;
|
||||
import dev.fyloz.colorrecipesexplorer.xlsx.XlsxExporter;
|
||||
import mu.KotlinLogging;
|
||||
import org.slf4j.Logger;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@ -16,14 +14,15 @@ import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
@Service
|
||||
@Profile("!emergency")
|
||||
public class XlsService {
|
||||
private final RecipeLogic recipeService;
|
||||
private final Logger logger = KotlinLogging.INSTANCE.logger("XlsService");
|
||||
|
||||
private final RecipeService recipeService;
|
||||
private final Logger logger;
|
||||
|
||||
@Autowired
|
||||
public XlsService(RecipeLogic recipeLogic) {
|
||||
this.recipeService = recipeLogic;
|
||||
public XlsService(RecipeService recipeService, Logger logger) {
|
||||
this.recipeService = recipeService;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -32,7 +31,7 @@ public class XlsService {
|
||||
* @param recipe La recette
|
||||
* @return Le fichier XLS de la recette
|
||||
*/
|
||||
public byte[] generate(RecipeDto recipe) {
|
||||
public byte[] generate(Recipe recipe) {
|
||||
return new XlsxExporter(logger).generate(recipe);
|
||||
}
|
||||
|
||||
@ -55,10 +54,10 @@ public class XlsService {
|
||||
logger.info("Exportation de toutes les couleurs en XLS");
|
||||
|
||||
byte[] zipContent;
|
||||
Collection<RecipeDto> recipes = recipeService.getAll();
|
||||
Collection<Recipe> recipes = recipeService.getAll();
|
||||
|
||||
try (ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(); ZipOutputStream zipOutput = new ZipOutputStream(byteOutput)) {
|
||||
for (RecipeDto recipe : recipes) {
|
||||
for (Recipe recipe : recipes) {
|
||||
byte[] recipeXLS = generate(recipe);
|
||||
zipOutput.putNextEntry(new ZipEntry(String.format("%s_%s.xlsx", recipe.getCompany().getName(), recipe.getName())));
|
||||
zipOutput.write(recipeXLS, 0, recipeXLS.length);
|
@ -1,8 +1,8 @@
|
||||
package dev.fyloz.colorrecipesexplorer.xlsx;
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.MixDto;
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.MixQuantityOutputDto;
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeDto;
|
||||
import dev.fyloz.colorrecipesexplorer.model.Mix;
|
||||
import dev.fyloz.colorrecipesexplorer.model.MixMaterial;
|
||||
import dev.fyloz.colorrecipesexplorer.model.Recipe;
|
||||
import dev.fyloz.colorrecipesexplorer.xlsx.component.Document;
|
||||
import dev.fyloz.colorrecipesexplorer.xlsx.component.Sheet;
|
||||
import dev.fyloz.colorrecipesexplorer.xlsx.component.Table;
|
||||
@ -23,7 +23,7 @@ public class XlsxExporter {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public byte[] generate(RecipeDto recipe) {
|
||||
public byte[] generate(Recipe recipe) {
|
||||
logger.info(String.format("Génération du XLS de la couleur %s (%s)", recipe.getName(), recipe.getId()));
|
||||
|
||||
Document document = new Document(recipe.getName(), logger);
|
||||
@ -44,7 +44,7 @@ public class XlsxExporter {
|
||||
return output;
|
||||
}
|
||||
|
||||
private void registerCells(RecipeDto recipe, Sheet sheet) {
|
||||
private void registerCells(Recipe recipe, Sheet sheet) {
|
||||
// Header
|
||||
sheet.registerCell(new TitleCell(recipe.getName()));
|
||||
sheet.registerCell(new DescriptionCell(DescriptionCell.DescriptionCellType.NAME, "Bannière"));
|
||||
@ -59,20 +59,20 @@ public class XlsxExporter {
|
||||
sheet.registerCell(new DescriptionCell(DescriptionCell.DescriptionCellType.VALUE_STR, recipe.getRemark()));
|
||||
|
||||
// Mélanges
|
||||
Collection<MixDto> recipeMixes = recipe.getMixes();
|
||||
Collection<Mix> recipeMixes = recipe.getMixes();
|
||||
if (recipeMixes.size() > 0) {
|
||||
sheet.registerCell(new SectionTitleCell("Recette"));
|
||||
|
||||
for (MixDto mix : recipeMixes) {
|
||||
Table mixTable = new Table(4, mix.getMixQuantities().getAll().size() + 1, mix.getMixType().getName());
|
||||
for (Mix mix : recipeMixes) {
|
||||
Table mixTable = new Table(4, mix.getMixMaterials().size() + 1, mix.getMixType().getName());
|
||||
mixTable.setColumnName(0, "Quantité");
|
||||
mixTable.setColumnName(2, "Unités");
|
||||
|
||||
int row = 0;
|
||||
for (MixQuantityOutputDto mixQuantity : mix.getMixQuantitiesOutput()) {
|
||||
mixTable.setRowName(row, mixQuantity.getMaterial().getName());
|
||||
mixTable.setContent(new Position(1, row + 1), mixQuantity.getQuantity());
|
||||
mixTable.setContent(new Position(3, row + 1), mixQuantity.getMaterial().getMaterialType().getUsePercentages() ? "%" : "mL");
|
||||
for (MixMaterial mixMaterial : mix.getMixMaterials()) {
|
||||
mixTable.setRowName(row, mixMaterial.getMaterial().getName());
|
||||
mixTable.setContent(new Position(1, row + 1), mixMaterial.getQuantity());
|
||||
mixTable.setContent(new Position(3, row + 1), mixMaterial.getMaterial().getMaterialType().getUsePercentages() ? "%" : "mL");
|
||||
|
||||
row++;
|
||||
}
|
||||
|
@ -1,39 +1,20 @@
|
||||
package dev.fyloz.colorrecipesexplorer
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.ApplicationInitializer
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.MaterialTypeProperties
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder
|
||||
import org.springframework.context.ConfigurableApplicationContext
|
||||
import org.springframework.scheduling.annotation.EnableScheduling
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.boot.runApplication
|
||||
|
||||
@SpringBootApplication(exclude = [LiquibaseAutoConfiguration::class])
|
||||
@EnableScheduling
|
||||
@EnableConfigurationProperties(
|
||||
MaterialTypeProperties::class,
|
||||
CreProperties::class,
|
||||
DatabaseUpdaterProperties::class
|
||||
)
|
||||
class ColorRecipesExplorerApplication
|
||||
|
||||
var emergencyMode = false
|
||||
|
||||
private lateinit var context: ConfigurableApplicationContext
|
||||
private lateinit var classLoader: ClassLoader
|
||||
|
||||
fun main() {
|
||||
classLoader = Thread.currentThread().contextClassLoader
|
||||
context = runApplication()
|
||||
runApplication<ColorRecipesExplorerApplication>()
|
||||
}
|
||||
|
||||
fun restartApplication(enableEmergencyMode: Boolean = false) {
|
||||
val thread = Thread {
|
||||
emergencyMode = enableEmergencyMode
|
||||
context.close()
|
||||
context = runApplication()
|
||||
}
|
||||
|
||||
thread.contextClassLoader = classLoader
|
||||
thread.isDaemon = false
|
||||
thread.start()
|
||||
}
|
||||
|
||||
private fun runApplication() =
|
||||
SpringApplicationBuilder(ColorRecipesExplorerApplication::class.java).apply {
|
||||
listeners(ApplicationInitializer())
|
||||
}.run()
|
||||
|
@ -1,50 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer
|
||||
|
||||
object Constants {
|
||||
object ControllerPaths {
|
||||
const val COMPANY = "/api/company"
|
||||
const val FILE = "/api/file"
|
||||
const val GROUP = "/api/user/group"
|
||||
const val INVENTORY = "/api/inventory"
|
||||
const val MATERIAL = "/api/material"
|
||||
const val MATERIAL_TYPE = "/api/materialtype"
|
||||
const val MIX = "/api/recipe/mix"
|
||||
const val RECIPE = "/api/recipe"
|
||||
const val TOUCH_UP_KIT = "/api/touchupkit"
|
||||
const val USER = "/api/user"
|
||||
}
|
||||
|
||||
object FilePaths {
|
||||
private const val PDF = "pdf"
|
||||
private const val IMAGES = "images"
|
||||
|
||||
const val SIMDUT = "$PDF/simdut"
|
||||
const val TOUCH_UP_KITS = "$PDF/touchupkits"
|
||||
const val RECIPE_IMAGES = "$IMAGES/recipes"
|
||||
}
|
||||
|
||||
object ModelNames {
|
||||
const val COMPANY = "Company"
|
||||
const val GROUP = "Group"
|
||||
const val MATERIAL = "Material"
|
||||
const val MATERIAL_TYPE = "MaterialType"
|
||||
const val MIX = "Mix"
|
||||
const val MIX_MATERIAL = "MixMaterial"
|
||||
const val MIX_TYPE = "MixType"
|
||||
const val RECIPE = "Recipe"
|
||||
const val RECIPE_STEP = "RecipeStep"
|
||||
const val TOUCH_UP_KIT = "TouchUpKit"
|
||||
const val USER = "User"
|
||||
}
|
||||
|
||||
object ValidationMessages {
|
||||
const val SIZE_GREATER_OR_EQUALS_ZERO = "Must be greater or equals to 0"
|
||||
const val SIZE_GREATER_OR_EQUALS_ONE = "Must be greater or equals to 1"
|
||||
const val RANGE_OUTSIDE_PERCENTS = "Must be between 0 and 100"
|
||||
const val PASSWORD_TOO_SMALL = "Must contains at least 8 characters"
|
||||
}
|
||||
|
||||
object ValidationRegexes {
|
||||
const val VALIDATION_COLOR_PATTERN = "^#([0-9a-f]{6})$"
|
||||
}
|
||||
}
|
@ -3,79 +3,35 @@ package dev.fyloz.colorrecipesexplorer
|
||||
import dev.fyloz.colorrecipesexplorer.databasemanager.CreDatabase
|
||||
import dev.fyloz.colorrecipesexplorer.databasemanager.databaseContext
|
||||
import dev.fyloz.colorrecipesexplorer.databasemanager.databaseUpdaterProperties
|
||||
import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic
|
||||
import dev.fyloz.colorrecipesexplorer.model.ConfigurationType
|
||||
import mu.KotlinLogging
|
||||
import org.slf4j.Logger
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.boot.jdbc.DataSourceBuilder
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.DependsOn
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.core.env.ConfigurableEnvironment
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.core.env.Environment
|
||||
import javax.sql.DataSource
|
||||
import org.springframework.context.annotation.Configuration as SpringConfiguration
|
||||
|
||||
const val SUPPORTED_DATABASE_VERSION = 6
|
||||
const val SUPPORTED_DATABASE_VERSION = 5
|
||||
const val ENV_VAR_ENABLE_DATABASE_UPDATE_NAME = "CRE_ENABLE_DB_UPDATE"
|
||||
val DATABASE_NAME_REGEX = Regex("(\\w+)$")
|
||||
|
||||
@Profile("!emergency")
|
||||
@SpringConfiguration
|
||||
@DependsOn("configurationsInitializer", "configurationService")
|
||||
@Configuration
|
||||
class DataSourceConfiguration {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@Bean(name = ["dataSource"])
|
||||
@ConfigurationProperties(prefix = "spring.datasource")
|
||||
fun customDataSource(
|
||||
environment: ConfigurableEnvironment,
|
||||
configurationService: ConfigurationLogic
|
||||
logger: Logger,
|
||||
environment: Environment,
|
||||
databaseUpdaterProperties: DatabaseUpdaterProperties
|
||||
): DataSource {
|
||||
fun getConfiguration(type: ConfigurationType) =
|
||||
if (type.secure) configurationService.getSecure(type)
|
||||
else configurationService.getContent(type)
|
||||
val databaseUrl: String = environment.getProperty("spring.datasource.url")!!
|
||||
|
||||
val databaseUrl = "jdbc:" + getConfiguration(ConfigurationType.DATABASE_URL)
|
||||
val databaseUsername = getConfiguration(ConfigurationType.DATABASE_USER)
|
||||
val databasePassword = getConfiguration(ConfigurationType.DATABASE_PASSWORD)
|
||||
|
||||
try {
|
||||
runDatabaseVersionCheck(logger, databaseUrl, DatabaseUpdaterProperties().apply {
|
||||
url = databaseUrl
|
||||
username = databaseUsername
|
||||
password = databasePassword
|
||||
})
|
||||
} catch (ex: Exception) {
|
||||
logger.error("Could not access database, restarting in emergency mode...", ex)
|
||||
emergencyMode = true
|
||||
|
||||
return emergencyDataSource()
|
||||
}
|
||||
runDatabaseVersionCheck(logger, databaseUrl, databaseUpdaterProperties)
|
||||
|
||||
return DataSourceBuilder
|
||||
.create()
|
||||
.url(databaseUrl)
|
||||
.username(databaseUsername)
|
||||
.password(databasePassword)
|
||||
.driverClassName(getDriverClassName(databaseUrl))
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun emergencyDataSource() = with("jdbc:h2:mem:emergency") {
|
||||
DataSourceBuilder
|
||||
.create()
|
||||
.url(this)
|
||||
.driverClassName(getDriverClassName(this))
|
||||
.username("sa")
|
||||
.password("")
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun getDriverClassName(url: String) = when {
|
||||
url.startsWith("jdbc:postgres") -> "org.postgresql.Driver"
|
||||
url.startsWith("jdbc:mssql") -> "com.microsoft.sqlserver.jdbc.SQLServerDriver"
|
||||
url.startsWith("jdbc:mysql") -> "com.mysql.cj.jdbc.Driver"
|
||||
url.startsWith("jdbc:h2") -> "org.h2.Driver"
|
||||
else -> "org.h2.Driver"
|
||||
.create()
|
||||
.url(databaseUrl) // Hikari won't start without that
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,24 +75,24 @@ fun runDatabaseUpdate(logger: Logger, database: CreDatabase) {
|
||||
}
|
||||
|
||||
fun getDatabase(
|
||||
databaseUrl: String,
|
||||
databaseUpdaterProperties: DatabaseUpdaterProperties,
|
||||
logger: Logger
|
||||
databaseUrl: String,
|
||||
databaseUpdaterProperties: DatabaseUpdaterProperties,
|
||||
logger: Logger
|
||||
): CreDatabase {
|
||||
val databaseName =
|
||||
(DATABASE_NAME_REGEX.find(databaseUrl) ?: throw DatabaseVersioningException.InvalidUrl(databaseUrl)).value
|
||||
(DATABASE_NAME_REGEX.find(databaseUrl) ?: throw DatabaseVersioningException.InvalidUrl(databaseUrl)).value
|
||||
|
||||
return CreDatabase(
|
||||
databaseContext(
|
||||
properties = databaseUpdaterProperties(
|
||||
targetVersion = SUPPORTED_DATABASE_VERSION,
|
||||
url = databaseUrl.removeSuffix(databaseName),
|
||||
dbName = databaseName,
|
||||
username = databaseUpdaterProperties.username,
|
||||
password = databaseUpdaterProperties.password
|
||||
),
|
||||
logger
|
||||
)
|
||||
databaseContext(
|
||||
properties = databaseUpdaterProperties(
|
||||
targetVersion = SUPPORTED_DATABASE_VERSION,
|
||||
url = databaseUrl.removeSuffix(databaseName),
|
||||
dbName = databaseName,
|
||||
username = databaseUpdaterProperties.username,
|
||||
password = databaseUpdaterProperties.password
|
||||
),
|
||||
logger
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -145,7 +101,7 @@ fun throwUnsupportedDatabaseVersion(version: Int, logger: Logger) {
|
||||
logger.error("Version $version of the database is not supported; Only version $SUPPORTED_DATABASE_VERSION is currently supported; Update this application to use the database.")
|
||||
} else {
|
||||
logger.error(
|
||||
"""Version $version of the database is not supported; Only version $SUPPORTED_DATABASE_VERSION is currently supported.
|
||||
"""Version $version of the database is not supported; Only version $SUPPORTED_DATABASE_VERSION is currently supported.
|
||||
|You can update the database to the supported version by either:
|
||||
| - Setting the environment variable '$ENV_VAR_ENABLE_DATABASE_UPDATE_NAME' to '1' to update the database automatically
|
||||
| - Updating the database manually with the database manager utility (https://git.fyloz.dev/color-recipes-explorer/database-manager)
|
||||
@ -157,8 +113,8 @@ fun throwUnsupportedDatabaseVersion(version: Int, logger: Logger) {
|
||||
throw DatabaseVersioningException.UnsupportedDatabaseVersion(version)
|
||||
}
|
||||
|
||||
@ConfigurationProperties(prefix = "databaseupdater")
|
||||
class DatabaseUpdaterProperties {
|
||||
var url: String = ""
|
||||
var username: String = ""
|
||||
var password: String = ""
|
||||
}
|
||||
@ -166,5 +122,5 @@ class DatabaseUpdaterProperties {
|
||||
sealed class DatabaseVersioningException(message: String) : Exception(message) {
|
||||
class InvalidUrl(url: String) : DatabaseVersioningException("Invalid database url: $url")
|
||||
class UnsupportedDatabaseVersion(version: Int) :
|
||||
DatabaseVersioningException("Unsupported database version: $version; Only version $SUPPORTED_DATABASE_VERSION is currently supported")
|
||||
DatabaseVersioningException("Unsupported database version: $version; Only version $SUPPORTED_DATABASE_VERSION is currently supported")
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer
|
||||
|
||||
typealias SpringUser = org.springframework.security.core.userdetails.User
|
||||
typealias SpringUserDetails = org.springframework.security.core.userdetails.UserDetails
|
||||
typealias SpringUserDetailsService = org.springframework.security.core.userdetails.UserDetailsService
|
||||
|
||||
typealias JavaFile = java.io.File
|
@ -1,64 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.config
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
|
||||
import dev.fyloz.colorrecipesexplorer.config.initializers.AbstractInitializer
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import dev.fyloz.colorrecipesexplorer.emergencyMode
|
||||
import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic
|
||||
import dev.fyloz.colorrecipesexplorer.rest.CRE_PROPERTIES
|
||||
import dev.fyloz.colorrecipesexplorer.restartApplication
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent
|
||||
import org.springframework.context.ApplicationListener
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.core.Ordered
|
||||
import org.springframework.core.annotation.Order
|
||||
import javax.annotation.PostConstruct
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
@Configuration
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
@RequireDatabase
|
||||
class ApplicationReadyListener(
|
||||
private val configurationLogic: ConfigurationLogic,
|
||||
private val creProperties: CreProperties
|
||||
) : AbstractInitializer() {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
override fun initialize() {
|
||||
if (emergencyMode) {
|
||||
logger.error("Emergency mode is enabled, default material types will not be created")
|
||||
thread {
|
||||
Thread.sleep(1000)
|
||||
logger.warn("Restarting in emergency mode...")
|
||||
restartApplication(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
initDatabaseConfigurations()
|
||||
CRE_PROPERTIES = creProperties
|
||||
}
|
||||
|
||||
private fun initDatabaseConfigurations() {
|
||||
configurationLogic.initializeProperties { !it.file }
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration("configurationsInitializer")
|
||||
class ConfigurationsInitializer(
|
||||
private val configurationLogic: ConfigurationLogic
|
||||
) {
|
||||
@PostConstruct
|
||||
fun initializeFileConfigurations() {
|
||||
configurationLogic.initializeProperties { it.file }
|
||||
}
|
||||
}
|
||||
|
||||
class ApplicationInitializer : ApplicationListener<ApplicationEnvironmentPreparedEvent> {
|
||||
override fun onApplicationEvent(event: ApplicationEnvironmentPreparedEvent) {
|
||||
if (emergencyMode) {
|
||||
event.environment.setActiveProfiles("emergency")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package dev.fyloz.colorrecipesexplorer.config
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.MaterialTypeProperties
|
||||
import dev.fyloz.colorrecipesexplorer.rest.CRE_PROPERTIES
|
||||
import dev.fyloz.colorrecipesexplorer.service.MaterialTypeService
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.context.ApplicationListener
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.core.Ordered
|
||||
import org.springframework.core.annotation.Order
|
||||
|
||||
@Configuration
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
class ApplicationReadyListener(
|
||||
private val materialTypeService: MaterialTypeService,
|
||||
private val materialTypeProperties: MaterialTypeProperties,
|
||||
private val creProperties: CreProperties
|
||||
) : ApplicationListener<ApplicationReadyEvent> {
|
||||
override fun onApplicationEvent(event: ApplicationReadyEvent) {
|
||||
materialTypeService.saveSystemTypes(materialTypeProperties.systemTypes)
|
||||
CRE_PROPERTIES = creProperties
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.config
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.MaterialTypeProperties
|
||||
import dev.fyloz.colorrecipesexplorer.logic.files.CachedFileSystemItem
|
||||
import dev.fyloz.memorycache.ExpiringMemoryCache
|
||||
import dev.fyloz.memorycache.MemoryCache
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(MaterialTypeProperties::class, CreProperties::class)
|
||||
class CreConfiguration(private val creProperties: CreProperties) {
|
||||
@Bean
|
||||
fun fileMemoryCache(): MemoryCache<String, CachedFileSystemItem> =
|
||||
ExpiringMemoryCache(maxAccessCount = creProperties.fileCacheMaxAccessCount)
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package dev.fyloz.colorrecipesexplorer.config
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.ColorRecipesExplorerApplication
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
@Configuration
|
||||
class SpringConfiguration {
|
||||
@Bean
|
||||
fun logger(): Logger = LoggerFactory.getLogger(ColorRecipesExplorerApplication::class.java)
|
||||
}
|
@ -0,0 +1,288 @@
|
||||
package dev.fyloz.colorrecipesexplorer.config
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.UserLoginRequest
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.User
|
||||
import dev.fyloz.colorrecipesexplorer.service.UserService
|
||||
import dev.fyloz.colorrecipesexplorer.service.UserServiceImpl
|
||||
import dev.fyloz.colorrecipesexplorer.service.CreUserDetailsService
|
||||
import dev.fyloz.colorrecipesexplorer.service.CreUserDetailsServiceImpl
|
||||
import io.jsonwebtoken.ExpiredJwtException
|
||||
import io.jsonwebtoken.Jwts
|
||||
import io.jsonwebtoken.SignatureAlgorithm
|
||||
import org.slf4j.Logger
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.core.env.Environment
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.security.authentication.AuthenticationManager
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
|
||||
import org.springframework.security.config.http.SessionCreationPolicy
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.core.AuthenticationException
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
import org.springframework.security.core.userdetails.User as SpringUser
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.security.web.AuthenticationEntryPoint
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.util.Assert
|
||||
import org.springframework.web.cors.CorsConfiguration
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
|
||||
import org.springframework.web.util.WebUtils
|
||||
import java.util.*
|
||||
import javax.annotation.PostConstruct
|
||||
import javax.servlet.FilterChain
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
||||
@EnableConfigurationProperties(SecurityConfigurationProperties::class)
|
||||
class WebSecurityConfig(
|
||||
val securityConfigurationProperties: SecurityConfigurationProperties,
|
||||
@Lazy val userDetailsService: CreUserDetailsServiceImpl,
|
||||
@Lazy val userService: UserServiceImpl,
|
||||
val environment: Environment,
|
||||
val logger: Logger
|
||||
) : WebSecurityConfigurerAdapter() {
|
||||
var debugMode = false
|
||||
|
||||
override fun configure(authBuilder: AuthenticationManagerBuilder) {
|
||||
authBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder())
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun passwordEncoder() =
|
||||
BCryptPasswordEncoder()
|
||||
|
||||
@Bean
|
||||
override fun authenticationManagerBean(): AuthenticationManager =
|
||||
super.authenticationManagerBean()
|
||||
|
||||
@Bean
|
||||
fun corsConfigurationSource() =
|
||||
UrlBasedCorsConfigurationSource().apply {
|
||||
registerCorsConfiguration("/**", CorsConfiguration().apply {
|
||||
allowedOrigins = listOf("http://localhost:4200") // Angular development server
|
||||
allowedMethods = listOf(
|
||||
HttpMethod.GET.name,
|
||||
HttpMethod.POST.name,
|
||||
HttpMethod.PUT.name,
|
||||
HttpMethod.DELETE.name,
|
||||
HttpMethod.OPTIONS.name,
|
||||
HttpMethod.HEAD.name
|
||||
)
|
||||
allowCredentials = true
|
||||
}.applyPermitDefaultValues())
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
fun initWebSecurity() {
|
||||
fun createUser(
|
||||
credentials: SecurityConfigurationProperties.SystemUserCredentials?,
|
||||
firstName: String,
|
||||
lastName: String,
|
||||
permissions: List<Permission>
|
||||
) {
|
||||
Assert.notNull(credentials, "No root user has been defined.")
|
||||
credentials!!
|
||||
Assert.notNull(credentials.id, "The root user has no identifier defined.")
|
||||
Assert.notNull(credentials.password, "The root user has no password defined.")
|
||||
if (!userService.existsById(credentials.id!!)) {
|
||||
userService.save(
|
||||
User(
|
||||
id = credentials.id!!,
|
||||
firstName = firstName,
|
||||
lastName = lastName,
|
||||
password = passwordEncoder().encode(credentials.password!!),
|
||||
isSystemUser = true,
|
||||
permissions = permissions.toMutableSet()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
createUser(securityConfigurationProperties.root, "Root", "User", listOf(Permission.ADMIN))
|
||||
debugMode = "debug" in environment.activeProfiles
|
||||
if (debugMode) logger.warn("Debug mode is enabled, security will be disabled!")
|
||||
}
|
||||
|
||||
override fun configure(http: HttpSecurity) {
|
||||
http
|
||||
.headers().frameOptions().disable()
|
||||
.and()
|
||||
.csrf().disable()
|
||||
.addFilter(
|
||||
JwtAuthenticationFilter(
|
||||
authenticationManager(),
|
||||
userService,
|
||||
securityConfigurationProperties
|
||||
)
|
||||
)
|
||||
.addFilter(
|
||||
JwtAuthorizationFilter(
|
||||
userDetailsService,
|
||||
securityConfigurationProperties,
|
||||
authenticationManager()
|
||||
)
|
||||
)
|
||||
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
|
||||
if (!debugMode) {
|
||||
http.authorizeRequests()
|
||||
.antMatchers("/api/login").permitAll()
|
||||
.antMatchers("/api/logout").authenticated()
|
||||
.antMatchers("/api/user/current").authenticated()
|
||||
.anyRequest().authenticated()
|
||||
} else {
|
||||
http
|
||||
.cors()
|
||||
.and()
|
||||
.authorizeRequests()
|
||||
.antMatchers("**").permitAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
class RestAuthenticationEntryPoint : AuthenticationEntryPoint {
|
||||
override fun commence(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
authException: AuthenticationException
|
||||
) = response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized")
|
||||
}
|
||||
|
||||
const val authorizationCookieName = "Authorization"
|
||||
const val defaultGroupCookieName = "Default-Group"
|
||||
val blacklistedJwtTokens = mutableListOf<String>()
|
||||
|
||||
class JwtAuthenticationFilter(
|
||||
private val authManager: AuthenticationManager,
|
||||
private val userService: UserService,
|
||||
private val securityConfigurationProperties: SecurityConfigurationProperties
|
||||
) : UsernamePasswordAuthenticationFilter() {
|
||||
private var debugMode = false
|
||||
|
||||
init {
|
||||
setFilterProcessesUrl("/api/login")
|
||||
debugMode = "debug" in environment.activeProfiles
|
||||
}
|
||||
|
||||
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
|
||||
val loginRequest = jacksonObjectMapper().readValue(request.inputStream, UserLoginRequest::class.java)
|
||||
return authManager.authenticate(UsernamePasswordAuthenticationToken(loginRequest.id, loginRequest.password))
|
||||
}
|
||||
|
||||
override fun successfulAuthentication(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
chain: FilterChain,
|
||||
authResult: Authentication
|
||||
) {
|
||||
val jwtSecret = securityConfigurationProperties.jwtSecret
|
||||
val jwtDuration = securityConfigurationProperties.jwtDuration
|
||||
Assert.notNull(jwtSecret, "No JWT secret has been defined.")
|
||||
Assert.notNull(jwtDuration, "No JWT duration has been defined.")
|
||||
val userId = (authResult.principal as SpringUser).username
|
||||
userService.updateLastLoginTime(userId.toLong())
|
||||
val expirationMs = System.currentTimeMillis() + jwtDuration!!
|
||||
val expirationDate = Date(expirationMs)
|
||||
val token = Jwts.builder()
|
||||
.setSubject(userId)
|
||||
.setExpiration(expirationDate)
|
||||
.signWith(SignatureAlgorithm.HS512, jwtSecret!!.toByteArray())
|
||||
.compact()
|
||||
response.addHeader("Access-Control-Expose-Headers", "X-Authentication-Expiration")
|
||||
var bearerCookie =
|
||||
"$authorizationCookieName=Bearer$token; Max-Age=${jwtDuration / 1000}; HttpOnly; SameSite=strict"
|
||||
if (!debugMode) bearerCookie += "; Secure;"
|
||||
response.addHeader(
|
||||
"Set-Cookie",
|
||||
bearerCookie
|
||||
)
|
||||
response.addHeader(authorizationCookieName, "Bearer $token")
|
||||
response.addHeader("X-Authentication-Expiration", "$expirationMs")
|
||||
}
|
||||
}
|
||||
|
||||
class JwtAuthorizationFilter(
|
||||
private val userDetailsService: CreUserDetailsService,
|
||||
private val securityConfigurationProperties: SecurityConfigurationProperties,
|
||||
authenticationManager: AuthenticationManager
|
||||
) : BasicAuthenticationFilter(authenticationManager) {
|
||||
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
|
||||
fun tryLoginFromBearer(): Boolean {
|
||||
val authorizationCookie = WebUtils.getCookie(request, authorizationCookieName)
|
||||
// Check for an authorization token cookie or header
|
||||
val authorizationToken = if (authorizationCookie != null)
|
||||
authorizationCookie.value
|
||||
else
|
||||
request.getHeader(authorizationCookieName)
|
||||
|
||||
// An authorization token is valid if it starts with "Bearer", is not expired and is not blacklisted
|
||||
if (authorizationToken != null && authorizationToken.startsWith("Bearer") && authorizationToken !in blacklistedJwtTokens) {
|
||||
val authenticationToken = getAuthentication(authorizationToken) ?: return false
|
||||
SecurityContextHolder.getContext().authentication = authenticationToken
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun tryLoginFromDefaultGroupCookie() {
|
||||
val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName)
|
||||
if (defaultGroupCookie != null) {
|
||||
val authenticationToken = getAuthenticationToken(defaultGroupCookie.value)
|
||||
SecurityContextHolder.getContext().authentication = authenticationToken
|
||||
}
|
||||
}
|
||||
|
||||
if (!tryLoginFromBearer())
|
||||
tryLoginFromDefaultGroupCookie()
|
||||
chain.doFilter(request, response)
|
||||
}
|
||||
|
||||
private fun getAuthentication(token: String): UsernamePasswordAuthenticationToken? {
|
||||
val jwtSecret = securityConfigurationProperties.jwtSecret
|
||||
Assert.notNull(jwtSecret, "No JWT secret has been defined.")
|
||||
return try {
|
||||
val userId = Jwts.parser()
|
||||
.setSigningKey(jwtSecret!!.toByteArray())
|
||||
.parseClaimsJws(token.replace("Bearer", ""))
|
||||
.body
|
||||
.subject
|
||||
if (userId != null) getAuthenticationToken(userId) else null
|
||||
} catch (_: ExpiredJwtException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAuthenticationToken(userId: String): UsernamePasswordAuthenticationToken? = try {
|
||||
val userDetails = userDetailsService.loadUserById(userId.toLong(), false)
|
||||
UsernamePasswordAuthenticationToken(userDetails.username, null, userDetails.authorities)
|
||||
} catch (_: NotFoundException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@ConfigurationProperties("cre.security")
|
||||
class SecurityConfigurationProperties {
|
||||
var jwtSecret: String? = null
|
||||
var jwtDuration: Long? = null
|
||||
var root: SystemUserCredentials? = null
|
||||
|
||||
class SystemUserCredentials(var id: Long? = null, var password: String? = null)
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.config.annotations
|
||||
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
@RequireDatabase
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class ServiceComponent
|
||||
|
||||
@Service
|
||||
@RequireDatabase
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class LogicComponent
|
@ -1,10 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.config.annotations
|
||||
|
||||
import org.springframework.context.annotation.Profile
|
||||
import java.lang.annotation.Inherited
|
||||
|
||||
@Profile("!emergency")
|
||||
@Inherited
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class RequireDatabase
|
@ -1,12 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.config.initializers
|
||||
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.context.ApplicationListener
|
||||
|
||||
abstract class AbstractInitializer : ApplicationListener<ApplicationReadyEvent> {
|
||||
override fun onApplicationEvent(event: ApplicationReadyEvent) {
|
||||
initialize()
|
||||
}
|
||||
|
||||
protected abstract fun initialize()
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.config.initializers
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.MaterialTypeProperties
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto
|
||||
import dev.fyloz.colorrecipesexplorer.logic.MaterialTypeLogic
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
@Configuration
|
||||
@RequireDatabase
|
||||
class MaterialTypeInitializer(
|
||||
private val materialTypeLogic: MaterialTypeLogic,
|
||||
private val materialTypeProperties: MaterialTypeProperties
|
||||
) : AbstractInitializer() {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
override fun initialize() {
|
||||
logger.debug("Executing material type initializer...")
|
||||
ensureSystemMaterialTypesExists()
|
||||
logger.debug("System material types are up to date!")
|
||||
}
|
||||
|
||||
private fun ensureSystemMaterialTypesExists() {
|
||||
val systemTypes = materialTypeProperties.systemTypes.map { it.toMaterialType() }
|
||||
val oldSystemTypes = materialTypeLogic.getAll(true).toMutableSet()
|
||||
|
||||
fun saveOrUpdateSystemType(type: MaterialTypeDto) {
|
||||
val storedMaterialType = materialTypeLogic.getByName(type.name)
|
||||
if (storedMaterialType != null) {
|
||||
if (!storedMaterialType.systemType) {
|
||||
logger.info("Material type '${type.name}' already exists and will be flagged as a system type")
|
||||
materialTypeLogic.update(storedMaterialType.copy(systemType = true))
|
||||
} else {
|
||||
logger.debug("System material type '${type.name}' already exists")
|
||||
}
|
||||
} else {
|
||||
logger.info("System material type '${type.name}' will be created")
|
||||
materialTypeLogic.save(type)
|
||||
}
|
||||
}
|
||||
|
||||
// Save new system types
|
||||
systemTypes.forEach {
|
||||
saveOrUpdateSystemType(it)
|
||||
oldSystemTypes.removeIf { c -> c.name == it.name }
|
||||
}
|
||||
|
||||
// Remove old system types
|
||||
oldSystemTypes.forEach {
|
||||
logger.info("Material type '${it.name}' is not a system type anymore")
|
||||
materialTypeLogic.updateNonSystemType(it.copy(systemType = false))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.config.initializers
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.*
|
||||
import dev.fyloz.colorrecipesexplorer.logic.MixLogic
|
||||
import dev.fyloz.colorrecipesexplorer.utils.merge
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import java.util.*
|
||||
|
||||
@Configuration
|
||||
@RequireDatabase
|
||||
class MixInitializer(
|
||||
private val mixLogic: MixLogic
|
||||
) : AbstractInitializer() {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
override fun initialize() {
|
||||
logger.debug("Executing mix initializer...")
|
||||
fixAllPositions()
|
||||
}
|
||||
|
||||
private fun fixAllPositions() {
|
||||
logger.debug("Validating mix materials positions...")
|
||||
|
||||
mixLogic.getAll()
|
||||
.filter { it.mixQuantities.all.any { mq -> mq.position == 0 } }
|
||||
.forEach(this::fixMixPositions)
|
||||
|
||||
logger.debug("Mix materials positions are valid!")
|
||||
}
|
||||
|
||||
private fun fixMixPositions(mix: MixDto) {
|
||||
val mixQuantities = mix.mixQuantitiesOutput
|
||||
val maxPosition = mixQuantities.maxOf { it.position }
|
||||
|
||||
logger.warn("Mix ${mix.id} (mix name: ${mix.mixType.name}, recipe id: ${mix.recipeId}) has invalid positions:")
|
||||
|
||||
val invalidMixQuantities: Collection<MixQuantityOutputDto> = with(mixQuantities.filter { it.position == 0 }) {
|
||||
if (maxPosition == 0 && this.size > 1) {
|
||||
orderMixMaterials(this)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
val fixedMixQuantities = increaseMixMaterialsPosition(invalidMixQuantities, maxPosition + 1)
|
||||
val updatedMixQuantities =
|
||||
mixQuantities.map { MixQuantitySaveDto(it.id, it.material.id, it.quantity, it.position, it.isMixType) }
|
||||
.merge(fixedMixQuantities)
|
||||
|
||||
val updatedMix = MixSaveDto(mix.id, mix.mixType.name, mix.recipeId, mix.mixType.materialType.id, updatedMixQuantities)
|
||||
mixLogic.update(updatedMix)
|
||||
}
|
||||
|
||||
private fun increaseMixMaterialsPosition(mixQuantities: Iterable<MixQuantityOutputDto>, firstPosition: Int) =
|
||||
mixQuantities
|
||||
.mapIndexed { index, mixQuantity ->
|
||||
MixQuantitySaveDto(
|
||||
mixQuantity.id,
|
||||
mixQuantity.material.id,
|
||||
mixQuantity.quantity,
|
||||
firstPosition + index,
|
||||
mixQuantity.isMixType
|
||||
)
|
||||
}
|
||||
.onEach {
|
||||
logger.info("\tPosition of material ${it.id} (mixType: ${it.isMixType}) has been set to ${it.position}")
|
||||
}
|
||||
|
||||
private fun orderMixMaterials(mixQuantities: Collection<MixQuantityOutputDto>) =
|
||||
LinkedList(mixQuantities).apply {
|
||||
while (this.peek().material.materialType.usePercentages) {
|
||||
// The first mix material can't use percents, so move it to the end of the queue
|
||||
val pop = this.pop()
|
||||
this.add(pop)
|
||||
logger.debug("\tMaterial ${pop.id} (mixType: ${pop.isMixType}) uses percents, moving to the end of the queue")
|
||||
}
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.config.initializers
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeGroupInformationDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto
|
||||
import dev.fyloz.colorrecipesexplorer.logic.RecipeLogic
|
||||
import dev.fyloz.colorrecipesexplorer.utils.merge
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
@Configuration
|
||||
@RequireDatabase
|
||||
class RecipeInitializer(
|
||||
private val recipeLogic: RecipeLogic
|
||||
) : AbstractInitializer() {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
override fun initialize() {
|
||||
logger.debug("Executing recipe initializer...")
|
||||
fixAllPositions()
|
||||
}
|
||||
|
||||
private fun fixAllPositions() {
|
||||
logger.debug("Validating recipes steps positions...")
|
||||
|
||||
recipeLogic.getAllWithMixesAndGroupsInformation()
|
||||
.forEach(this::fixRecipePositions)
|
||||
|
||||
logger.debug("Recipes steps positions are valid!")
|
||||
}
|
||||
|
||||
private fun fixRecipePositions(recipe: RecipeDto) {
|
||||
val fixedGroupInformation = recipe.groupsInformation
|
||||
.filter { groupInfo -> groupInfo.steps.any { it.position == 0 } }
|
||||
.map { fixGroupInformationPositions(recipe, it) }
|
||||
|
||||
val updatedGroupInformation = recipe.groupsInformation.merge(fixedGroupInformation) { it.id }
|
||||
|
||||
with(recipe.copy(groupsInformation = updatedGroupInformation)) {
|
||||
recipeLogic.update(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fixGroupInformationPositions(
|
||||
recipe: RecipeDto,
|
||||
groupInformation: RecipeGroupInformationDto
|
||||
): RecipeGroupInformationDto {
|
||||
val steps = groupInformation.steps
|
||||
val maxPosition = steps.maxOf { it.position }
|
||||
|
||||
logger.warn("Recipe ${recipe.id} (${recipe.name}) has invalid positions:")
|
||||
|
||||
val invalidRecipeSteps = steps.filter { it.position == 0 }
|
||||
val fixedRecipeSteps = increaseRecipeStepsPosition(groupInformation, invalidRecipeSteps, maxPosition + 1)
|
||||
val updatedRecipeSteps = steps.merge(fixedRecipeSteps) { it.id }
|
||||
|
||||
return groupInformation.copy(steps = updatedRecipeSteps)
|
||||
}
|
||||
|
||||
private fun increaseRecipeStepsPosition(
|
||||
groupInformation: RecipeGroupInformationDto,
|
||||
recipeSteps: Iterable<RecipeStepDto>,
|
||||
firstPosition: Int
|
||||
) =
|
||||
recipeSteps
|
||||
.mapIndexed { index, recipeStep -> recipeStep.copy(position = firstPosition + index) }
|
||||
.onEach {
|
||||
logger.info("\tPosition of step ${it.id} (group: ${groupInformation.group.name}) has been set to ${it.position}")
|
||||
}
|
||||
}
|
@ -1,33 +1,10 @@
|
||||
package dev.fyloz.colorrecipesexplorer.config.properties
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import kotlin.properties.Delegates.notNull
|
||||
|
||||
const val DEFAULT_DATA_DIRECTORY = "data"
|
||||
const val DEFAULT_CONFIG_DIRECTORY = "config"
|
||||
const val DEFAULT_FILE_CACHE_MAX_ACCESS_COUNT = 10_000L
|
||||
|
||||
@ConfigurationProperties(prefix = "cre.server")
|
||||
class CreProperties {
|
||||
var dataDirectory: String = DEFAULT_DATA_DIRECTORY
|
||||
var configDirectory: String = DEFAULT_CONFIG_DIRECTORY
|
||||
var fileCacheMaxAccessCount: Long = DEFAULT_FILE_CACHE_MAX_ACCESS_COUNT
|
||||
}
|
||||
|
||||
@ConfigurationProperties(prefix = "cre.security")
|
||||
class CreSecurityProperties {
|
||||
// JWT
|
||||
var jwtSecret by notNull<String>()
|
||||
var jwtDuration by notNull<Long>()
|
||||
|
||||
// Configs
|
||||
var configSalt: String? = null
|
||||
|
||||
// Users
|
||||
var root: SystemUserCredentials? = null
|
||||
|
||||
class SystemUserCredentials{
|
||||
var id by notNull<Long>()
|
||||
var password by notNull<String>()
|
||||
}
|
||||
var workingDirectory: String = "data"
|
||||
var deploymentUrl: String = "http://localhost"
|
||||
var cacheGeneratedFiles: Boolean = false
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package dev.fyloz.colorrecipesexplorer.config.properties
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.MaterialType
|
||||
import dev.fyloz.colorrecipesexplorer.model.materialType
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.util.Assert
|
||||
@ -9,15 +10,16 @@ import org.springframework.util.Assert
|
||||
@ConfigurationProperties(prefix = "entities.material-types")
|
||||
class MaterialTypeProperties {
|
||||
var systemTypes: MutableList<MaterialTypeProperty> = mutableListOf()
|
||||
var baseName: String = ""
|
||||
|
||||
data class MaterialTypeProperty(
|
||||
var name: String = "",
|
||||
var prefix: String = "",
|
||||
var usePercentages: Boolean = false
|
||||
) {
|
||||
fun toMaterialType(): MaterialTypeDto {
|
||||
fun toMaterialType(): MaterialType {
|
||||
Assert.hasText(name, "A system material type has an empty name")
|
||||
return MaterialTypeDto(name = name, prefix = prefix, usePercentages = usePercentages, systemType = true)
|
||||
return materialType(name = name, prefix = prefix, usePercentages = usePercentages, systemType = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,130 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.config.security
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserLoginRequestDto
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.JwtLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.UserDetailsLogic
|
||||
import dev.fyloz.colorrecipesexplorer.utils.addCookie
|
||||
import io.jsonwebtoken.ExpiredJwtException
|
||||
import org.springframework.security.authentication.AuthenticationManager
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter
|
||||
import org.springframework.web.util.WebUtils
|
||||
import javax.servlet.FilterChain
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
const val authorizationCookieName = "Authorization"
|
||||
const val defaultGroupCookieName = "Default-Group"
|
||||
val blacklistedJwtTokens = mutableListOf<String>() // Not working, move to a cache or something
|
||||
|
||||
class JwtAuthenticationFilter(
|
||||
private val authManager: AuthenticationManager,
|
||||
private val jwtLogic: JwtLogic,
|
||||
private val securityProperties: CreSecurityProperties,
|
||||
private val updateUserLoginTime: (Long) -> Unit
|
||||
) : UsernamePasswordAuthenticationFilter() {
|
||||
private var debugMode = false
|
||||
|
||||
init {
|
||||
setFilterProcessesUrl("/api/login")
|
||||
debugMode = "debug" in environment.activeProfiles
|
||||
}
|
||||
|
||||
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
|
||||
val loginRequest = jacksonObjectMapper().readValue(request.inputStream, UserLoginRequestDto::class.java)
|
||||
logger.debug("Login attempt for user ${loginRequest.id}...")
|
||||
return authManager.authenticate(UsernamePasswordAuthenticationToken(loginRequest.id, loginRequest.password))
|
||||
}
|
||||
|
||||
override fun successfulAuthentication(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
chain: FilterChain,
|
||||
auth: Authentication
|
||||
) {
|
||||
val userDetails = auth.principal as UserDetails
|
||||
val token = jwtLogic.buildJwt(userDetails)
|
||||
|
||||
with(userDetails.user) {
|
||||
logger.info("User ${this.id} (${this.firstName} ${this.lastName}) has logged in successfully")
|
||||
}
|
||||
|
||||
response.addHeader("Access-Control-Expose-Headers", authorizationCookieName)
|
||||
response.addHeader(authorizationCookieName, "Bearer $token")
|
||||
response.addCookie(authorizationCookieName, "Bearer$token") {
|
||||
httpOnly = true
|
||||
sameSite = true
|
||||
secure = !debugMode
|
||||
maxAge = securityProperties.jwtDuration / 1000
|
||||
}
|
||||
|
||||
updateUserLoginTime(userDetails.user.id)
|
||||
}
|
||||
}
|
||||
|
||||
class JwtAuthorizationFilter(
|
||||
private val jwtLogic: JwtLogic,
|
||||
authenticationManager: AuthenticationManager,
|
||||
private val userDetailsLogic: UserDetailsLogic
|
||||
) : BasicAuthenticationFilter(authenticationManager) {
|
||||
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
|
||||
fun tryLoginFromBearer(): Boolean {
|
||||
val authorizationCookie = WebUtils.getCookie(request, authorizationCookieName)
|
||||
// Check for an authorization token cookie or header
|
||||
val authorizationToken = if (authorizationCookie != null)
|
||||
authorizationCookie.value
|
||||
else
|
||||
request.getHeader(authorizationCookieName)
|
||||
|
||||
// An authorization token is valid if it starts with "Bearer", is not expired and is not blacklisted
|
||||
if (authorizationToken != null && authorizationToken.startsWith("Bearer") && authorizationToken !in blacklistedJwtTokens) {
|
||||
val authenticationToken = getAuthentication(authorizationToken) ?: return false
|
||||
SecurityContextHolder.getContext().authentication = authenticationToken
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun tryLoginFromDefaultGroupCookie() {
|
||||
val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName)
|
||||
if (defaultGroupCookie != null) {
|
||||
val authenticationToken = getAuthenticationToken(defaultGroupCookie.value)
|
||||
SecurityContextHolder.getContext().authentication = authenticationToken
|
||||
}
|
||||
}
|
||||
|
||||
if (!tryLoginFromBearer())
|
||||
tryLoginFromDefaultGroupCookie()
|
||||
chain.doFilter(request, response)
|
||||
}
|
||||
|
||||
private fun getAuthentication(token: String): UsernamePasswordAuthenticationToken? {
|
||||
return try {
|
||||
val user = jwtLogic.parseJwt(token.replace("Bearer", ""))
|
||||
getAuthenticationToken(user)
|
||||
} catch (_: ExpiredJwtException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAuthenticationToken(user: UserDto) =
|
||||
UsernamePasswordAuthenticationToken(user.id, null, user.authorities)
|
||||
|
||||
private fun getAuthenticationToken(userId: Long): UsernamePasswordAuthenticationToken? = try {
|
||||
val userDetails = userDetailsLogic.loadUserById(userId)
|
||||
UsernamePasswordAuthenticationToken(userDetails.username, null, userDetails.authorities)
|
||||
} catch (_: NotFoundException) {
|
||||
null
|
||||
}
|
||||
|
||||
private fun getAuthenticationToken(userId: String) =
|
||||
getAuthenticationToken(userId.toLong())
|
||||
}
|
@ -1,191 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.config.security
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.emergencyMode
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.JwtLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.UserDetailsLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.UserLogic
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import mu.KotlinLogging
|
||||
import org.slf4j.Logger
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.core.env.Environment
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
|
||||
import org.springframework.security.config.http.SessionCreationPolicy
|
||||
import org.springframework.security.core.AuthenticationException
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.security.web.AuthenticationEntryPoint
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.cors.CorsConfiguration
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
|
||||
import javax.annotation.PostConstruct
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
private const val angularDevServerOrigin = "http://localhost:4200"
|
||||
private const val rootUserFirstName = "Root"
|
||||
private const val rootUserLastName = "User"
|
||||
|
||||
abstract class BaseSecurityConfig(
|
||||
private val userDetailsLogic: UserDetailsLogic,
|
||||
private val jwtLogic: JwtLogic,
|
||||
private val environment: Environment,
|
||||
protected val securityProperties: CreSecurityProperties
|
||||
) : WebSecurityConfigurerAdapter() {
|
||||
protected abstract val logger: Logger
|
||||
|
||||
protected val passwordEncoder = BCryptPasswordEncoder()
|
||||
var debugMode = false
|
||||
|
||||
@Bean
|
||||
open fun passwordEncoder() =
|
||||
passwordEncoder
|
||||
|
||||
@Bean
|
||||
open fun corsConfigurationSource() =
|
||||
UrlBasedCorsConfigurationSource().apply {
|
||||
registerCorsConfiguration("/**", CorsConfiguration().apply {
|
||||
allowedOrigins = listOf(angularDevServerOrigin)
|
||||
allowedMethods = listOf(
|
||||
HttpMethod.GET.name,
|
||||
HttpMethod.POST.name,
|
||||
HttpMethod.PUT.name,
|
||||
HttpMethod.DELETE.name,
|
||||
HttpMethod.OPTIONS.name,
|
||||
HttpMethod.HEAD.name
|
||||
)
|
||||
allowCredentials = true
|
||||
}.applyPermitDefaultValues())
|
||||
}
|
||||
|
||||
override fun configure(authBuilder: AuthenticationManagerBuilder) {
|
||||
authBuilder.userDetailsService(userDetailsLogic).passwordEncoder(passwordEncoder)
|
||||
}
|
||||
|
||||
override fun configure(http: HttpSecurity) {
|
||||
http
|
||||
.headers().frameOptions().disable()
|
||||
.and()
|
||||
.csrf().disable()
|
||||
.addFilter(
|
||||
JwtAuthenticationFilter(
|
||||
authenticationManager(),
|
||||
jwtLogic,
|
||||
securityProperties,
|
||||
this::updateUserLoginTime
|
||||
)
|
||||
)
|
||||
.addFilter(
|
||||
JwtAuthorizationFilter(jwtLogic, authenticationManager(), userDetailsLogic)
|
||||
)
|
||||
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
.and()
|
||||
.authorizeRequests()
|
||||
.antMatchers("/api/config/**").permitAll() // Allow access to logo and icon
|
||||
.antMatchers("/api/login").permitAll() // Allow access to login
|
||||
.antMatchers("**").fullyAuthenticated()
|
||||
|
||||
if (debugMode) {
|
||||
http
|
||||
.cors()
|
||||
}
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
fun initDebugMode() {
|
||||
debugMode = "debug" in environment.activeProfiles
|
||||
if (debugMode) logger.warn("Debug mode is enabled, security will be decreased!")
|
||||
}
|
||||
|
||||
protected open fun updateUserLoginTime(userId: Long) {
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Profile("!emergency")
|
||||
@EnableWebSecurity
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
||||
@EnableConfigurationProperties(CreSecurityProperties::class)
|
||||
class SecurityConfig(
|
||||
@Lazy userDetailsLogic: UserDetailsLogic,
|
||||
@Lazy private val userLogic: UserLogic,
|
||||
jwtLogic: JwtLogic,
|
||||
environment: Environment,
|
||||
securityProperties: CreSecurityProperties
|
||||
) : BaseSecurityConfig(userDetailsLogic, jwtLogic, environment, securityProperties) {
|
||||
override val logger = KotlinLogging.logger {}
|
||||
|
||||
@PostConstruct
|
||||
fun initWebSecurity() {
|
||||
if (emergencyMode) {
|
||||
logger.error("Emergency mode is enabled, system users will not be created")
|
||||
return
|
||||
}
|
||||
|
||||
createRootUser()
|
||||
}
|
||||
|
||||
override fun updateUserLoginTime(userId: Long) {
|
||||
userLogic.updateLastLoginTime(userId)
|
||||
}
|
||||
|
||||
private fun createRootUser() {
|
||||
if (securityProperties.root == null) {
|
||||
throw InvalidSystemUserException("root", "cre.security.root configuration is not defined")
|
||||
}
|
||||
|
||||
with(securityProperties.root!!) {
|
||||
if (!userLogic.existsById(this.id)) {
|
||||
userLogic.save(
|
||||
UserDto(
|
||||
id = this.id,
|
||||
firstName = rootUserFirstName,
|
||||
lastName = rootUserLastName,
|
||||
group = null,
|
||||
password = passwordEncoder.encode(this.password),
|
||||
permissions = listOf(Permission.ADMIN),
|
||||
isSystemUser = true
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Profile("emergency")
|
||||
@EnableConfigurationProperties(CreSecurityProperties::class)
|
||||
class EmergencySecurityConfig(
|
||||
userDetailsLogic: UserDetailsLogic,
|
||||
jwtLogic: JwtLogic,
|
||||
environment: Environment,
|
||||
securityProperties: CreSecurityProperties
|
||||
) : BaseSecurityConfig(userDetailsLogic, jwtLogic, environment, securityProperties) {
|
||||
override val logger = KotlinLogging.logger {}
|
||||
|
||||
init {
|
||||
emergencyMode = true
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
class RestAuthenticationEntryPoint : AuthenticationEntryPoint {
|
||||
override fun commence(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
authException: AuthenticationException
|
||||
) = response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized")
|
||||
}
|
||||
|
||||
private class InvalidSystemUserException(userType: String, message: String) :
|
||||
RuntimeException("Invalid $userType user: $message")
|
@ -1,10 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.dtos
|
||||
|
||||
import javax.validation.constraints.NotBlank
|
||||
|
||||
data class CompanyDto(
|
||||
override val id: Long = 0L,
|
||||
|
||||
@field:NotBlank
|
||||
val name: String
|
||||
) : EntityDto
|
@ -1,5 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.dtos
|
||||
|
||||
interface EntityDto {
|
||||
val id: Long
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.dtos
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.NotEmpty
|
||||
|
||||
data class GroupDto(
|
||||
override val id: Long = 0L,
|
||||
|
||||
@field:NotBlank
|
||||
val name: String,
|
||||
|
||||
@field:NotEmpty
|
||||
val permissions: List<Permission>,
|
||||
|
||||
val explicitPermissions: List<Permission> = listOf()
|
||||
) : EntityDto {
|
||||
@get:JsonIgnore
|
||||
val defaultGroupUserId = getDefaultGroupUserId(id)
|
||||
|
||||
companion object {
|
||||
fun getDefaultGroupUserId(id: Long) = 1000000 + id
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.dtos
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import javax.validation.constraints.Min
|
||||
import javax.validation.constraints.NotBlank
|
||||
|
||||
data class MaterialDto(
|
||||
override val id: Long = 0L,
|
||||
|
||||
val name: String,
|
||||
|
||||
val inventoryQuantity: Float,
|
||||
|
||||
val isMixType: Boolean,
|
||||
|
||||
val materialType: MaterialTypeDto,
|
||||
|
||||
val hasSimdut: Boolean = false
|
||||
) : EntityDto
|
||||
|
||||
data class MaterialSaveDto(
|
||||
override val id: Long = 0L,
|
||||
|
||||
@field:NotBlank
|
||||
val name: String,
|
||||
|
||||
@field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO)
|
||||
val inventoryQuantity: Float,
|
||||
|
||||
val materialTypeId: Long,
|
||||
|
||||
val simdutFile: MultipartFile?
|
||||
) : EntityDto
|
||||
|
||||
data class MaterialQuantityDto(
|
||||
val materialId: Long,
|
||||
|
||||
@field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO)
|
||||
val quantity: Float
|
||||
)
|
@ -1,13 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.dtos
|
||||
|
||||
data class MaterialTypeDto(
|
||||
override val id: Long = 0L,
|
||||
|
||||
val name: String,
|
||||
|
||||
val prefix: String,
|
||||
|
||||
val usePercentages: Boolean,
|
||||
|
||||
val systemType: Boolean = false
|
||||
) : EntityDto
|
@ -1,77 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.dtos
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import javax.validation.constraints.Min
|
||||
import javax.validation.constraints.NotBlank
|
||||
|
||||
data class MixDto(
|
||||
override val id: Long = 0L,
|
||||
|
||||
val location: String? = null,
|
||||
|
||||
@JsonIgnore
|
||||
val recipeId: Long,
|
||||
|
||||
val mixType: MixTypeDto,
|
||||
|
||||
@JsonIgnore
|
||||
val mixQuantities: MixQuantitiesDto,
|
||||
) : EntityDto {
|
||||
@Suppress("unused")
|
||||
@get:JsonProperty("mixQuantities")
|
||||
val mixQuantitiesOutput by lazy {
|
||||
mixQuantities.materials.map {
|
||||
MixQuantityOutputDto(it.id, it.material, it.quantity, it.position, false)
|
||||
} + mixQuantities.mixTypes.map {
|
||||
MixQuantityOutputDto(it.id, it.mixType.asMaterial(), it.quantity, it.position, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class MixQuantitiesDto(
|
||||
val materials: List<MixMaterialDto> = listOf(),
|
||||
|
||||
val mixTypes: List<MixMixTypeDto> = listOf()
|
||||
) {
|
||||
val all get() = materials + mixTypes
|
||||
}
|
||||
|
||||
data class MixQuantityOutputDto(
|
||||
val id: Long,
|
||||
|
||||
val material: MaterialDto,
|
||||
|
||||
val quantity: Float,
|
||||
|
||||
val position: Int,
|
||||
|
||||
val isMixType: Boolean
|
||||
)
|
||||
|
||||
data class MixSaveDto(
|
||||
val id: Long = 0L,
|
||||
|
||||
@field:NotBlank
|
||||
val name: String,
|
||||
|
||||
val recipeId: Long = 0L,
|
||||
|
||||
val materialTypeId: Long,
|
||||
|
||||
val mixQuantities: List<MixQuantitySaveDto>
|
||||
)
|
||||
|
||||
data class MixDeductDto(
|
||||
val id: Long,
|
||||
|
||||
@field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO)
|
||||
val ratio: Float
|
||||
)
|
||||
|
||||
data class MixLocationDto(
|
||||
val mixId: Long,
|
||||
|
||||
val location: String?
|
||||
)
|
@ -1,57 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.dtos
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import javax.validation.constraints.Min
|
||||
|
||||
sealed interface MixQuantityDto : EntityDto {
|
||||
val quantity: Float
|
||||
val position: Int
|
||||
|
||||
val materialType: MaterialTypeDto
|
||||
val name: String
|
||||
}
|
||||
|
||||
data class MixMaterialDto(
|
||||
override val id: Long = 0L,
|
||||
|
||||
val material: MaterialDto,
|
||||
|
||||
override val quantity: Float,
|
||||
|
||||
override val position: Int
|
||||
) : MixQuantityDto {
|
||||
override val materialType: MaterialTypeDto
|
||||
get() = material.materialType
|
||||
|
||||
override val name: String
|
||||
get() = material.name
|
||||
}
|
||||
|
||||
data class MixMixTypeDto(
|
||||
override val id: Long,
|
||||
|
||||
val mixType: MixTypeDto,
|
||||
|
||||
override val quantity: Float,
|
||||
|
||||
override val position: Int
|
||||
) : MixQuantityDto {
|
||||
override val materialType: MaterialTypeDto
|
||||
get() = mixType.materialType
|
||||
|
||||
override val name: String
|
||||
get() = mixType.name
|
||||
}
|
||||
|
||||
data class MixQuantitySaveDto(
|
||||
override val id: Long = 0L,
|
||||
|
||||
val materialId: Long,
|
||||
|
||||
@field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO)
|
||||
val quantity: Float,
|
||||
|
||||
val position: Int,
|
||||
|
||||
val isMixType: Boolean
|
||||
) : EntityDto
|
@ -1,14 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.dtos
|
||||
|
||||
data class MixTypeDto(
|
||||
override val id: Long = 0L,
|
||||
|
||||
val name: String,
|
||||
|
||||
val materialType: MaterialTypeDto,
|
||||
|
||||
val material: MaterialDto? = null
|
||||
) : EntityDto {
|
||||
fun asMaterial() =
|
||||
MaterialDto(id, name, 0f, true, materialType)
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.dtos
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import java.time.LocalDate
|
||||
import javax.validation.constraints.Max
|
||||
import javax.validation.constraints.Min
|
||||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.Pattern
|
||||
|
||||
data class RecipeDto(
|
||||
override val id: Long = 0L,
|
||||
|
||||
val name: String,
|
||||
|
||||
val description: String,
|
||||
|
||||
val color: String,
|
||||
|
||||
val gloss: Byte,
|
||||
|
||||
val sample: Int?,
|
||||
|
||||
val approbationDate: LocalDate?,
|
||||
|
||||
val approbationExpired: Boolean,
|
||||
|
||||
val remark: String,
|
||||
|
||||
val company: CompanyDto,
|
||||
|
||||
val mixes: List<MixDto>,
|
||||
|
||||
val groupsInformation: List<RecipeGroupInformationDto>
|
||||
) : EntityDto {
|
||||
val mixTypes: Collection<MixTypeDto>
|
||||
@JsonIgnore
|
||||
get() = mixes.map { it.mixType }
|
||||
}
|
||||
|
||||
data class RecipeSaveDto(
|
||||
@field:NotBlank
|
||||
val name: String,
|
||||
|
||||
@field:NotBlank
|
||||
val description: String,
|
||||
|
||||
@field:NotBlank
|
||||
@field:Pattern(regexp = Constants.ValidationRegexes.VALIDATION_COLOR_PATTERN)
|
||||
val color: String,
|
||||
|
||||
@field:Min(0, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS)
|
||||
@field:Max(100, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS)
|
||||
val gloss: Byte,
|
||||
|
||||
@field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO)
|
||||
val sample: Int?,
|
||||
|
||||
val approbationDate: LocalDate?,
|
||||
|
||||
val remark: String?,
|
||||
|
||||
val companyId: Long
|
||||
)
|
||||
|
||||
data class RecipeUpdateDto(
|
||||
val id: Long,
|
||||
|
||||
@field:NotBlank
|
||||
val name: String,
|
||||
|
||||
@field:NotBlank
|
||||
val description: String,
|
||||
|
||||
@field:NotBlank
|
||||
@field:Pattern(regexp = Constants.ValidationRegexes.VALIDATION_COLOR_PATTERN)
|
||||
val color: String,
|
||||
|
||||
@field:Min(0, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS)
|
||||
@field:Max(100, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS)
|
||||
val gloss: Byte,
|
||||
|
||||
@field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO)
|
||||
val sample: Int?,
|
||||
|
||||
val approbationDate: LocalDate?,
|
||||
|
||||
val remark: String?,
|
||||
|
||||
val steps: List<RecipeGroupStepsDto>
|
||||
)
|
||||
|
||||
data class RecipeGroupInformationDto(
|
||||
override val id: Long = 0L,
|
||||
|
||||
val group: GroupDto,
|
||||
|
||||
val note: String? = null,
|
||||
|
||||
val steps: List<RecipeStepDto> = listOf()
|
||||
) : EntityDto
|
||||
|
||||
data class RecipeGroupStepsDto(
|
||||
val groupId: Long,
|
||||
|
||||
val steps: List<RecipeStepDto>
|
||||
)
|
||||
|
||||
data class RecipeGroupNoteDto(
|
||||
val groupId: Long,
|
||||
|
||||
val content: String?
|
||||
)
|
||||
|
||||
data class RecipePublicDataDto(
|
||||
val recipeId: Long,
|
||||
|
||||
val notes: List<RecipeGroupNoteDto>,
|
||||
|
||||
val mixesLocation: List<MixLocationDto>
|
||||
)
|
@ -1,9 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.dtos
|
||||
|
||||
data class RecipeStepDto(
|
||||
override val id: Long = 0L,
|
||||
|
||||
val position: Int,
|
||||
|
||||
val message: String
|
||||
) : EntityDto
|
@ -1,52 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.dtos
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import java.time.LocalDate
|
||||
import javax.validation.constraints.Min
|
||||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.NotEmpty
|
||||
|
||||
data class TouchUpKitDto(
|
||||
override val id: Long = 0L,
|
||||
|
||||
@field:NotBlank
|
||||
val project: String,
|
||||
|
||||
@field:NotBlank
|
||||
val buggy: String,
|
||||
|
||||
@field:NotBlank
|
||||
val company: String,
|
||||
|
||||
@field:Min(1, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ONE)
|
||||
val quantity: Int,
|
||||
|
||||
val shippingDate: LocalDate,
|
||||
|
||||
val completionDate: LocalDate?,
|
||||
|
||||
val completed: Boolean = false,
|
||||
|
||||
val expired: Boolean = false,
|
||||
|
||||
@field:NotEmpty
|
||||
val finish: List<String>,
|
||||
|
||||
@field:NotEmpty
|
||||
val material: List<String>,
|
||||
|
||||
@field:NotEmpty
|
||||
val content: List<TouchUpKitProductDto>
|
||||
) : EntityDto
|
||||
|
||||
data class TouchUpKitProductDto(
|
||||
override val id: Long = 0L,
|
||||
|
||||
val name: String,
|
||||
|
||||
val description: String?,
|
||||
|
||||
val quantity: Float,
|
||||
|
||||
val ready: Boolean
|
||||
) : EntityDto
|
@ -1,94 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.dtos
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.SpringUserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.toAuthority
|
||||
import java.time.LocalDateTime
|
||||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.Size
|
||||
|
||||
data class UserDto(
|
||||
override val id: Long = 0L,
|
||||
|
||||
val firstName: String,
|
||||
|
||||
val lastName: String,
|
||||
|
||||
@field:JsonIgnore
|
||||
val password: String = "",
|
||||
|
||||
val group: GroupDto?,
|
||||
|
||||
val permissions: List<Permission>,
|
||||
|
||||
val explicitPermissions: List<Permission> = listOf(),
|
||||
|
||||
val lastLoginTime: LocalDateTime? = null,
|
||||
|
||||
@field:JsonIgnore
|
||||
val isDefaultGroupUser: Boolean = false,
|
||||
|
||||
@field:JsonIgnore
|
||||
val isSystemUser: Boolean = false
|
||||
) : EntityDto {
|
||||
@get:JsonIgnore
|
||||
val authorities
|
||||
get() = permissions
|
||||
.map { it.toAuthority() }
|
||||
.toMutableSet()
|
||||
}
|
||||
|
||||
data class UserSaveDto(
|
||||
val id: Long = 0L,
|
||||
|
||||
@field:NotBlank
|
||||
val firstName: String,
|
||||
|
||||
@field:NotBlank
|
||||
val lastName: String,
|
||||
|
||||
@field:NotBlank
|
||||
@field:Size(min = 8, message = Constants.ValidationMessages.PASSWORD_TOO_SMALL)
|
||||
val password: String,
|
||||
|
||||
val groupId: Long?,
|
||||
|
||||
val permissions: List<Permission>,
|
||||
|
||||
// TODO WN: Test if working
|
||||
// @JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
@field:JsonIgnore
|
||||
val isSystemUser: Boolean = false,
|
||||
|
||||
@field:JsonIgnore
|
||||
val isDefaultGroupUser: Boolean = false
|
||||
)
|
||||
|
||||
data class UserUpdateDto(
|
||||
val id: Long = 0L,
|
||||
|
||||
@field:NotBlank
|
||||
val firstName: String,
|
||||
|
||||
@field:NotBlank
|
||||
val lastName: String,
|
||||
|
||||
val groupId: Long?,
|
||||
|
||||
val permissions: List<Permission>
|
||||
)
|
||||
|
||||
data class UserLoginRequestDto(val id: Long, val password: String)
|
||||
|
||||
class UserDetails(val user: UserDto) : SpringUserDetails {
|
||||
override fun getPassword() = user.password
|
||||
override fun getUsername() = user.id.toString()
|
||||
override fun getAuthorities() = user.authorities
|
||||
|
||||
override fun isAccountNonExpired() = true
|
||||
override fun isAccountNonLocked() = true
|
||||
override fun isCredentialsNonExpired() = true
|
||||
override fun isEnabled() = true
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.exception
|
||||
|
||||
import org.springframework.http.HttpStatus
|
||||
|
||||
class InvalidPositionsException(val errors: Set<InvalidPositionError>) : RestException(
|
||||
"invalid-positions",
|
||||
"Invalid positions",
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"The positions are invalid",
|
||||
mapOf(
|
||||
"errors" to errors
|
||||
)
|
||||
)
|
||||
|
||||
data class InvalidPositionError(val type: String, val details: String)
|
@ -1,10 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.exception
|
||||
|
||||
import org.springframework.http.HttpStatus
|
||||
|
||||
class NoDefaultGroupException : RestException(
|
||||
"nodefaultgroup",
|
||||
"No default group",
|
||||
HttpStatus.NOT_FOUND,
|
||||
"No default group cookie is defined in the current request"
|
||||
)
|
@ -59,17 +59,6 @@ class AlreadyExistsException(
|
||||
extensions = extensions.apply { this[identifierName] = identifierValue }.toMap()
|
||||
)
|
||||
|
||||
class CannotUpdateException(
|
||||
errorCode: String,
|
||||
title: String,
|
||||
details: String
|
||||
) : RestException(
|
||||
errorCode = "cannotupdate-$errorCode",
|
||||
title = title,
|
||||
status = HttpStatus.BAD_REQUEST,
|
||||
details = details
|
||||
)
|
||||
|
||||
class CannotDeleteException(
|
||||
errorCode: String,
|
||||
title: String,
|
||||
|
@ -1,38 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.logic
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto
|
||||
import dev.fyloz.colorrecipesexplorer.service.CompanyService
|
||||
|
||||
interface CompanyLogic : Logic<CompanyDto, CompanyService>
|
||||
|
||||
@LogicComponent
|
||||
class DefaultCompanyLogic(service: CompanyService) :
|
||||
BaseLogic<CompanyDto, CompanyService>(service, Constants.ModelNames.COMPANY), CompanyLogic {
|
||||
override fun save(dto: CompanyDto): CompanyDto {
|
||||
throwIfNameAlreadyExists(dto.name)
|
||||
|
||||
return super.save(dto)
|
||||
}
|
||||
|
||||
override fun update(dto: CompanyDto): CompanyDto {
|
||||
throwIfNameAlreadyExists(dto.name, dto.id)
|
||||
|
||||
return super.update(dto)
|
||||
}
|
||||
|
||||
override fun deleteById(id: Long) {
|
||||
if (service.isUsedByRecipe(id)) {
|
||||
throw cannotDeleteException("Cannot delete the company with the id '$id' because one or more recipes depends on it")
|
||||
}
|
||||
|
||||
super.deleteById(id)
|
||||
}
|
||||
|
||||
private fun throwIfNameAlreadyExists(name: String, id: Long? = null) {
|
||||
if (service.existsByName(name, id)) {
|
||||
throw alreadyExistsException(value = name)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,125 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.logic
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.MaterialQuantityDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.MixDeductDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto
|
||||
import dev.fyloz.colorrecipesexplorer.exception.RestException
|
||||
import dev.fyloz.colorrecipesexplorer.model.Material
|
||||
import dev.fyloz.colorrecipesexplorer.utils.mapMayThrow
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.stereotype.Service
|
||||
import javax.transaction.Transactional
|
||||
|
||||
interface InventoryLogic {
|
||||
/** Adds each given [MaterialQuantityDto] to the inventory and returns the updated quantities. */
|
||||
fun add(materialQuantities: Collection<MaterialQuantityDto>): Collection<MaterialQuantityDto>
|
||||
|
||||
/** Adds a given quantity to the given [Material]'s inventory quantity according to the given [materialQuantity] and returns the updated quantity. */
|
||||
fun add(materialQuantity: MaterialQuantityDto): Float
|
||||
|
||||
/** Deducts the inventory quantity of each [Material]s in the mix according to the ratio defined in the given [mixRatio] and returns the updated quantities. */
|
||||
fun deductMix(mixRatio: MixDeductDto): Collection<MaterialQuantityDto>
|
||||
|
||||
/** Deducts the inventory quantity of each given [MaterialQuantityDto] and returns the updated quantities. */
|
||||
fun deduct(materialQuantities: Collection<MaterialQuantityDto>): Collection<MaterialQuantityDto>
|
||||
|
||||
/** Deducts the inventory quantity of a given [Material] by a given quantity according to the given [materialQuantity] and returns the updated quantity. */
|
||||
fun deduct(materialQuantity: MaterialQuantityDto): Float
|
||||
}
|
||||
|
||||
@Service
|
||||
@RequireDatabase
|
||||
class DefaultInventoryLogic(
|
||||
private val materialLogic: MaterialLogic,
|
||||
private val mixLogic: MixLogic
|
||||
) : InventoryLogic {
|
||||
@Transactional
|
||||
override fun add(materialQuantities: Collection<MaterialQuantityDto>) =
|
||||
materialQuantities.map { MaterialQuantityDto(it.materialId, add(it)) }
|
||||
|
||||
override fun add(materialQuantity: MaterialQuantityDto) =
|
||||
materialLogic.updateQuantity(
|
||||
materialLogic.getById(materialQuantity.materialId),
|
||||
materialQuantity.quantity
|
||||
)
|
||||
|
||||
@Transactional
|
||||
override fun deductMix(mixRatio: MixDeductDto): Collection<MaterialQuantityDto> {
|
||||
val mix = mixLogic.getById(mixRatio.id)
|
||||
val mixMaterials = mix.mixQuantities.materials
|
||||
|
||||
if (mixMaterials.isEmpty()) return listOf()
|
||||
return deduct(getMaterialsWithAdjustedQuantities(mixMaterials, mixRatio))
|
||||
}
|
||||
|
||||
@Transactional
|
||||
override fun deduct(materialQuantities: Collection<MaterialQuantityDto>): Collection<MaterialQuantityDto> {
|
||||
val thrown = mutableListOf<NotEnoughInventoryException>()
|
||||
|
||||
val updatedQuantities =
|
||||
materialQuantities.mapMayThrow<MaterialQuantityDto, MaterialQuantityDto, NotEnoughInventoryException>(
|
||||
{ thrown.add(it) }
|
||||
) {
|
||||
MaterialQuantityDto(it.materialId, deduct(it))
|
||||
}
|
||||
|
||||
if (thrown.isNotEmpty()) {
|
||||
throw MultiplesNotEnoughInventoryException(thrown)
|
||||
}
|
||||
|
||||
return updatedQuantities
|
||||
}
|
||||
|
||||
override fun deduct(materialQuantity: MaterialQuantityDto): Float =
|
||||
with(materialLogic.getById(materialQuantity.materialId)) {
|
||||
if (this.inventoryQuantity >= materialQuantity.quantity) {
|
||||
materialLogic.updateQuantity(this, -materialQuantity.quantity)
|
||||
} else {
|
||||
throw NotEnoughInventoryException(materialQuantity.quantity, this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMaterialsWithAdjustedQuantities(
|
||||
mixMaterials: Collection<MixMaterialDto>,
|
||||
mixRatio: MixDeductDto
|
||||
): Collection<MaterialQuantityDto> {
|
||||
val adjustedFirstMaterialQuantity = mixMaterials.first().quantity * mixRatio.ratio
|
||||
|
||||
fun getAdjustedQuantity(material: MaterialDto, quantity: Float) =
|
||||
if (!material.materialType.usePercentages)
|
||||
quantity * mixRatio.ratio // Simply multiply the quantity by the ratio
|
||||
else
|
||||
(quantity * adjustedFirstMaterialQuantity) / 100f // Percents quantities are a ratio of the first material
|
||||
|
||||
return mixMaterials.associate { it.material to it.quantity }
|
||||
.mapValues { getAdjustedQuantity(it.key, it.value) }
|
||||
.map { MaterialQuantityDto(it.key.id, it.value) }
|
||||
}
|
||||
}
|
||||
|
||||
class NotEnoughInventoryException(quantity: Float, material: MaterialDto) :
|
||||
RestException(
|
||||
"notenoughinventory",
|
||||
"Not enough inventory",
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Cannot deduct ${quantity}mL of ${material.name} because there is only ${material.inventoryQuantity}mL in inventory",
|
||||
mapOf(
|
||||
"material" to material.name,
|
||||
"materialId" to material.id.toString(),
|
||||
"requestQuantity" to quantity,
|
||||
"availableQuantity" to material.inventoryQuantity
|
||||
)
|
||||
)
|
||||
|
||||
class MultiplesNotEnoughInventoryException(exceptions: List<NotEnoughInventoryException>) :
|
||||
RestException(
|
||||
"notenoughinventory-multiple",
|
||||
"Not enough inventory",
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Cannot deduct requested quantities because there is no enough of them in inventory",
|
||||
mapOf(
|
||||
"lowQuantities" to exceptions.map { it.extensions }
|
||||
)
|
||||
)
|
@ -1,101 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.logic
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.EntityDto
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.service.Service
|
||||
import dev.fyloz.colorrecipesexplorer.utils.collections.LazyMapList
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
/**
|
||||
* Represents the logic for a DTO type.
|
||||
*
|
||||
* @param D The type of the DTO.
|
||||
* @param S The service for the DTO.
|
||||
*/
|
||||
interface Logic<D : EntityDto, S : Service<D, *, *>> {
|
||||
/** Checks if a DTO with the given [id] exists. */
|
||||
fun existsById(id: Long): Boolean
|
||||
|
||||
/** Get all DTOs. */
|
||||
fun getAll(): Collection<D>
|
||||
|
||||
/** Get the DTO for the given [id]. Throws if no DTO were found. */
|
||||
fun getById(id: Long): D
|
||||
|
||||
/** Saves the given [dto]. */
|
||||
fun save(dto: D): D
|
||||
|
||||
/** Saves all the given [dtos]. */
|
||||
fun saveAll(dtos: Collection<D>): Collection<D>
|
||||
|
||||
/** Updates the given [dto]. Throws if no DTO with the same id exists. */
|
||||
fun update(dto: D): D
|
||||
|
||||
/** Deletes the dto with the given [id]. */
|
||||
fun deleteById(id: Long)
|
||||
}
|
||||
|
||||
abstract class BaseLogic<D : EntityDto, S : Service<D, *, *>>(
|
||||
protected val service: S,
|
||||
protected val typeName: String
|
||||
) : Logic<D, S> {
|
||||
protected val typeNameLowerCase = typeName.lowercase()
|
||||
|
||||
override fun existsById(id: Long) =
|
||||
service.existsById(id)
|
||||
|
||||
override fun getAll() =
|
||||
service.getAll()
|
||||
|
||||
override fun getById(id: Long) =
|
||||
service.getById(id) ?: throw notFoundException(value = id)
|
||||
|
||||
override fun save(dto: D) =
|
||||
service.save(dto)
|
||||
|
||||
override fun saveAll(dtos: Collection<D>) =
|
||||
dtos.map(::save)
|
||||
|
||||
override fun update(dto: D): D {
|
||||
if (!existsById(dto.id)) {
|
||||
throw notFoundException(value = dto.id)
|
||||
}
|
||||
|
||||
return service.save(dto)
|
||||
}
|
||||
|
||||
override fun deleteById(id: Long) =
|
||||
service.deleteById(id)
|
||||
|
||||
protected fun notFoundException(identifierName: String = ID_IDENTIFIER_NAME, value: Any) =
|
||||
NotFoundException(
|
||||
typeNameLowerCase,
|
||||
"$typeName not found",
|
||||
"A $typeNameLowerCase with the $identifierName '$value' could not be found",
|
||||
value,
|
||||
identifierName
|
||||
)
|
||||
|
||||
protected fun alreadyExistsException(identifierName: String = NAME_IDENTIFIER_NAME, value: Any) =
|
||||
AlreadyExistsException(
|
||||
typeNameLowerCase,
|
||||
"$typeName already exists",
|
||||
"A $typeNameLowerCase with the $identifierName '$value' already exists",
|
||||
value,
|
||||
identifierName
|
||||
)
|
||||
|
||||
protected fun cannotDeleteException(details: String) =
|
||||
CannotDeleteException(
|
||||
typeNameLowerCase,
|
||||
"Cannot delete $typeNameLowerCase",
|
||||
details
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val ID_IDENTIFIER_NAME = "id"
|
||||
const val NAME_IDENTIFIER_NAME = "name"
|
||||
}
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.logic
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.MaterialSaveDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.MixTypeDto
|
||||
import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic
|
||||
import dev.fyloz.colorrecipesexplorer.model.Material
|
||||
import dev.fyloz.colorrecipesexplorer.service.MaterialService
|
||||
|
||||
interface MaterialLogic : Logic<MaterialDto, MaterialService> {
|
||||
/** Checks if a material with the given [name] exists. */
|
||||
fun existsByName(name: String): Boolean
|
||||
|
||||
/**
|
||||
* Returns every material available in the context of the recipe with the given [recipeId].
|
||||
* The materials included contains every non mix type material, and the materials generated for the recipe mix types.
|
||||
*/
|
||||
fun getAllForRecipe(recipeId: Long): Collection<MaterialDto>
|
||||
|
||||
/**
|
||||
* Returns every material available in the context of the mix with the given [mixId].
|
||||
* The materials included contains every non mix type material, and the materials generated for
|
||||
* the mix's recipe mix types, excluding the mix's mix type.
|
||||
*/
|
||||
fun getAllForMix(mixId: Long): Collection<MaterialDto>
|
||||
|
||||
/** Saves the given [dto]. */
|
||||
fun save(dto: MaterialSaveDto): MaterialDto
|
||||
|
||||
/** Updates the given [dto]. */
|
||||
fun update(dto: MaterialSaveDto): MaterialDto
|
||||
|
||||
/** Updates the quantity of the given [material] with the given [factor] and returns the updated quantity. */
|
||||
fun updateQuantity(material: MaterialDto, factor: Float): Float
|
||||
}
|
||||
|
||||
@LogicComponent
|
||||
class DefaultMaterialLogic(
|
||||
service: MaterialService,
|
||||
val recipeLogic: RecipeLogic,
|
||||
val mixLogic: MixLogic,
|
||||
val materialTypeLogic: MaterialTypeLogic,
|
||||
val fileLogic: WriteableFileLogic
|
||||
) : BaseLogic<MaterialDto, MaterialService>(service, Constants.ModelNames.MATERIAL), MaterialLogic {
|
||||
override fun existsByName(name: String) = service.existsByName(name, null)
|
||||
|
||||
override fun getAllForRecipe(recipeId: Long): Collection<MaterialDto> {
|
||||
val recipe = recipeLogic.getById(recipeId)
|
||||
|
||||
return getAllWithMixTypesMaterials(recipe.mixTypes)
|
||||
}
|
||||
|
||||
override fun getAllForMix(mixId: Long): Collection<MaterialDto> {
|
||||
val mix = mixLogic.getById(mixId)
|
||||
val recipe = recipeLogic.getById(mix.recipeId)
|
||||
|
||||
val availableMixTypes = recipe.mixTypes.filter { it != mix.mixType }
|
||||
return getAllWithMixTypesMaterials(availableMixTypes)
|
||||
}
|
||||
|
||||
private fun getAllWithMixTypesMaterials(mixTypes: Collection<MixTypeDto>) =
|
||||
getAll() + mixTypes.map { it.asMaterial() }
|
||||
|
||||
override fun save(dto: MaterialSaveDto) = save(saveDtoToDto(dto, false)).also { saveSimdutFile(dto, false) }
|
||||
override fun save(dto: MaterialDto): MaterialDto {
|
||||
throwIfNameAlreadyExists(dto.name)
|
||||
|
||||
return super.save(dto)
|
||||
}
|
||||
|
||||
override fun update(dto: MaterialSaveDto) = update(saveDtoToDto(dto, true)).also { saveSimdutFile(dto, true) }
|
||||
override fun update(dto: MaterialDto): MaterialDto {
|
||||
throwIfNameAlreadyExists(dto.name, dto.id)
|
||||
|
||||
return super.update(dto)
|
||||
}
|
||||
|
||||
override fun updateQuantity(material: MaterialDto, factor: Float): Float {
|
||||
val updatedQuantity = material.inventoryQuantity + factor
|
||||
service.updateInventoryQuantityById(material.id, updatedQuantity)
|
||||
|
||||
return updatedQuantity
|
||||
}
|
||||
|
||||
override fun deleteById(id: Long) {
|
||||
if (service.isUsedByMixMaterialOrMixType(id)) {
|
||||
throw cannotDeleteException("Cannot delete the material with the id '$id' because mix types and/or recipes depends on it")
|
||||
}
|
||||
|
||||
val material = getById(id)
|
||||
val simdutPath = Material.getSimdutFilePath(material.name)
|
||||
if (fileLogic.exists(simdutPath)) {
|
||||
fileLogic.delete(simdutPath)
|
||||
}
|
||||
|
||||
super.deleteById(id)
|
||||
}
|
||||
|
||||
private fun saveDtoToDto(saveDto: MaterialSaveDto, updating: Boolean): MaterialDto {
|
||||
val isMixType = !updating || getById(saveDto.id).isMixType
|
||||
val materialType = materialTypeLogic.getById(saveDto.materialTypeId)
|
||||
|
||||
return MaterialDto(saveDto.id, saveDto.name, saveDto.inventoryQuantity, isMixType, materialType)
|
||||
}
|
||||
|
||||
private fun saveSimdutFile(dto: MaterialSaveDto, updating: Boolean) {
|
||||
val file = dto.simdutFile
|
||||
|
||||
if (file != null && !file.isEmpty) {
|
||||
fileLogic.write(file, Material.getSimdutFilePath(dto.name), updating)
|
||||
}
|
||||
}
|
||||
|
||||
private fun throwIfNameAlreadyExists(name: String, id: Long? = null) {
|
||||
if (service.existsByName(name, id)) {
|
||||
throw alreadyExistsException(value = name)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.logic
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto
|
||||
import dev.fyloz.colorrecipesexplorer.exception.CannotUpdateException
|
||||
import dev.fyloz.colorrecipesexplorer.service.MaterialTypeService
|
||||
|
||||
interface MaterialTypeLogic : Logic<MaterialTypeDto, MaterialTypeService> {
|
||||
/** Gets all material types which are or not [systemType]s. */
|
||||
fun getAll(systemType: Boolean): Collection<MaterialTypeDto>
|
||||
|
||||
/** Gets the material type with the given [name]. */
|
||||
fun getByName(name: String): MaterialTypeDto?
|
||||
|
||||
/** Updates the given [dto], and throws if it is a system types. */
|
||||
fun updateNonSystemType(dto: MaterialTypeDto)
|
||||
}
|
||||
|
||||
@LogicComponent
|
||||
class DefaultMaterialTypeLogic(service: MaterialTypeService) :
|
||||
BaseLogic<MaterialTypeDto, MaterialTypeService>(service, Constants.ModelNames.MATERIAL_TYPE), MaterialTypeLogic {
|
||||
override fun getAll(systemType: Boolean) = service.getAll(systemType)
|
||||
override fun getByName(name: String) = service.getByName(name)
|
||||
|
||||
override fun updateNonSystemType(dto: MaterialTypeDto) {
|
||||
if (service.existsById(dto.id, true)) {
|
||||
throw CannotUpdateException(
|
||||
typeNameLowerCase,
|
||||
"Cannot update $typeNameLowerCase",
|
||||
"Cannot update material type '${dto.name}' because it is a system material type"
|
||||
)
|
||||
}
|
||||
|
||||
update(dto)
|
||||
}
|
||||
|
||||
override fun save(dto: MaterialTypeDto): MaterialTypeDto {
|
||||
throwIfNameAlreadyExists(dto.name)
|
||||
throwIfPrefixAlreadyExists(dto.prefix)
|
||||
|
||||
return super.save(dto)
|
||||
}
|
||||
|
||||
override fun update(dto: MaterialTypeDto): MaterialTypeDto {
|
||||
throwIfNameAlreadyExists(dto.name, dto.id)
|
||||
throwIfPrefixAlreadyExists(dto.prefix, dto.id)
|
||||
|
||||
return super.update(dto)
|
||||
}
|
||||
|
||||
override fun deleteById(id: Long) {
|
||||
if (service.isUsedByMaterial(id)) {
|
||||
throw cannotDeleteException("Cannot delete material type with the id '$id' because one or more materials depends on it")
|
||||
}
|
||||
|
||||
super.deleteById(id)
|
||||
}
|
||||
|
||||
private fun throwIfNameAlreadyExists(name: String, id: Long? = null) {
|
||||
if (service.existsByName(name, id)) {
|
||||
throw alreadyExistsException(value = name)
|
||||
}
|
||||
}
|
||||
|
||||
private fun throwIfPrefixAlreadyExists(prefix: String, id: Long? = null) {
|
||||
if (service.existsByPrefix(prefix, id)) {
|
||||
throw alreadyExistsException(PREFIX_IDENTIFIER_NAME, prefix)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PREFIX_IDENTIFIER_NAME = "prefix"
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.logic
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.MixDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.MixLocationDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.MixSaveDto
|
||||
import dev.fyloz.colorrecipesexplorer.service.MixService
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
interface MixLogic : Logic<MixDto, MixService> {
|
||||
/** Saves the given [dto]. */
|
||||
fun save(dto: MixSaveDto): MixDto
|
||||
|
||||
/** Updates the given [dto]. */
|
||||
fun update(dto: MixSaveDto): MixDto
|
||||
|
||||
/** Updates the location of each mix in the given [updatedLocations]. */
|
||||
fun updateLocations(updatedLocations: Collection<MixLocationDto>)
|
||||
}
|
||||
|
||||
@LogicComponent
|
||||
class DefaultMixLogic(
|
||||
service: MixService,
|
||||
@Lazy private val recipeLogic: RecipeLogic,
|
||||
@Lazy private val materialTypeLogic: MaterialTypeLogic,
|
||||
private val mixTypeLogic: MixTypeLogic,
|
||||
private val mixQuantityLogic: MixQuantityLogic
|
||||
) : BaseLogic<MixDto, MixService>(service, Constants.ModelNames.MIX), MixLogic {
|
||||
@Transactional
|
||||
override fun save(dto: MixSaveDto): MixDto {
|
||||
val recipe = recipeLogic.getById(dto.recipeId)
|
||||
val materialType = materialTypeLogic.getById(dto.materialTypeId)
|
||||
|
||||
val mix = MixDto(
|
||||
recipeId = recipe.id,
|
||||
mixType = mixTypeLogic.getOrCreateForNameAndMaterialType(dto.name, materialType),
|
||||
mixQuantities = mixQuantityLogic.validateAndPrepareForMix(dto.mixQuantities)
|
||||
)
|
||||
|
||||
return save(mix)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
override fun update(dto: MixSaveDto): MixDto {
|
||||
val materialType = materialTypeLogic.getById(dto.materialTypeId)
|
||||
val mix = getById(dto.id)
|
||||
|
||||
// Update the mix type if it has been changed
|
||||
val mixType = if (mix.mixType.name != dto.name || mix.mixType.materialType.id != dto.materialTypeId) {
|
||||
mixTypeLogic.updateOrCreateForNameAndMaterialType(mix.mixType, dto.name, materialType)
|
||||
} else {
|
||||
mix.mixType
|
||||
}
|
||||
|
||||
return update(
|
||||
MixDto(
|
||||
id = dto.id,
|
||||
recipeId = mix.recipeId,
|
||||
mixType = mixType,
|
||||
mixQuantities = mixQuantityLogic.validateAndPrepareForMix(dto.mixQuantities)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateLocations(updatedLocations: Collection<MixLocationDto>) =
|
||||
updatedLocations.forEach(::updateLocation)
|
||||
|
||||
private fun updateLocation(updatedLocation: MixLocationDto) {
|
||||
service.updateLocationById(updatedLocation.mixId, updatedLocation.location)
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.logic
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.*
|
||||
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError
|
||||
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.RestException
|
||||
import dev.fyloz.colorrecipesexplorer.utils.PositionUtils
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.http.HttpStatus
|
||||
|
||||
interface MixQuantityLogic {
|
||||
/**
|
||||
* Validates if the given [mixMaterials]. To be valid, the position of each mix material must be greater or equals to 1 and unique in the set.
|
||||
* There must also be no gap between the positions. Also, the quantity of the first mix material in the set must not be expressed in percentages.
|
||||
* If any of those criteria are not met, an [InvalidGroupStepsPositionsException] will be thrown.
|
||||
*/
|
||||
fun validateMixQuantities(mixMaterials: List<MixQuantityDto>)
|
||||
|
||||
/** Validates the given mix quantities [dtos] and put them in [MixQuantitiesDto] to be consumed by a mix. */
|
||||
fun validateAndPrepareForMix(dtos: List<MixQuantitySaveDto>): MixQuantitiesDto
|
||||
}
|
||||
|
||||
@LogicComponent
|
||||
class DefaultMixQuantityLogic(
|
||||
@Lazy private val materialLogic: MaterialLogic,
|
||||
private val mixTypeLogic: MixTypeLogic
|
||||
) : MixQuantityLogic {
|
||||
override fun validateMixQuantities(mixMaterials: List<MixQuantityDto>) {
|
||||
if (mixMaterials.isEmpty()) return
|
||||
|
||||
val sortedMixMaterials = mixMaterials.sortedBy { it.position }
|
||||
|
||||
try {
|
||||
PositionUtils.validate(sortedMixMaterials.map { it.position })
|
||||
} catch (ex: InvalidPositionsException) {
|
||||
throw InvalidMixMaterialsPositionsException(ex.errors)
|
||||
}
|
||||
|
||||
val firstMixMaterial = sortedMixMaterials[0]
|
||||
if (firstMixMaterial is MixMaterialDto) {
|
||||
if (firstMixMaterial.material.materialType.usePercentages) {
|
||||
throw InvalidFirstMixMaterialException(sortedMixMaterials[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun validateAndPrepareForMix(dtos: List<MixQuantitySaveDto>): MixQuantitiesDto {
|
||||
val mixMixTypes = dtos.filter { it.isMixType }.map {
|
||||
MixMixTypeDto(
|
||||
id = it.id,
|
||||
mixType = mixTypeLogic.getById(it.materialId),
|
||||
quantity = it.quantity,
|
||||
position = it.position
|
||||
)
|
||||
}
|
||||
|
||||
val mixMaterials = dtos.filter { !it.isMixType }.map {
|
||||
MixMaterialDto(
|
||||
id = it.id,
|
||||
material = materialLogic.getById(it.materialId),
|
||||
quantity = it.quantity,
|
||||
position = it.position
|
||||
)
|
||||
}
|
||||
|
||||
validateMixQuantities(mixMixTypes + mixMaterials)
|
||||
return MixQuantitiesDto(mixMaterials, mixMixTypes)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO check if required
|
||||
class InvalidMixMaterialsPositionsException(
|
||||
val errors: Set<InvalidPositionError>
|
||||
) : RestException(
|
||||
"invalid-mixmaterial-position",
|
||||
"Invalid mix materials positions",
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"The position of mix materials are invalid",
|
||||
mapOf(
|
||||
"invalidMixMaterials" to errors
|
||||
)
|
||||
)
|
||||
|
||||
class InvalidFirstMixMaterialException(
|
||||
val mixMaterial: MixQuantityDto
|
||||
) : RestException(
|
||||
"invalid-mixmaterial-first",
|
||||
"Invalid first mix material",
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"The first mix material is invalid because its material must not be expressed in percents",
|
||||
mapOf(
|
||||
"mixMaterial" to mixMaterial
|
||||
)
|
||||
)
|
@ -1,59 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.logic
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.MixTypeDto
|
||||
import dev.fyloz.colorrecipesexplorer.service.MixTypeService
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
interface MixTypeLogic : Logic<MixTypeDto, MixTypeService> {
|
||||
/** Returns a mix type for the given [name] and [materialType]. If this mix type does not already exist, it will be created. */
|
||||
fun getOrCreateForNameAndMaterialType(name: String, materialType: MaterialTypeDto): MixTypeDto
|
||||
|
||||
/** Updates the [mixType] with the given [name] and [materialType], or create a new one if it is shared with other mixes. */
|
||||
fun updateOrCreateForNameAndMaterialType(
|
||||
mixType: MixTypeDto,
|
||||
name: String,
|
||||
materialType: MaterialTypeDto
|
||||
): MixTypeDto
|
||||
}
|
||||
|
||||
@LogicComponent
|
||||
class DefaultMixTypeLogic(service: MixTypeService) : BaseLogic<MixTypeDto, MixTypeService>(service, Constants.ModelNames.MIX_TYPE), MixTypeLogic {
|
||||
@Transactional
|
||||
override fun getOrCreateForNameAndMaterialType(name: String, materialType: MaterialTypeDto) =
|
||||
service.getByNameAndMaterialType(name, materialType.id) ?: saveForNameAndMaterialType(name, materialType)
|
||||
|
||||
override fun updateOrCreateForNameAndMaterialType(
|
||||
mixType: MixTypeDto,
|
||||
name: String,
|
||||
materialType: MaterialTypeDto
|
||||
) = if (service.existsByNameAndMaterialType(name, materialType.id, mixType.id)) {
|
||||
service.getByNameAndMaterialType(name, materialType.id)!!
|
||||
} else if (service.isShared(mixType.id)) {
|
||||
saveForNameAndMaterialType(name, materialType)
|
||||
} else {
|
||||
updateForNameAndMaterialType(mixType, name, materialType)
|
||||
}
|
||||
|
||||
override fun deleteById(id: Long) {
|
||||
if (service.isUsedByMixes(id)) {
|
||||
throw cannotDeleteException("Cannot delete the mix type with the id '$id' because one or more mixes depends on it")
|
||||
}
|
||||
|
||||
super.deleteById(id)
|
||||
}
|
||||
|
||||
private fun saveForNameAndMaterialType(name: String, materialType: MaterialTypeDto): MixTypeDto {
|
||||
return save(MixTypeDto(name = name, materialType = materialType))
|
||||
}
|
||||
|
||||
private fun updateForNameAndMaterialType(
|
||||
mixType: MixTypeDto,
|
||||
name: String,
|
||||
materialType: MaterialTypeDto
|
||||
): MixTypeDto {
|
||||
return update(mixType.copy(name = name, materialType = materialType, material = mixType.material))
|
||||
}
|
||||
}
|
@ -1,190 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.logic
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.*
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic
|
||||
import dev.fyloz.colorrecipesexplorer.service.RecipeService
|
||||
import dev.fyloz.colorrecipesexplorer.utils.collections.LazyMapList
|
||||
import dev.fyloz.colorrecipesexplorer.utils.merge
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
|
||||
interface RecipeLogic : Logic<RecipeDto, RecipeService> {
|
||||
/** Gets all recipes and load their mixes and groupsInformation, to prevent LazyInitializationExceptions */
|
||||
fun getAllWithMixesAndGroupsInformation(): Collection<RecipeDto>
|
||||
|
||||
/** Gets all recipes with the given [name]. */
|
||||
fun getAllByName(name: String): Collection<RecipeDto>
|
||||
|
||||
/** Saves the given [dto]. */
|
||||
fun save(dto: RecipeSaveDto): RecipeDto
|
||||
|
||||
/** Updates the given [dto]. */
|
||||
fun update(dto: RecipeUpdateDto): RecipeDto
|
||||
|
||||
/** Updates the public data of a recipe with the given [publicDataDto]. */
|
||||
fun updatePublicData(publicDataDto: RecipePublicDataDto)
|
||||
}
|
||||
|
||||
@LogicComponent
|
||||
class DefaultRecipeLogic(
|
||||
service: RecipeService,
|
||||
private val companyLogic: CompanyLogic,
|
||||
private val recipeStepLogic: RecipeStepLogic,
|
||||
private val mixLogic: MixLogic,
|
||||
private val groupLogic: GroupLogic
|
||||
) : BaseLogic<RecipeDto, RecipeService>(service, Constants.ModelNames.RECIPE), RecipeLogic {
|
||||
@Transactional
|
||||
override fun getAllWithMixesAndGroupsInformation() =
|
||||
getAll().onEach { (it.mixes as LazyMapList<*, *>).initialize() }
|
||||
.onEach { (it.groupsInformation as LazyMapList<*, *>).initialize() }
|
||||
|
||||
override fun getAllByName(name: String) = service.getAllByName(name)
|
||||
|
||||
override fun save(dto: RecipeSaveDto) = save(
|
||||
RecipeDto(
|
||||
name = dto.name,
|
||||
description = dto.description,
|
||||
color = dto.color,
|
||||
gloss = dto.gloss,
|
||||
sample = dto.sample,
|
||||
approbationDate = dto.approbationDate,
|
||||
approbationExpired = false,
|
||||
remark = dto.remark ?: "",
|
||||
company = companyLogic.getById(dto.companyId),
|
||||
mixes = listOf(),
|
||||
groupsInformation = listOf()
|
||||
)
|
||||
)
|
||||
|
||||
override fun save(dto: RecipeDto): RecipeDto {
|
||||
throwIfNameAndCompanyAlreadyExists(dto.name, dto.company.id)
|
||||
|
||||
return super.save(dto)
|
||||
}
|
||||
|
||||
override fun update(dto: RecipeUpdateDto): RecipeDto {
|
||||
val recipe = getById(dto.id)
|
||||
|
||||
return update(
|
||||
RecipeDto(
|
||||
id = dto.id,
|
||||
name = dto.name,
|
||||
description = dto.description,
|
||||
color = dto.color,
|
||||
gloss = dto.gloss,
|
||||
sample = dto.sample,
|
||||
approbationDate = dto.approbationDate,
|
||||
approbationExpired = false,
|
||||
remark = dto.remark ?: "",
|
||||
company = recipe.company,
|
||||
mixes = recipe.mixes,
|
||||
groupsInformation = updateGroupsInformationSteps(recipe, dto)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun update(dto: RecipeDto): RecipeDto {
|
||||
throwIfNameAndCompanyAlreadyExists(dto.name, dto.company.id, dto.id)
|
||||
|
||||
return super.update(dto)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
override fun updatePublicData(publicDataDto: RecipePublicDataDto) {
|
||||
// Update notes
|
||||
if (publicDataDto.notes.isNotEmpty()) {
|
||||
val recipe = getById(publicDataDto.recipeId)
|
||||
update(recipe.copy(groupsInformation = updateGroupsInformationNotes(recipe, publicDataDto.notes)))
|
||||
}
|
||||
|
||||
// Update mixes locations
|
||||
if (publicDataDto.mixesLocation.isNotEmpty()) {
|
||||
mixLogic.updateLocations(publicDataDto.mixesLocation)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateGroupsInformationSteps(recipe: RecipeDto, dto: RecipeUpdateDto): List<RecipeGroupInformationDto> {
|
||||
val updatedGroupsInformation = dto.steps.map { updateGroupInformationSteps(recipe, it) }
|
||||
return recipe.groupsInformation.merge(updatedGroupsInformation)
|
||||
}
|
||||
|
||||
private fun updateGroupInformationSteps(recipe: RecipeDto, groupSteps: RecipeGroupStepsDto) =
|
||||
getOrCreateGroupInformation(recipe, groupSteps.groupId).copy(steps = groupSteps.steps).also {
|
||||
recipeStepLogic.validateGroupInformationSteps(it)
|
||||
}
|
||||
|
||||
private fun updateGroupsInformationNotes(
|
||||
recipe: RecipeDto, notes: List<RecipeGroupNoteDto>
|
||||
): List<RecipeGroupInformationDto> {
|
||||
val updatedGroupsInformation = notes.map { updateGroupInformationNote(recipe, it) }
|
||||
return recipe.groupsInformation.merge(updatedGroupsInformation)
|
||||
}
|
||||
|
||||
private fun updateGroupInformationNote(recipe: RecipeDto, groupNote: RecipeGroupNoteDto) =
|
||||
getOrCreateGroupInformation(recipe, groupNote.groupId).copy(note = groupNote.content)
|
||||
|
||||
private fun getOrCreateGroupInformation(recipe: RecipeDto, groupId: Long) =
|
||||
recipe.groupsInformation.firstOrNull { it.group.id == groupId }
|
||||
?: RecipeGroupInformationDto(group = groupLogic.getById(groupId))
|
||||
|
||||
private fun throwIfNameAndCompanyAlreadyExists(name: String, companyId: Long, id: Long? = null) {
|
||||
if (service.existsByNameAndCompany(name, companyId, id)) {
|
||||
throw AlreadyExistsException(
|
||||
"$typeNameLowerCase-company",
|
||||
"$typeName already exists",
|
||||
"A recipe with the name '$name' already exists for the company with the id '$companyId'",
|
||||
name,
|
||||
NAME_IDENTIFIER_NAME,
|
||||
mutableMapOf(
|
||||
"companyId" to companyId
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface RecipeImageLogic {
|
||||
/** Gets the id of every image associated to the recipe with the given [recipeId]. */
|
||||
fun getAllImages(recipeId: Long): List<String>
|
||||
|
||||
/** Saves the given [image] and associate it to the recipe with the given [recipeId]. Returns the id of the saved image. */
|
||||
fun download(image: MultipartFile, recipeId: Long): String
|
||||
|
||||
/** Deletes the image with the given [id] for the given [recipeId]. */
|
||||
fun delete(recipeId: Long, id: String)
|
||||
}
|
||||
|
||||
@LogicComponent
|
||||
class DefaultRecipeImageLogic(val fileLogic: WriteableFileLogic) : RecipeImageLogic {
|
||||
override fun getAllImages(recipeId: Long) =
|
||||
fileLogic.listDirectoryFiles(getRecipeImagesDirectory(recipeId)).map { it.name }
|
||||
|
||||
override fun download(image: MultipartFile, recipeId: Long): String {
|
||||
/** Gets the next id available for a new image for the given [recipeId]. */
|
||||
fun getNextAvailableId(): String = with(getAllImages(recipeId)) {
|
||||
val currentIds = mapNotNull { it.toLongOrNull() }
|
||||
if (currentIds.isEmpty()) {
|
||||
return 0.toString()
|
||||
}
|
||||
|
||||
val nextId = currentIds.maxOf { it } + 1L
|
||||
return nextId.toString()
|
||||
}
|
||||
|
||||
return getNextAvailableId().also {
|
||||
val imagePath = getImagePath(recipeId, it)
|
||||
fileLogic.writeToDirectory(image, imagePath, getRecipeImagesDirectory(recipeId), true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun delete(recipeId: Long, id: String) =
|
||||
fileLogic.deleteFromDirectory(getImagePath(recipeId, id), getRecipeImagesDirectory(recipeId))
|
||||
|
||||
private fun getImagePath(recipeId: Long, id: String) = "${getRecipeImagesDirectory(recipeId)}/$id"
|
||||
|
||||
private fun getRecipeImagesDirectory(recipeId: Long) = "${Constants.FilePaths.RECIPE_IMAGES}/$recipeId"
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.logic
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeGroupInformationDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto
|
||||
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError
|
||||
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.RestException
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Group
|
||||
import dev.fyloz.colorrecipesexplorer.service.RecipeStepService
|
||||
import dev.fyloz.colorrecipesexplorer.utils.PositionUtils
|
||||
import org.springframework.http.HttpStatus
|
||||
|
||||
interface RecipeStepLogic : Logic<RecipeStepDto, RecipeStepService> {
|
||||
/** Validates the steps of the given [groupInformation], according to the criteria of [PositionUtils.validate]. */
|
||||
fun validateGroupInformationSteps(groupInformation: RecipeGroupInformationDto)
|
||||
}
|
||||
|
||||
@LogicComponent
|
||||
class DefaultRecipeStepLogic(recipeStepService: RecipeStepService) :
|
||||
BaseLogic<RecipeStepDto, RecipeStepService>(recipeStepService, Constants.ModelNames.RECIPE_STEP), RecipeStepLogic {
|
||||
override fun validateGroupInformationSteps(groupInformation: RecipeGroupInformationDto) {
|
||||
try {
|
||||
PositionUtils.validate(groupInformation.steps.map { it.position }.toList())
|
||||
} catch (ex: InvalidPositionsException) {
|
||||
throw InvalidGroupStepsPositionsException(groupInformation.group, ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidGroupStepsPositionsException(
|
||||
val group: GroupDto,
|
||||
val exception: InvalidPositionsException
|
||||
) : RestException(
|
||||
"invalid-groupinformation-recipestep-position",
|
||||
"Invalid steps positions",
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"The position of steps for the group ${group.name} are invalid",
|
||||
mapOf(
|
||||
"group" to group.name,
|
||||
"groupId" to group.id,
|
||||
"invalidSteps" to exception.errors
|
||||
)
|
||||
) {
|
||||
val errors: Set<InvalidPositionError>
|
||||
get() = exception.errors
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.logic
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.TouchUpKitDto
|
||||
import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic
|
||||
import dev.fyloz.colorrecipesexplorer.model.ConfigurationType
|
||||
import dev.fyloz.colorrecipesexplorer.service.TouchUpKitService
|
||||
import dev.fyloz.colorrecipesexplorer.utils.*
|
||||
import org.springframework.core.io.ByteArrayResource
|
||||
import org.springframework.core.io.Resource
|
||||
import java.time.LocalDate
|
||||
|
||||
interface TouchUpKitLogic : Logic<TouchUpKitDto, TouchUpKitService> {
|
||||
/** Sets the touch up kit with the given [id] as complete. */
|
||||
fun complete(id: Long)
|
||||
|
||||
/**
|
||||
* Generates and returns a [PdfDocument] for the given [job] as a [ByteArrayResource].
|
||||
*
|
||||
* If TOUCH_UP_KIT_CACHE_PDF is enabled and a file exists for the job, its content will be returned.
|
||||
* If caching is enabled but no file exists for the job, the generated ByteArrayResource will be cached on the disk.
|
||||
*/
|
||||
fun generateJobPdfResource(job: String): Resource
|
||||
|
||||
/** Generates and returns a [PdfDocument] for the given [job]. */
|
||||
fun generateJobPdf(job: String): PdfDocument
|
||||
|
||||
/** Writes the given [pdf] to the disk if TOUCH_UP_KIT_CACHE_PDF is enabled. */
|
||||
fun cacheJobPdf(job: String, pdf: PdfDocument)
|
||||
}
|
||||
|
||||
@LogicComponent
|
||||
class DefaultTouchUpKitLogic(
|
||||
service: TouchUpKitService,
|
||||
private val fileLogic: WriteableFileLogic,
|
||||
private val configLogic: ConfigurationLogic
|
||||
) : BaseLogic<TouchUpKitDto, TouchUpKitService>(service, Constants.ModelNames.TOUCH_UP_KIT), TouchUpKitLogic {
|
||||
private val cacheGeneratedFiles by lazy {
|
||||
configLogic.getContent(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) == true.toString()
|
||||
}
|
||||
|
||||
override fun complete(id: Long) = service.updateCompletionDateById(id, LocalDate.now())
|
||||
|
||||
override fun generateJobPdfResource(job: String): Resource {
|
||||
if (cacheGeneratedFiles) {
|
||||
val pdfPath = jobPdfPath(job)
|
||||
if (fileLogic.exists(pdfPath)) {
|
||||
return fileLogic.read(pdfPath)
|
||||
}
|
||||
}
|
||||
|
||||
val pdf = generateJobPdf(job)
|
||||
cacheJobPdf(job, pdf)
|
||||
|
||||
return pdf.toByteArrayResource()
|
||||
}
|
||||
|
||||
override fun generateJobPdf(job: String) = pdf {
|
||||
container {
|
||||
centeredVertically = true
|
||||
drawContainerBottom = true
|
||||
text(TOUCH_UP_TEXT_FR) {
|
||||
bold = true
|
||||
fontSize = PDF_DEFAULT_FONT_SIZE + 12
|
||||
}
|
||||
text(TOUCH_UP_TEXT_EN) {
|
||||
bold = true
|
||||
fontSize = PDF_DEFAULT_FONT_SIZE + 12
|
||||
}
|
||||
text(job) {
|
||||
marginTop = 10f
|
||||
}
|
||||
}
|
||||
|
||||
container(containers[0]) {
|
||||
drawContainerBottom = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun cacheJobPdf(job: String, pdf: PdfDocument) {
|
||||
if (!cacheGeneratedFiles) return
|
||||
|
||||
fileLogic.write(pdf.toByteArrayResource(), jobPdfPath(job), true)
|
||||
}
|
||||
|
||||
private fun jobPdfPath(job: String) =
|
||||
"${Constants.FilePaths.TOUCH_UP_KITS}/$job.pdf"
|
||||
|
||||
companion object {
|
||||
const val TOUCH_UP_TEXT_FR = "KIT DE RETOUCHE"
|
||||
const val TOUCH_UP_TEXT_EN = "TOUCH UP KIT"
|
||||
}
|
||||
}
|
@ -1,253 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.logic.config
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
|
||||
import dev.fyloz.colorrecipesexplorer.logic.files.ResourceFileLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic
|
||||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import dev.fyloz.colorrecipesexplorer.utils.decrypt
|
||||
import dev.fyloz.colorrecipesexplorer.utils.encrypt
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.core.io.Resource
|
||||
import org.springframework.security.crypto.keygen.KeyGenerators
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
|
||||
interface ConfigurationLogic {
|
||||
/** Gets all set configurations. */
|
||||
fun getAll(): List<ConfigurationBase>
|
||||
|
||||
/**
|
||||
* Gets all configurations with keys contained in the given [formattedKeyList].
|
||||
* The [formattedKeyList] contains wanted configuration keys separated by a semi-colon.
|
||||
*/
|
||||
fun getAll(formattedKeyList: String): List<ConfigurationBase>
|
||||
|
||||
/**
|
||||
* Gets the configuration with the given [key].
|
||||
* If the [key] does not exists, an [InvalidConfigurationKeyException] will be thrown.
|
||||
*/
|
||||
fun get(key: String): ConfigurationBase
|
||||
|
||||
/** Gets the configuration with the given [type]. */
|
||||
fun get(type: ConfigurationType): ConfigurationBase
|
||||
|
||||
/** Gets the content of the configuration with the given [type]. */
|
||||
fun getContent(type: ConfigurationType): String
|
||||
|
||||
/** Gets the content of the secure configuration with the given [type]. Should not be accessible to the users. */
|
||||
fun getSecure(type: ConfigurationType): String
|
||||
|
||||
/** Gets the app's icon. */
|
||||
fun getConfiguredIcon(): Resource
|
||||
|
||||
/** Gets the app's logo. */
|
||||
fun getConfiguredLogo(): Resource
|
||||
|
||||
/** Sets the content of each configuration in the given [configurations] list. */
|
||||
fun set(configurations: List<ConfigurationDto>)
|
||||
|
||||
/**
|
||||
* Sets the content of the configuration matching the given [configuration].
|
||||
* If the given key does not exists, an [InvalidConfigurationKeyException] will be thrown.
|
||||
*/
|
||||
fun set(configuration: ConfigurationDto)
|
||||
|
||||
/** Sets the content given [configuration]. */
|
||||
fun set(configuration: Configuration)
|
||||
|
||||
/** Sets the app's icon. */
|
||||
fun setConfiguredIcon(icon: MultipartFile)
|
||||
|
||||
/** Sets the app's logo. */
|
||||
fun setConfiguredLogo(logo: MultipartFile)
|
||||
|
||||
/** Initialize the properties matching the given [predicate]. */
|
||||
fun initializeProperties(predicate: (ConfigurationType) -> Boolean)
|
||||
}
|
||||
|
||||
const val CONFIGURATION_LOGO_RESOURCE_PATH = "images/logo.png"
|
||||
const val CONFIGURATION_LOGO_FILE_PATH = "images/logo"
|
||||
const val CONFIGURATION_ICON_RESOURCE_PATH = "images/icon.png"
|
||||
const val CONFIGURATION_ICON_FILE_PATH = "images/icon"
|
||||
const val CONFIGURATION_FORMATTED_LIST_DELIMITER = ';'
|
||||
|
||||
@Service("configurationService")
|
||||
class DefaultConfigurationLogic(
|
||||
@Lazy private val fileService: WriteableFileLogic,
|
||||
private val resourceFileService: ResourceFileLogic,
|
||||
private val configurationSource: ConfigurationSource,
|
||||
private val securityProperties: CreSecurityProperties
|
||||
) : ConfigurationLogic {
|
||||
private val logger = KotlinLogging.logger { }
|
||||
private val saltConfigurationType = ConfigurationType.GENERATED_ENCRYPTION_SALT
|
||||
private val encryptionSalt by lazy {
|
||||
securityProperties.configSalt ?: getGeneratedSalt()
|
||||
}
|
||||
|
||||
override fun getAll() =
|
||||
ConfigurationType.values().mapNotNull {
|
||||
try {
|
||||
get(it)
|
||||
} catch (_: ConfigurationNotSetException) {
|
||||
null
|
||||
} catch (_: InvalidConfigurationKeyException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAll(formattedKeyList: String) =
|
||||
formattedKeyList.split(CONFIGURATION_FORMATTED_LIST_DELIMITER).mapNotNull {
|
||||
try {
|
||||
get(it)
|
||||
} catch (_: ConfigurationNotSetException) {
|
||||
null
|
||||
} catch (_: InvalidConfigurationKeyException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun get(key: String) =
|
||||
get(key.toConfigurationType())
|
||||
|
||||
override fun get(type: ConfigurationType): ConfigurationBase {
|
||||
// Encryption salt should never be returned, but cannot be set as "secure" without encrypting it
|
||||
if (type == ConfigurationType.GENERATED_ENCRYPTION_SALT) throw InvalidConfigurationKeyException(type.key)
|
||||
|
||||
val configuration = configurationSource.get(type) ?: throw ConfigurationNotSetException(type)
|
||||
return if (type.secure) {
|
||||
secureConfiguration(configuration)
|
||||
} else {
|
||||
configuration
|
||||
}
|
||||
}
|
||||
|
||||
override fun getContent(type: ConfigurationType): String {
|
||||
val configuration = get(type)
|
||||
if (configuration is SecureConfiguration) throw UnsupportedOperationException("Cannot get '${type.key}' configuration content because it is secure")
|
||||
|
||||
return (configuration as Configuration).content
|
||||
}
|
||||
|
||||
override fun getSecure(type: ConfigurationType): String {
|
||||
if (!type.secure) throw UnsupportedOperationException("Cannot get configuration of type '${type.key}' because it is not a secure configuration")
|
||||
|
||||
val configuration = configurationSource.get(type) ?: throw ConfigurationNotSetException(type)
|
||||
return decryptConfiguration(configuration).content
|
||||
}
|
||||
|
||||
override fun getConfiguredIcon() =
|
||||
getConfiguredImage(
|
||||
type = ConfigurationType.INSTANCE_ICON_SET,
|
||||
filePath = CONFIGURATION_ICON_FILE_PATH,
|
||||
resourcePath = CONFIGURATION_ICON_RESOURCE_PATH
|
||||
)
|
||||
|
||||
override fun getConfiguredLogo() =
|
||||
getConfiguredImage(
|
||||
type = ConfigurationType.INSTANCE_LOGO_SET,
|
||||
filePath = CONFIGURATION_LOGO_FILE_PATH,
|
||||
resourcePath = CONFIGURATION_LOGO_RESOURCE_PATH
|
||||
)
|
||||
|
||||
private fun getConfiguredImage(type: ConfigurationType, filePath: String, resourcePath: String) =
|
||||
with(get(type) as Configuration) {
|
||||
if (this.content == true.toString()) {
|
||||
fileService.read(filePath)
|
||||
} else {
|
||||
resourceFileService.read(resourcePath)
|
||||
}
|
||||
}
|
||||
|
||||
override fun set(configurations: List<ConfigurationDto>) {
|
||||
configurationSource.set(
|
||||
configurations
|
||||
.map(::configuration)
|
||||
.map(this::encryptConfigurationIfSecure)
|
||||
)
|
||||
}
|
||||
|
||||
override fun set(configuration: ConfigurationDto) =
|
||||
set(configuration(configuration))
|
||||
|
||||
override fun set(configuration: Configuration) {
|
||||
configurationSource.set(encryptConfigurationIfSecure(configuration))
|
||||
}
|
||||
|
||||
override fun setConfiguredIcon(icon: MultipartFile) =
|
||||
setConfiguredImage(icon, CONFIGURATION_ICON_FILE_PATH, ConfigurationType.INSTANCE_ICON_SET)
|
||||
|
||||
override fun setConfiguredLogo(logo: MultipartFile) =
|
||||
setConfiguredImage(logo, CONFIGURATION_LOGO_FILE_PATH, ConfigurationType.INSTANCE_LOGO_SET)
|
||||
|
||||
private fun setConfiguredImage(image: MultipartFile, path: String, type: ConfigurationType) {
|
||||
fileService.write(image, path, true)
|
||||
set(configuration(type, content = true.toString()))
|
||||
}
|
||||
|
||||
override fun initializeProperties(predicate: (ConfigurationType) -> Boolean) {
|
||||
ConfigurationType.values()
|
||||
.filter(predicate)
|
||||
.filter { !it.computed } // Can't initialize computed configurations
|
||||
.filter { it != ConfigurationType.GENERATED_ENCRYPTION_SALT }
|
||||
.forEach {
|
||||
try {
|
||||
get(it)
|
||||
} catch (_: ConfigurationNotSetException) {
|
||||
with(it.defaultContent) {
|
||||
if (this != null) { // Ignores configurations with null default values
|
||||
logger.info("Configuration ${it.key} was not set and will be initialized to a default value")
|
||||
set(configuration(type = it, content = this.toString()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun encryptConfigurationIfSecure(configuration: Configuration) =
|
||||
with(configuration) {
|
||||
if (type.secure) {
|
||||
encryptConfiguration(this)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
private fun encryptConfiguration(configuration: Configuration) =
|
||||
with(configuration) {
|
||||
configuration(
|
||||
type = type,
|
||||
content = content.encrypt(type.key, encryptionSalt)
|
||||
)
|
||||
}
|
||||
|
||||
private fun decryptConfiguration(configuration: Configuration) =
|
||||
with(configuration) {
|
||||
try {
|
||||
configuration(
|
||||
type = type,
|
||||
content = content.decrypt(type.key, encryptionSalt)
|
||||
)
|
||||
} catch (ex: IllegalStateException) {
|
||||
logger.error(
|
||||
"Could not read encrypted configuration, using default value. Are you using the correct salt?",
|
||||
ex
|
||||
)
|
||||
configuration(type = type)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getGeneratedSalt(): String {
|
||||
logger.warn("Sensitives configurations encryption salt was not configured, using generated salt")
|
||||
logger.warn("Consider configuring the encryption salt. More details at: https://cre.fyloz.dev/docs/Configuration/S%C3%A9curit%C3%A9/#sel")
|
||||
|
||||
var saltConfiguration = configurationSource.get(saltConfigurationType)
|
||||
if (saltConfiguration == null) {
|
||||
val generatedSalt = KeyGenerators.string().generateKey()
|
||||
saltConfiguration = configuration(type = saltConfigurationType, content = generatedSalt)
|
||||
configurationSource.set(saltConfiguration)
|
||||
}
|
||||
|
||||
return saltConfiguration.content
|
||||
}
|
||||
}
|
@ -1,171 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.logic.config
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.JavaFile
|
||||
import dev.fyloz.colorrecipesexplorer.SUPPORTED_DATABASE_VERSION
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import dev.fyloz.colorrecipesexplorer.emergencyMode
|
||||
import dev.fyloz.colorrecipesexplorer.model.CannotSetComputedConfigurationException
|
||||
import dev.fyloz.colorrecipesexplorer.model.Configuration
|
||||
import dev.fyloz.colorrecipesexplorer.model.ConfigurationType
|
||||
import dev.fyloz.colorrecipesexplorer.model.configuration
|
||||
import dev.fyloz.colorrecipesexplorer.repository.ConfigurationRepository
|
||||
import dev.fyloz.colorrecipesexplorer.utils.create
|
||||
import dev.fyloz.colorrecipesexplorer.utils.excludeAll
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.boot.info.BuildProperties
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Component
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.util.*
|
||||
|
||||
const val CONFIGURATION_FILE_PATH = "config.properties"
|
||||
const val CONFIGURATION_FILE_COMMENT = "---Color Recipes Explorer configuration---"
|
||||
|
||||
interface ConfigurationSource {
|
||||
fun get(type: ConfigurationType): Configuration?
|
||||
fun set(configuration: Configuration)
|
||||
fun set(configurations: Iterable<Configuration>)
|
||||
}
|
||||
|
||||
@Component("configurationSource")
|
||||
class CompositeConfigurationSource(
|
||||
@Lazy private val configurationRepository: ConfigurationRepository,
|
||||
private val properties: CreProperties,
|
||||
private val buildInfo: BuildProperties
|
||||
) : ConfigurationSource {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
private val repository by lazy { RepositoryConfigurationSource(configurationRepository) }
|
||||
private val file by lazy {
|
||||
FileConfigurationSource("${properties.configDirectory}/$CONFIGURATION_FILE_PATH")
|
||||
}
|
||||
private val computed by lazy {
|
||||
ComputedConfigurationSource(buildInfo)
|
||||
}
|
||||
|
||||
override fun get(type: ConfigurationType) =
|
||||
when {
|
||||
type.file -> file.get(type)
|
||||
type.computed -> computed.get(type)
|
||||
!emergencyMode -> repository.get(type)
|
||||
else -> null
|
||||
}
|
||||
|
||||
override fun set(configuration: Configuration) =
|
||||
when {
|
||||
configuration.type.file -> file.set(configuration)
|
||||
configuration.type.computed -> throw CannotSetComputedConfigurationException(configuration.type)
|
||||
!emergencyMode -> repository.set(configuration)
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
|
||||
override fun set(configurations: Iterable<Configuration>) {
|
||||
val mutableConfigurations = configurations.toMutableList()
|
||||
val fileConfigurations = mutableConfigurations.excludeAll { it.type.file }
|
||||
val repositoryConfigurations = mutableConfigurations.excludeAll { !emergencyMode }
|
||||
|
||||
repository.set(repositoryConfigurations)
|
||||
file.set(fileConfigurations)
|
||||
|
||||
mutableConfigurations.forEach {
|
||||
logger.warn("Could not find where to store updated value of configuration '${it.key}'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class RepositoryConfigurationSource(
|
||||
private val repository: ConfigurationRepository
|
||||
) : ConfigurationSource {
|
||||
override fun get(type: ConfigurationType) =
|
||||
repository.findByIdOrNull(type.key)?.toConfiguration()
|
||||
|
||||
override fun set(configuration: Configuration) {
|
||||
repository.save(configuration.toEntity())
|
||||
}
|
||||
|
||||
override fun set(configurations: Iterable<Configuration>) =
|
||||
configurations.forEach { set(it) }
|
||||
}
|
||||
|
||||
private class FileConfigurationSource(
|
||||
private val configFilePath: String
|
||||
) : ConfigurationSource {
|
||||
private val properties = Properties().apply {
|
||||
with(JavaFile(configFilePath)) {
|
||||
if (!this.exists()) this.create()
|
||||
FileInputStream(this).use {
|
||||
this@apply.load(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun get(type: ConfigurationType) =
|
||||
if (properties.containsKey(type.key))
|
||||
configuration(
|
||||
type,
|
||||
getConfigurationContent(type.key),
|
||||
LocalDateTime.parse(getConfigurationContent(configurationLastUpdateKey(type.key)))
|
||||
)
|
||||
else null
|
||||
|
||||
override fun set(configuration: Configuration) {
|
||||
setConfigurationContent(configuration.type.key, configuration.content)
|
||||
save()
|
||||
}
|
||||
|
||||
override fun set(configurations: Iterable<Configuration>) {
|
||||
configurations.forEach {
|
||||
setConfigurationContent(it.type.key, it.content)
|
||||
}
|
||||
save()
|
||||
}
|
||||
|
||||
fun save() {
|
||||
FileOutputStream(configFilePath).use {
|
||||
properties.store(it, CONFIGURATION_FILE_COMMENT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfigurationContent(key: String) =
|
||||
properties[key] as String
|
||||
|
||||
private fun setConfigurationContent(key: String, content: String) {
|
||||
properties[key] = content
|
||||
properties[configurationLastUpdateKey(key)] = LocalDateTime.now().toString()
|
||||
}
|
||||
|
||||
private fun configurationLastUpdateKey(key: String) = "$key.last-updated"
|
||||
}
|
||||
|
||||
private class ComputedConfigurationSource(
|
||||
private val buildInfo: BuildProperties
|
||||
) : ConfigurationSource {
|
||||
override fun get(type: ConfigurationType) = configuration(
|
||||
type, when (type) {
|
||||
ConfigurationType.EMERGENCY_MODE_ENABLED -> emergencyMode
|
||||
ConfigurationType.BUILD_VERSION -> buildInfo.version
|
||||
ConfigurationType.BUILD_TIME -> LocalDate.ofInstant(buildInfo.time, ZoneId.systemDefault()).toString()
|
||||
ConfigurationType.DATABASE_SUPPORTED_VERSION -> SUPPORTED_DATABASE_VERSION
|
||||
ConfigurationType.JAVA_VERSION -> Runtime.version()
|
||||
ConfigurationType.OPERATING_SYSTEM -> "${System.getProperty("os.name")} ${System.getProperty("os.version")} ${
|
||||
System.getProperty(
|
||||
"os.arch"
|
||||
)
|
||||
}"
|
||||
else -> throw IllegalArgumentException("Cannot get the value of the configuration with the key ${type.key} because it is not a computed configuration")
|
||||
}.toString()
|
||||
)
|
||||
|
||||
override fun set(configuration: Configuration) {
|
||||
throw UnsupportedOperationException("Cannot set computed configurations")
|
||||
}
|
||||
|
||||
override fun set(configurations: Iterable<Configuration>) {
|
||||
throw UnsupportedOperationException("Cannot set computed configurations")
|
||||
}
|
||||
}
|
@ -1,184 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.logic.files
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.JavaFile
|
||||
import dev.fyloz.colorrecipesexplorer.utils.File
|
||||
import dev.fyloz.colorrecipesexplorer.utils.FilePath
|
||||
import dev.fyloz.memorycache.MemoryCache
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
interface FileCache {
|
||||
/** Checks if the cache contains the given [path]. */
|
||||
operator fun contains(path: FilePath): Boolean
|
||||
|
||||
/** Gets the cached file system item at the given [path]. */
|
||||
operator fun get(path: FilePath): CachedFileSystemItem?
|
||||
|
||||
/** Gets the cached directory at the given [path]. */
|
||||
fun getDirectory(path: FilePath): CachedDirectory?
|
||||
|
||||
/** Gets the cached file at the given [path]. */
|
||||
fun getFile(path: FilePath): CachedFile?
|
||||
|
||||
/** Checks if the cached file system item at the given [path] exists. */
|
||||
fun exists(path: FilePath): Boolean
|
||||
|
||||
/** Checks if the cached directory at the given [path] exists. */
|
||||
fun directoryExists(path: FilePath): Boolean
|
||||
|
||||
/** Checks if the cached file at the given [path] exists. */
|
||||
fun fileExists(path: FilePath): Boolean
|
||||
|
||||
/** Sets the file system item at the given [path] as existing or not. Loads the item in the cache if not already present. */
|
||||
fun setExists(path: FilePath, exists: Boolean = true)
|
||||
|
||||
/** Loads the file system item at the given [path] into the cache. */
|
||||
fun load(path: FilePath)
|
||||
|
||||
/** Adds the file system item at the given [itemPath] to the cached directory at the given [directoryPath]. */
|
||||
fun addItemToDirectory(directoryPath: FilePath, itemPath: FilePath)
|
||||
|
||||
/** Removes the file system item at the given [itemPath] from the cached directory at the given [directoryPath]. */
|
||||
fun removeItemFromDirectory(directoryPath: FilePath, itemPath: FilePath)
|
||||
}
|
||||
|
||||
@Component
|
||||
class DefaultFileCache(private val cache: MemoryCache<String, CachedFileSystemItem>) : FileCache {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
override operator fun contains(path: FilePath) =
|
||||
path.value in cache
|
||||
|
||||
override operator fun get(path: FilePath) =
|
||||
cache[path.value]
|
||||
|
||||
private operator fun set(path: FilePath, item: CachedFileSystemItem) {
|
||||
cache[path.value] = item
|
||||
}
|
||||
|
||||
override fun getDirectory(path: FilePath) =
|
||||
if (directoryExists(path)) {
|
||||
this[path] as CachedDirectory
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
override fun getFile(path: FilePath) =
|
||||
if (fileExists(path)) {
|
||||
this[path] as CachedFile
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
override fun exists(path: FilePath) =
|
||||
path in this && this[path]!!.exists
|
||||
|
||||
override fun directoryExists(path: FilePath) =
|
||||
exists(path) && this[path] is CachedDirectory
|
||||
|
||||
override fun fileExists(path: FilePath) =
|
||||
exists(path) && this[path] is CachedFile
|
||||
|
||||
override fun setExists(path: FilePath, exists: Boolean) {
|
||||
if (path !in this) {
|
||||
load(path)
|
||||
}
|
||||
|
||||
this[path] = this[path]!!.clone(exists = exists)
|
||||
logger.debug("Updated FileCache state: ${path.value} exists -> $exists")
|
||||
}
|
||||
|
||||
override fun load(path: FilePath) =
|
||||
with(JavaFile(path.value).toFileSystemItem()) {
|
||||
this@DefaultFileCache[path] = this
|
||||
|
||||
logger.debug("Loaded file at ${path.value} into FileCache")
|
||||
}
|
||||
|
||||
override fun addItemToDirectory(directoryPath: FilePath, itemPath: FilePath) {
|
||||
val directory = prepareDirectory(directoryPath) ?: return
|
||||
|
||||
val updatedContent = setOf(
|
||||
*directory.content.toTypedArray(),
|
||||
JavaFile(itemPath.value).toFileSystemItem()
|
||||
)
|
||||
|
||||
this[directoryPath] = directory.copy(content = updatedContent)
|
||||
logger.debug("Added child ${itemPath.value} to ${directoryPath.value} in FileCache")
|
||||
}
|
||||
|
||||
override fun removeItemFromDirectory(directoryPath: FilePath, itemPath: FilePath) {
|
||||
val directory = prepareDirectory(directoryPath) ?: return
|
||||
|
||||
val updatedContent = directory.content
|
||||
.filter { it.path.value != itemPath.value }
|
||||
.toSet()
|
||||
|
||||
this[directoryPath] = directory.copy(content = updatedContent)
|
||||
logger.debug("Removed child ${itemPath.value} from ${directoryPath.value} in FileCache")
|
||||
}
|
||||
|
||||
private fun prepareDirectory(path: FilePath): CachedDirectory? {
|
||||
if (!directoryExists(path)) {
|
||||
logger.warn("Cannot add child to ${path.value} because it is not in the cache")
|
||||
return null
|
||||
}
|
||||
|
||||
val directory = getDirectory(path)
|
||||
if (directory == null) {
|
||||
logger.warn("Cannot add child to ${path.value} because it is not a directory")
|
||||
return null
|
||||
}
|
||||
|
||||
return directory
|
||||
}
|
||||
}
|
||||
|
||||
interface CachedFileSystemItem {
|
||||
val name: String
|
||||
val path: FilePath
|
||||
val exists: Boolean
|
||||
|
||||
fun clone(exists: Boolean): CachedFileSystemItem
|
||||
}
|
||||
|
||||
data class CachedFile(
|
||||
override val name: String,
|
||||
override val path: FilePath,
|
||||
override val exists: Boolean
|
||||
) : CachedFileSystemItem {
|
||||
constructor(file: File) : this(file.name, file.toFilePath(), file.exists() && file.isFile)
|
||||
|
||||
override fun clone(exists: Boolean) =
|
||||
this.copy(exists = exists)
|
||||
}
|
||||
|
||||
data class CachedDirectory(
|
||||
override val name: String,
|
||||
override val path: FilePath,
|
||||
override val exists: Boolean,
|
||||
val content: Set<CachedFileSystemItem> = setOf()
|
||||
) : CachedFileSystemItem {
|
||||
constructor(file: File) : this(file.name, file.toFilePath(), file.exists() && file.isDirectory, file.fetchContent())
|
||||
|
||||
val contentFiles: Collection<CachedFile>
|
||||
get() = content.filterIsInstance<CachedFile>()
|
||||
|
||||
override fun clone(exists: Boolean) =
|
||||
this.copy(exists = exists)
|
||||
|
||||
companion object {
|
||||
private fun File.fetchContent() =
|
||||
(this.file.listFiles() ?: arrayOf<JavaFile>())
|
||||
.filterNotNull()
|
||||
.map { it.toFileSystemItem() }
|
||||
.toSet()
|
||||
}
|
||||
}
|
||||
|
||||
fun JavaFile.toFileSystemItem() =
|
||||
if (this.isDirectory) {
|
||||
CachedDirectory(File(this))
|
||||
} else {
|
||||
CachedFile(File(this))
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.logic.files
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.utils.FilePath
|
||||
import org.springframework.core.io.Resource
|
||||
import org.springframework.core.io.ResourceLoader
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class ResourceFileLogic(
|
||||
private val resourceLoader: ResourceLoader
|
||||
) : FileLogic {
|
||||
override fun exists(path: String) =
|
||||
fullPath(path).resource.exists()
|
||||
|
||||
override fun read(path: String): Resource =
|
||||
fullPath(path).resource.also {
|
||||
if (!it.exists()) {
|
||||
throw FileNotFoundException(path)
|
||||
}
|
||||
}
|
||||
|
||||
override fun listDirectoryFiles(path: String): Collection<CachedFile> {
|
||||
val content = fullPath(path).resource.file.listFiles() ?: return setOf()
|
||||
|
||||
return content
|
||||
.filterNotNull()
|
||||
.filter { it.isFile }
|
||||
.map { it.toFileSystemItem() as CachedFile }
|
||||
}
|
||||
|
||||
override fun fullPath(path: String) =
|
||||
FilePath("classpath:${path}")
|
||||
|
||||
val FilePath.resource: Resource
|
||||
get() = resourceLoader.getResource(this.value)
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.logic.jobs
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.logic.TouchUpKitLogic
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.scheduling.annotation.Scheduled
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
@Profile("!emergency")
|
||||
class TouchUpKitRemover(
|
||||
private val touchUpKitLogic: TouchUpKitLogic
|
||||
) {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@Scheduled(cron = "0 0 0 * * *")
|
||||
fun execute() {
|
||||
logger.debug("Executing expired touch up kits removal job... ")
|
||||
removeExpiredKits()
|
||||
}
|
||||
|
||||
private fun removeExpiredKits() {
|
||||
with(touchUpKitLogic.getAll().filter { it.expired }) {
|
||||
this.forEach {
|
||||
logger.debug("Removed expired touch up kit ${it.id} (${it.project} ${it.buggy})")
|
||||
touchUpKitLogic.deleteById(it.id)
|
||||
}
|
||||
logger.info("Removed ${this.size} expired touch up kits")
|
||||
}
|
||||
}
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.logic.users
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
|
||||
import dev.fyloz.colorrecipesexplorer.config.security.defaultGroupCookieName
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NoDefaultGroupException
|
||||
import dev.fyloz.colorrecipesexplorer.logic.BaseLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.Logic
|
||||
import dev.fyloz.colorrecipesexplorer.service.GroupService
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import org.springframework.web.util.WebUtils
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
const val defaultGroupCookieMaxAge = 10 * 365 * 24 * 60 * 60 // 10 ans
|
||||
|
||||
interface GroupLogic : Logic<GroupDto, GroupService> {
|
||||
/** Gets all the users of the group with the given [id]. */
|
||||
fun getUsersForGroup(id: Long): Collection<UserDto>
|
||||
|
||||
/** Gets the default group from a cookie in the given HTTP [request]. */
|
||||
fun getRequestDefaultGroup(request: HttpServletRequest): GroupDto
|
||||
|
||||
/** Sets the default group cookie for the given HTTP [response]. */
|
||||
fun setResponseDefaultGroup(id: Long, response: HttpServletResponse)
|
||||
}
|
||||
|
||||
@LogicComponent
|
||||
class DefaultGroupLogic(service: GroupService, private val userLogic: UserLogic) :
|
||||
BaseLogic<GroupDto, GroupService>(service, Constants.ModelNames.GROUP),
|
||||
GroupLogic {
|
||||
override fun getUsersForGroup(id: Long) = userLogic.getAllByGroup(getById(id))
|
||||
|
||||
override fun getRequestDefaultGroup(request: HttpServletRequest): GroupDto {
|
||||
val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName)
|
||||
?: throw NoDefaultGroupException()
|
||||
val defaultGroupUser = userLogic.getById(
|
||||
defaultGroupCookie.value.toLong(),
|
||||
isSystemUser = false,
|
||||
isDefaultGroupUser = true
|
||||
)
|
||||
return defaultGroupUser.group!!
|
||||
}
|
||||
|
||||
override fun setResponseDefaultGroup(id: Long, response: HttpServletResponse) {
|
||||
val defaultGroupUser = userLogic.getDefaultGroupUser(getById(id))
|
||||
response.addHeader(
|
||||
"Set-Cookie",
|
||||
"$defaultGroupCookieName=${defaultGroupUser.id}; Max-Age=$defaultGroupCookieMaxAge; Path=/api; HttpOnly; Secure; SameSite=strict"
|
||||
)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
override fun save(dto: GroupDto): GroupDto {
|
||||
throwIfNameAlreadyExists(dto.name)
|
||||
|
||||
return super.save(dto).also {
|
||||
userLogic.saveDefaultGroupUser(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun update(dto: GroupDto): GroupDto {
|
||||
throwIfNameAlreadyExists(dto.name, dto.id)
|
||||
|
||||
return super.update(dto)
|
||||
}
|
||||
|
||||
override fun deleteById(id: Long) {
|
||||
userLogic.deleteById(GroupDto.getDefaultGroupUserId(id))
|
||||
super.deleteById(id)
|
||||
}
|
||||
|
||||
private fun throwIfNameAlreadyExists(name: String, id: Long? = null) {
|
||||
if (service.existsByName(name, id)) {
|
||||
throw alreadyExistsException(value = name)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.logic.users
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.utils.base64encode
|
||||
import dev.fyloz.colorrecipesexplorer.utils.toDate
|
||||
import io.jsonwebtoken.Jwts
|
||||
import io.jsonwebtoken.jackson.io.JacksonDeserializer
|
||||
import io.jsonwebtoken.jackson.io.JacksonSerializer
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
const val jwtClaimUser = "user"
|
||||
|
||||
interface JwtLogic {
|
||||
/** Build a JWT token for the given [userDetails]. */
|
||||
fun buildJwt(userDetails: UserDetails): String
|
||||
|
||||
/** Build a JWT token for the given [user]. */
|
||||
fun buildJwt(user: UserDto): String
|
||||
|
||||
/** Parses a user from the given [jwt] token. */
|
||||
fun parseJwt(jwt: String): UserDto
|
||||
}
|
||||
|
||||
@Service
|
||||
class DefaultJwtLogic(
|
||||
val objectMapper: ObjectMapper,
|
||||
val securityProperties: CreSecurityProperties
|
||||
) : JwtLogic {
|
||||
private val secretKey by lazy {
|
||||
securityProperties.jwtSecret.base64encode()
|
||||
}
|
||||
|
||||
private val jwtBuilder by lazy {
|
||||
Jwts.builder()
|
||||
.serializeToJsonWith(JacksonSerializer<Map<String, *>>(objectMapper))
|
||||
.signWith(secretKey)
|
||||
}
|
||||
|
||||
private val jwtParser by lazy {
|
||||
Jwts.parserBuilder()
|
||||
.deserializeJsonWith(JacksonDeserializer<Map<String, *>>(objectMapper))
|
||||
.setSigningKey(secretKey)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun buildJwt(userDetails: UserDetails) =
|
||||
buildJwt(userDetails.user)
|
||||
|
||||
override fun buildJwt(user: UserDto): String =
|
||||
jwtBuilder
|
||||
.setSubject(user.id.toString())
|
||||
.setExpiration(getCurrentExpirationDate())
|
||||
.claim(jwtClaimUser, user.serialize())
|
||||
.compact()
|
||||
|
||||
override fun parseJwt(jwt: String): UserDto =
|
||||
with(
|
||||
jwtParser.parseClaimsJws(jwt)
|
||||
.body.get(jwtClaimUser, String::class.java)
|
||||
) {
|
||||
objectMapper.readValue(this)
|
||||
}
|
||||
|
||||
private fun getCurrentExpirationDate(): Date =
|
||||
Instant.now()
|
||||
.plusSeconds(securityProperties.jwtDuration)
|
||||
.toDate()
|
||||
|
||||
private fun UserDto.serialize(): String =
|
||||
objectMapper.writeValueAsString(this)
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.logic.users
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.SpringUserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.SpringUserDetailsService
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserDetails
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.User
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
interface UserDetailsLogic : SpringUserDetailsService {
|
||||
/** Loads an [User] for the given [id]. */
|
||||
fun loadUserById(id: Long, isDefaultGroupUser: Boolean = true): UserDetails
|
||||
}
|
||||
|
||||
@Service
|
||||
@RequireDatabase
|
||||
class DefaultUserDetailsLogic(
|
||||
private val userLogic: UserLogic
|
||||
) : UserDetailsLogic {
|
||||
override fun loadUserByUsername(username: String): UserDetails {
|
||||
try {
|
||||
return loadUserById(username.toLong(), false)
|
||||
} catch (ex: NotFoundException) {
|
||||
throw UsernameNotFoundException(username)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadUserById(id: Long, isDefaultGroupUser: Boolean): UserDetails {
|
||||
val user = userLogic.getById(
|
||||
id,
|
||||
isSystemUser = true,
|
||||
isDefaultGroupUser = isDefaultGroupUser
|
||||
)
|
||||
return UserDetails(user)
|
||||
}
|
||||
}
|
||||
|
||||
@Service
|
||||
@Profile("emergency")
|
||||
class EmergencyUserDetailsLogic(
|
||||
securityProperties: CreSecurityProperties
|
||||
) : UserDetailsLogic {
|
||||
private val users: Set<UserDto>
|
||||
|
||||
init {
|
||||
if (securityProperties.root == null) {
|
||||
throw NullPointerException("The root user has not been configured")
|
||||
}
|
||||
|
||||
users = setOf(
|
||||
// Add root user
|
||||
with(securityProperties.root!!) {
|
||||
UserDto(
|
||||
id = this.id,
|
||||
firstName = "Root",
|
||||
lastName = "User",
|
||||
group = null,
|
||||
password = this.password,
|
||||
permissions = listOf(Permission.ADMIN)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun loadUserByUsername(username: String): SpringUserDetails {
|
||||
return loadUserById(username.toLong(), false)
|
||||
}
|
||||
|
||||
override fun loadUserById(id: Long, isDefaultGroupUser: Boolean): UserDetails {
|
||||
val user = users.firstOrNull { it.id == id }
|
||||
?: throw UsernameNotFoundException(id.toString())
|
||||
|
||||
return UserDetails(user)
|
||||
}
|
||||
}
|
@ -1,169 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.logic.users
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
|
||||
import dev.fyloz.colorrecipesexplorer.config.security.authorizationCookieName
|
||||
import dev.fyloz.colorrecipesexplorer.config.security.blacklistedJwtTokens
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserSaveDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserUpdateDto
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.logic.BaseLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.Logic
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import dev.fyloz.colorrecipesexplorer.service.UserService
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.web.util.WebUtils
|
||||
import java.time.LocalDateTime
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
|
||||
interface UserLogic : Logic<UserDto, UserService> {
|
||||
/** Gets all users which have the given [group]. */
|
||||
fun getAllByGroup(group: GroupDto): Collection<UserDto>
|
||||
|
||||
/** Gets the user with the given [id]. */
|
||||
fun getById(id: Long, isSystemUser: Boolean, isDefaultGroupUser: Boolean): UserDto
|
||||
|
||||
/** Gets the default user of the given [group]. */
|
||||
fun getDefaultGroupUser(group: GroupDto): UserDto
|
||||
|
||||
/** Save a default group user for the given [group]. */
|
||||
fun saveDefaultGroupUser(group: GroupDto)
|
||||
|
||||
/** Saves the given [dto]. */
|
||||
fun save(dto: UserSaveDto): UserDto
|
||||
|
||||
/** Updates the given [dto]. */
|
||||
fun update(dto: UserUpdateDto): UserDto
|
||||
|
||||
/** Updates the last login time of the user with the given [id]. */
|
||||
fun updateLastLoginTime(id: Long, time: LocalDateTime = LocalDateTime.now()): UserDto
|
||||
|
||||
/** Updates the password of the user with the given [id]. */
|
||||
fun updatePassword(id: Long, password: String): UserDto
|
||||
|
||||
/** Adds the given [permission] to the user with the given [id]. */
|
||||
fun addPermission(id: Long, permission: Permission): UserDto
|
||||
|
||||
/** Removes the given [permission] from the user with the given [id]. */
|
||||
fun removePermission(id: Long, permission: Permission): UserDto
|
||||
|
||||
/** Logout a user. Add the authorization token of the given [request] to the blacklisted tokens. */
|
||||
fun logout(request: HttpServletRequest)
|
||||
}
|
||||
|
||||
@LogicComponent
|
||||
class DefaultUserLogic(
|
||||
service: UserService, @Lazy private val groupLogic: GroupLogic, @Lazy private val passwordEncoder: PasswordEncoder
|
||||
) : BaseLogic<UserDto, UserService>(service, Constants.ModelNames.USER), UserLogic {
|
||||
override fun getAll() = service.getAll(isSystemUser = false, isDefaultGroupUser = false)
|
||||
|
||||
override fun getAllByGroup(group: GroupDto) = service.getAllByGroup(group)
|
||||
|
||||
override fun getById(id: Long) = getById(id, isSystemUser = false, isDefaultGroupUser = false)
|
||||
override fun getById(id: Long, isSystemUser: Boolean, isDefaultGroupUser: Boolean) =
|
||||
service.getById(id, !isDefaultGroupUser, !isSystemUser) ?: throw notFoundException(value = id)
|
||||
|
||||
override fun getDefaultGroupUser(group: GroupDto) =
|
||||
service.getDefaultGroupUser(group) ?: throw notFoundException(identifierName = "groupId", value = group.id)
|
||||
|
||||
override fun saveDefaultGroupUser(group: GroupDto) {
|
||||
save(
|
||||
UserSaveDto(
|
||||
id = group.defaultGroupUserId,
|
||||
firstName = group.name,
|
||||
lastName = "User",
|
||||
password = group.name,
|
||||
groupId = group.id,
|
||||
permissions = listOf(),
|
||||
isDefaultGroupUser = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun save(dto: UserSaveDto) = save(
|
||||
UserDto(
|
||||
id = dto.id,
|
||||
firstName = dto.firstName,
|
||||
lastName = dto.lastName,
|
||||
password = passwordEncoder.encode(dto.password),
|
||||
group = if (dto.groupId != null) groupLogic.getById(dto.groupId) else null,
|
||||
permissions = dto.permissions,
|
||||
isSystemUser = dto.isSystemUser,
|
||||
isDefaultGroupUser = dto.isDefaultGroupUser
|
||||
)
|
||||
)
|
||||
|
||||
override fun save(dto: UserDto): UserDto {
|
||||
throwIfIdAlreadyExists(dto.id)
|
||||
throwIfFirstNameAndLastNameAlreadyExists(dto.firstName, dto.lastName)
|
||||
|
||||
return super.save(dto)
|
||||
}
|
||||
|
||||
override fun update(dto: UserUpdateDto): UserDto {
|
||||
val user = getById(dto.id, isSystemUser = false, isDefaultGroupUser = false)
|
||||
|
||||
return update(
|
||||
user.copy(
|
||||
firstName = dto.firstName,
|
||||
lastName = dto.lastName,
|
||||
group = if (dto.groupId != null) groupLogic.getById(dto.groupId) else null,
|
||||
permissions = dto.permissions
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun update(dto: UserDto): UserDto {
|
||||
throwIfFirstNameAndLastNameAlreadyExists(dto.firstName, dto.lastName, dto.id)
|
||||
|
||||
return super.update(dto)
|
||||
}
|
||||
|
||||
override fun updateLastLoginTime(id: Long, time: LocalDateTime) = with(getById(id)) {
|
||||
update(this.copy(lastLoginTime = time))
|
||||
}
|
||||
|
||||
override fun updatePassword(id: Long, password: String) = with(getById(id)) {
|
||||
update(this.copy(password = passwordEncoder.encode(password)))
|
||||
}
|
||||
|
||||
override fun addPermission(id: Long, permission: Permission) = with(getById(id)) {
|
||||
update(this.copy(permissions = this.permissions + permission))
|
||||
}
|
||||
|
||||
override fun removePermission(id: Long, permission: Permission) = with(getById(id)) {
|
||||
update(this.copy(permissions = this.permissions - permission))
|
||||
}
|
||||
|
||||
override fun logout(request: HttpServletRequest) {
|
||||
val authorizationCookie = WebUtils.getCookie(request, authorizationCookieName)
|
||||
if (authorizationCookie != null) {
|
||||
val authorizationToken = authorizationCookie.value
|
||||
if (authorizationToken != null && authorizationToken.startsWith("Bearer")) {
|
||||
blacklistedJwtTokens.add(authorizationToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun throwIfIdAlreadyExists(id: Long) {
|
||||
if (service.existsById(id)) {
|
||||
throw alreadyExistsException(identifierName = ID_IDENTIFIER_NAME, value = id)
|
||||
}
|
||||
}
|
||||
|
||||
private fun throwIfFirstNameAndLastNameAlreadyExists(firstName: String, lastName: String, id: Long? = null) {
|
||||
if (service.existsByFirstNameAndLastName(firstName, lastName, id)) {
|
||||
throw AlreadyExistsException(
|
||||
typeNameLowerCase,
|
||||
"$typeName already exists",
|
||||
"A $typeNameLowerCase with the name '$firstName $lastName' already exists",
|
||||
"$firstName $lastName",
|
||||
"fullName"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +1,107 @@
|
||||
package dev.fyloz.colorrecipesexplorer.model
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
|
||||
import javax.persistence.*
|
||||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.NotNull
|
||||
|
||||
@Entity
|
||||
@Table(name = "company")
|
||||
data class Company(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
override val id: Long,
|
||||
override val id: Long?,
|
||||
|
||||
@Column(unique = true)
|
||||
override val name: String
|
||||
) : NamedModel {
|
||||
override fun toString(): String {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
open class CompanySaveDto(
|
||||
@field:NotBlank
|
||||
val name: String
|
||||
) : ModelEntity
|
||||
) : EntityDto<Company> {
|
||||
override fun toEntity(): Company = Company(null, name)
|
||||
}
|
||||
|
||||
|
||||
open class CompanyUpdateDto(
|
||||
val id: Long,
|
||||
|
||||
@field:NotBlank
|
||||
val name: String?
|
||||
) : EntityDto<Company> {
|
||||
override fun toEntity(): Company = Company(id, name ?: "")
|
||||
}
|
||||
|
||||
// ==== DSL ====
|
||||
fun company(
|
||||
id: Long? = null,
|
||||
name: String = "name",
|
||||
op: Company.() -> Unit = {}
|
||||
) = Company(id, name).apply(op)
|
||||
|
||||
fun companySaveDto(
|
||||
name: String = "name",
|
||||
op: CompanySaveDto.() -> Unit = {}
|
||||
) = CompanySaveDto(name).apply(op)
|
||||
|
||||
fun companyUpdateDto(
|
||||
id: Long = 0L,
|
||||
name: String? = "name",
|
||||
op: CompanyUpdateDto.() -> Unit = {}
|
||||
) = CompanyUpdateDto(id, name).apply(op)
|
||||
|
||||
// ==== Exceptions ====
|
||||
private const val COMPANY_NOT_FOUND_EXCEPTION_TITLE = "Company not found"
|
||||
private const val COMPANY_ALREADY_EXISTS_EXCEPTION_TITLE = "Company already exists"
|
||||
private const val COMPANY_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete company"
|
||||
private const val COMPANY_EXCEPTION_ERROR_CODE = "company"
|
||||
|
||||
fun companyIdNotFoundException(id: Long) =
|
||||
NotFoundException(
|
||||
COMPANY_EXCEPTION_ERROR_CODE,
|
||||
COMPANY_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"A company with the id $id could not be found",
|
||||
id
|
||||
)
|
||||
|
||||
fun companyNameNotFoundException(name: String) =
|
||||
NotFoundException(
|
||||
COMPANY_EXCEPTION_ERROR_CODE,
|
||||
COMPANY_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"A company with the name $name could not be found",
|
||||
name,
|
||||
"name"
|
||||
)
|
||||
|
||||
fun companyIdAlreadyExistsException(id: Long) =
|
||||
AlreadyExistsException(
|
||||
COMPANY_EXCEPTION_ERROR_CODE,
|
||||
COMPANY_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A company with the id $id already exists",
|
||||
id
|
||||
)
|
||||
|
||||
fun companyNameAlreadyExistsException(name: String) =
|
||||
AlreadyExistsException(
|
||||
COMPANY_EXCEPTION_ERROR_CODE,
|
||||
COMPANY_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A company with the name $name already exists",
|
||||
name,
|
||||
"name"
|
||||
)
|
||||
|
||||
fun cannotDeleteCompany(company: Company) =
|
||||
CannotDeleteException(
|
||||
COMPANY_EXCEPTION_ERROR_CODE,
|
||||
COMPANY_CANNOT_DELETE_EXCEPTION_TITLE,
|
||||
"Cannot delete the company ${company.name} because one or more recipes depends on it"
|
||||
)
|
||||
|
@ -1,39 +1,14 @@
|
||||
package dev.fyloz.colorrecipesexplorer.model
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import dev.fyloz.colorrecipesexplorer.exception.RestException
|
||||
import dev.fyloz.colorrecipesexplorer.utils.months
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.time.LocalDateTime
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.Id
|
||||
import javax.persistence.Table
|
||||
import javax.validation.constraints.NotBlank
|
||||
|
||||
sealed class ConfigurationBase(
|
||||
@JsonIgnore
|
||||
val type: ConfigurationType,
|
||||
val lastUpdated: LocalDateTime
|
||||
) {
|
||||
val key = type.key
|
||||
val requireRestart = type.requireRestart
|
||||
val editable = !type.computed
|
||||
}
|
||||
|
||||
class Configuration(type: ConfigurationType, val content: String, lastUpdated: LocalDateTime) :
|
||||
ConfigurationBase(type, lastUpdated) {
|
||||
fun toEntity() =
|
||||
ConfigurationEntity(key, content, lastUpdated)
|
||||
}
|
||||
|
||||
class SecureConfiguration(type: ConfigurationType, lastUpdated: LocalDateTime) :
|
||||
ConfigurationBase(type, lastUpdated)
|
||||
|
||||
@Entity
|
||||
@Table(name = "configuration")
|
||||
data class ConfigurationEntity(
|
||||
data class Configuration(
|
||||
@Id
|
||||
@Column(name = "config_key")
|
||||
val key: String,
|
||||
@ -42,134 +17,4 @@ data class ConfigurationEntity(
|
||||
|
||||
@Column(name = "last_updated")
|
||||
val lastUpdated: LocalDateTime
|
||||
) {
|
||||
fun toConfiguration() =
|
||||
configuration(key.toConfigurationType(), content, lastUpdated)
|
||||
|
||||
override fun equals(other: Any?) =
|
||||
other is ConfigurationEntity && key == other.key && content == other.content
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = key.hashCode()
|
||||
result = 31 * result + content.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
data class ConfigurationDto(
|
||||
val key: String,
|
||||
|
||||
@NotBlank
|
||||
val content: String
|
||||
)
|
||||
|
||||
data class ConfigurationImageDto(
|
||||
val key: String,
|
||||
|
||||
val image: MultipartFile
|
||||
)
|
||||
|
||||
fun configuration(
|
||||
type: ConfigurationType,
|
||||
content: String = type.defaultContent.toString(),
|
||||
lastUpdated: LocalDateTime? = null
|
||||
) = Configuration(type, content, lastUpdated ?: LocalDateTime.now())
|
||||
|
||||
fun configuration(
|
||||
dto: ConfigurationDto
|
||||
) = with(dto) {
|
||||
configuration(type = key.toConfigurationType(), content = content)
|
||||
}
|
||||
|
||||
fun secureConfiguration(
|
||||
type: ConfigurationType,
|
||||
lastUpdated: LocalDateTime? = null
|
||||
) = SecureConfiguration(type, lastUpdated ?: LocalDateTime.now())
|
||||
|
||||
fun secureConfiguration(
|
||||
configuration: Configuration
|
||||
) = secureConfiguration(configuration.type, configuration.lastUpdated)
|
||||
|
||||
enum class ConfigurationType(
|
||||
val key: String,
|
||||
val defaultContent: Any? = null,
|
||||
val computed: Boolean = false,
|
||||
val file: Boolean = false,
|
||||
val requireRestart: Boolean = false,
|
||||
val public: Boolean = false,
|
||||
val secure: Boolean = false
|
||||
) {
|
||||
INSTANCE_NAME("instance.name", defaultContent = "Color Recipes Explorer", public = true),
|
||||
INSTANCE_LOGO_SET("instance.logo.set", defaultContent = false, public = true),
|
||||
INSTANCE_ICON_SET("instance.icon.set", defaultContent = false, public = true),
|
||||
INSTANCE_URL("instance.url", "http://localhost:9090", public = true),
|
||||
|
||||
DATABASE_URL("database.url", defaultContent = "mysql://localhost/cre", file = true, requireRestart = true),
|
||||
DATABASE_USER("database.user", defaultContent = "cre", file = true, requireRestart = true),
|
||||
DATABASE_PASSWORD("database.password", defaultContent = "asecurepassword", file = true, requireRestart = true, secure = true),
|
||||
DATABASE_SUPPORTED_VERSION("database.version.supported", computed = true),
|
||||
|
||||
RECIPE_APPROBATION_EXPIRATION("recipe.approbation.expiration", defaultContent = 4.months),
|
||||
|
||||
TOUCH_UP_KIT_CACHE_PDF("touchupkit.pdf.cache", defaultContent = true),
|
||||
TOUCH_UP_KIT_EXPIRATION("touchupkit.expiration", defaultContent = 1.months),
|
||||
|
||||
EMERGENCY_MODE_ENABLED("env.emergency", computed = true, public = true),
|
||||
BUILD_VERSION("env.build.version", computed = true),
|
||||
BUILD_TIME("env.build.time", computed = true),
|
||||
JAVA_VERSION("env.java.version", computed = true),
|
||||
OPERATING_SYSTEM("env.os", computed = true),
|
||||
|
||||
GENERATED_ENCRYPTION_SALT("security.salt", file = true, requireRestart = true)
|
||||
;
|
||||
|
||||
override fun toString() = key
|
||||
}
|
||||
|
||||
fun String.toConfigurationType() =
|
||||
ConfigurationType.values().firstOrNull { it.key == this }
|
||||
?: throw InvalidConfigurationKeyException(this)
|
||||
|
||||
class InvalidConfigurationKeyException(val key: String) :
|
||||
RestException(
|
||||
"invalid-configuration-key",
|
||||
"Invalid configuration key",
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"The configuration key '$key' does not exists",
|
||||
mapOf(
|
||||
"key" to key
|
||||
)
|
||||
)
|
||||
|
||||
class InvalidImageConfigurationException(val type: ConfigurationType) :
|
||||
RestException(
|
||||
"invalid-configuration-image",
|
||||
"Invalid image configuration",
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"The configuration with the key '${type.key}' does not accept images as content",
|
||||
mapOf(
|
||||
"key" to type.key
|
||||
)
|
||||
)
|
||||
|
||||
class ConfigurationNotSetException(val type: ConfigurationType) :
|
||||
RestException(
|
||||
"unset-configuration",
|
||||
"Unset configuration",
|
||||
HttpStatus.NOT_FOUND,
|
||||
"The configuration with the key '${type.key}' is not set",
|
||||
mapOf(
|
||||
"key" to type.key
|
||||
)
|
||||
)
|
||||
|
||||
class CannotSetComputedConfigurationException(val type: ConfigurationType) :
|
||||
RestException(
|
||||
"cannot-set-computed-configuration",
|
||||
"Cannot set computed configuration",
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"The configuration with the key '${type.key}' is a computed configuration and cannot be modified",
|
||||
mapOf(
|
||||
"key" to type.key
|
||||
)
|
||||
)
|
||||
|
@ -1,30 +1,172 @@
|
||||
package dev.fyloz.colorrecipesexplorer.model
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import javax.persistence.*
|
||||
import javax.validation.constraints.Min
|
||||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.Size
|
||||
|
||||
const val SIMDUT_FILES_PATH = "pdf/simdut"
|
||||
|
||||
@Entity
|
||||
@Table(name = "material")
|
||||
data class Material(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
override val id: Long,
|
||||
override val id: Long?,
|
||||
|
||||
@Column(unique = true)
|
||||
val name: String,
|
||||
override var name: String,
|
||||
|
||||
@Column(name = "inventory_quantity")
|
||||
val inventoryQuantity: Float,
|
||||
var inventoryQuantity: Float,
|
||||
|
||||
@Column(name = "mix_type")
|
||||
val isMixType: Boolean,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "material_type_id")
|
||||
val materialType: MaterialType?
|
||||
) : ModelEntity {
|
||||
companion object {
|
||||
fun getSimdutFilePath(name: String) =
|
||||
"${Constants.FilePaths.SIMDUT}/$name.pdf"
|
||||
}
|
||||
}
|
||||
var materialType: MaterialType?
|
||||
) : NamedModel {
|
||||
val simdutFilePath
|
||||
@JsonIgnore
|
||||
@Transient
|
||||
get() = "$SIMDUT_FILES_PATH/$name.pdf"
|
||||
}
|
||||
|
||||
open class MaterialSaveDto(
|
||||
@field:NotBlank
|
||||
val name: String,
|
||||
|
||||
@field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
|
||||
val inventoryQuantity: Float,
|
||||
|
||||
val materialTypeId: Long,
|
||||
|
||||
val simdutFile: MultipartFile? = null
|
||||
) : EntityDto<Material>
|
||||
|
||||
open class MaterialUpdateDto(
|
||||
val id: Long,
|
||||
|
||||
@field:NotBlank
|
||||
val name: String?,
|
||||
|
||||
@field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
|
||||
val inventoryQuantity: Float?,
|
||||
|
||||
val materialTypeId: Long?,
|
||||
|
||||
val simdutFile: MultipartFile? = null
|
||||
) : EntityDto<Material>
|
||||
|
||||
data class MaterialOutputDto(
|
||||
override val id: Long,
|
||||
val name: String,
|
||||
val inventoryQuantity: Float,
|
||||
val isMixType: Boolean,
|
||||
val materialType: MaterialType,
|
||||
val simdutUrl: String?
|
||||
) : Model
|
||||
|
||||
data class MaterialQuantityDto(
|
||||
val material: Long,
|
||||
|
||||
@field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
|
||||
val quantity: Float
|
||||
)
|
||||
|
||||
// === DSL ===
|
||||
|
||||
fun material(
|
||||
id: Long? = null,
|
||||
name: String = "name",
|
||||
inventoryQuantity: Float = 0f,
|
||||
isMixType: Boolean = false,
|
||||
materialType: MaterialType? = materialType(),
|
||||
op: Material.() -> Unit = {}
|
||||
) = Material(id, name, inventoryQuantity, isMixType, materialType).apply(op)
|
||||
|
||||
fun material(
|
||||
material: Material,
|
||||
id: Long? = null,
|
||||
name: String? = null,
|
||||
) = Material(
|
||||
id ?: material.id, name
|
||||
?: material.name, material.inventoryQuantity, material.isMixType, material.materialType
|
||||
)
|
||||
|
||||
fun materialSaveDto(
|
||||
name: String = "name",
|
||||
inventoryQuantity: Float = 0f,
|
||||
materialTypeId: Long = 0L,
|
||||
simdutFile: MultipartFile? = null,
|
||||
op: MaterialSaveDto.() -> Unit = {}
|
||||
) = MaterialSaveDto(name, inventoryQuantity, materialTypeId, simdutFile).apply(op)
|
||||
|
||||
fun materialUpdateDto(
|
||||
id: Long = 0L,
|
||||
name: String? = "name",
|
||||
inventoryQuantity: Float? = 0f,
|
||||
materialTypeId: Long? = 0L,
|
||||
simdutFile: MultipartFile? = null,
|
||||
op: MaterialUpdateDto.() -> Unit = {}
|
||||
) = MaterialUpdateDto(id, name, inventoryQuantity, materialTypeId, simdutFile).apply(op)
|
||||
|
||||
fun materialQuantityDto(
|
||||
materialId: Long,
|
||||
quantity: Float,
|
||||
op: MaterialQuantityDto.() -> Unit = {}
|
||||
) = MaterialQuantityDto(materialId, quantity).apply(op)
|
||||
|
||||
// ==== Exceptions ====
|
||||
private const
|
||||
val MATERIAL_NOT_FOUND_EXCEPTION_TITLE = "Material not found"
|
||||
private const val MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE = "Material already exists"
|
||||
private const val MATERIAL_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete material"
|
||||
private const val MATERIAL_EXCEPTION_ERROR_CODE = "material"
|
||||
|
||||
fun materialIdNotFoundException(id: Long) =
|
||||
NotFoundException(
|
||||
MATERIAL_EXCEPTION_ERROR_CODE,
|
||||
MATERIAL_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"A material with the id $id could not be found",
|
||||
id
|
||||
)
|
||||
|
||||
fun materialNameNotFoundException(name: String) =
|
||||
NotFoundException(
|
||||
MATERIAL_EXCEPTION_ERROR_CODE,
|
||||
MATERIAL_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"A material with the name $name could not be found",
|
||||
name,
|
||||
"name"
|
||||
)
|
||||
|
||||
fun materialIdAlreadyExistsException(id: Long) =
|
||||
AlreadyExistsException(
|
||||
MATERIAL_EXCEPTION_ERROR_CODE,
|
||||
MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A material with the id $id already exists",
|
||||
id
|
||||
)
|
||||
|
||||
fun materialNameAlreadyExistsException(name: String) =
|
||||
AlreadyExistsException(
|
||||
MATERIAL_EXCEPTION_ERROR_CODE,
|
||||
MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A material with the name $name already exists",
|
||||
name,
|
||||
"name"
|
||||
)
|
||||
|
||||
fun cannotDeleteMaterialException(material: Material) =
|
||||
CannotDeleteException(
|
||||
MATERIAL_EXCEPTION_ERROR_CODE,
|
||||
MATERIAL_CANNOT_DELETE_EXCEPTION_TITLE,
|
||||
"Cannot delete the material ${material.name} because one or more recipes depends on it"
|
||||
)
|
||||
|
@ -1,26 +1,158 @@
|
||||
package dev.fyloz.colorrecipesexplorer.model
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
|
||||
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrSize
|
||||
import org.hibernate.annotations.ColumnDefault
|
||||
import javax.persistence.*
|
||||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.NotNull
|
||||
import javax.validation.constraints.Size
|
||||
|
||||
private const val VALIDATION_PREFIX_SIZE = "Must contains exactly 3 characters"
|
||||
|
||||
@Entity
|
||||
@Table(name = "material_type")
|
||||
data class MaterialType(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
override val id: Long,
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
override val id: Long? = null,
|
||||
|
||||
@Column(unique = true)
|
||||
val name: String = "",
|
||||
@Column(unique = true)
|
||||
override val name: String = "",
|
||||
|
||||
@Column(unique = true)
|
||||
val prefix: String = "",
|
||||
@Column(unique = true)
|
||||
val prefix: String = "",
|
||||
|
||||
@Column(name = "use_percentages")
|
||||
@ColumnDefault("false")
|
||||
val usePercentages: Boolean = false,
|
||||
@Column(name = "use_percentages")
|
||||
@ColumnDefault("false")
|
||||
val usePercentages: Boolean = false,
|
||||
|
||||
@Column(name = "system_type")
|
||||
@ColumnDefault("false")
|
||||
val systemType: Boolean = false
|
||||
) : ModelEntity
|
||||
@Column(name = "system_type")
|
||||
@ColumnDefault("false")
|
||||
val systemType: Boolean = false
|
||||
) : NamedModel
|
||||
|
||||
open class MaterialTypeSaveDto(
|
||||
@field:NotBlank
|
||||
val name: String,
|
||||
|
||||
@field:NotBlank
|
||||
@field:Size(min = 3, max = 3, message = VALIDATION_PREFIX_SIZE)
|
||||
val prefix: String,
|
||||
|
||||
val usePercentages: Boolean = false
|
||||
) : EntityDto<MaterialType> {
|
||||
override fun toEntity(): MaterialType =
|
||||
MaterialType(null, name, prefix, usePercentages)
|
||||
}
|
||||
|
||||
open class MaterialTypeUpdateDto(
|
||||
val id: Long,
|
||||
|
||||
@field:NotBlank
|
||||
val name: String?,
|
||||
|
||||
@field:Size(min = 3, max = 3, message = VALIDATION_PREFIX_SIZE)
|
||||
val prefix: String?
|
||||
) : EntityDto<MaterialType> {
|
||||
override fun toEntity(): MaterialType =
|
||||
MaterialType(id, name ?: "", prefix ?: "")
|
||||
}
|
||||
|
||||
// ==== DSL ====
|
||||
fun materialType(
|
||||
id: Long? = null,
|
||||
name: String = "name",
|
||||
prefix: String = "PRE",
|
||||
usePercentages: Boolean = false,
|
||||
systemType: Boolean = false,
|
||||
op: MaterialType.() -> Unit = {}
|
||||
) = MaterialType(id, name, prefix, usePercentages, systemType).apply(op)
|
||||
|
||||
fun materialType(
|
||||
materialType: MaterialType,
|
||||
newId: Long? = null,
|
||||
newName: String? = null,
|
||||
newSystemType: Boolean? = null
|
||||
) = with(materialType) {
|
||||
MaterialType(
|
||||
newId ?: id,
|
||||
newName ?: name,
|
||||
prefix,
|
||||
usePercentages,
|
||||
newSystemType ?: systemType
|
||||
)
|
||||
}
|
||||
|
||||
fun materialTypeSaveDto(
|
||||
name: String = "name",
|
||||
prefix: String = "PRE",
|
||||
usePercentages: Boolean = false,
|
||||
op: MaterialTypeSaveDto.() -> Unit = {}
|
||||
) = MaterialTypeSaveDto(name, prefix, usePercentages).apply(op)
|
||||
|
||||
fun materialTypeUpdateDto(
|
||||
id: Long = 0L,
|
||||
name: String? = null,
|
||||
prefix: String? = null,
|
||||
op: MaterialTypeUpdateDto.() -> Unit = {}
|
||||
) = MaterialTypeUpdateDto(id, name, prefix).apply(op)
|
||||
|
||||
// ==== Exceptions ====
|
||||
private const val MATERIAL_TYPE_NOT_FOUND_EXCEPTION_TITLE = "Material type not found"
|
||||
private const val MATERIAL_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE = "Material type already exists"
|
||||
private const val MATERIAL_TYPE_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete material type"
|
||||
private const val MATERIAL_TYPE_EXCEPTION_ERROR_CODE = "materialtype"
|
||||
|
||||
fun materialTypeIdNotFoundException(id: Long) =
|
||||
NotFoundException(
|
||||
MATERIAL_TYPE_EXCEPTION_ERROR_CODE,
|
||||
MATERIAL_TYPE_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"A material type with the id $id could not be found",
|
||||
id
|
||||
)
|
||||
|
||||
fun materialTypeNameNotFoundException(name: String) =
|
||||
NotFoundException(
|
||||
MATERIAL_TYPE_EXCEPTION_ERROR_CODE,
|
||||
MATERIAL_TYPE_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"A material type with the name $name could not be found",
|
||||
name,
|
||||
"name"
|
||||
)
|
||||
|
||||
fun materialTypeIdAlreadyExistsException(id: Long) =
|
||||
AlreadyExistsException(
|
||||
MATERIAL_TYPE_EXCEPTION_ERROR_CODE,
|
||||
MATERIAL_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A material type with the id $id already exists",
|
||||
id
|
||||
)
|
||||
|
||||
fun materialTypeNameAlreadyExistsException(name: String) =
|
||||
AlreadyExistsException(
|
||||
MATERIAL_TYPE_EXCEPTION_ERROR_CODE,
|
||||
MATERIAL_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A material type with the name $name already exists",
|
||||
name,
|
||||
"name"
|
||||
)
|
||||
|
||||
fun materialTypePrefixAlreadyExistsException(prefix: String) =
|
||||
AlreadyExistsException(
|
||||
MATERIAL_TYPE_EXCEPTION_ERROR_CODE,
|
||||
MATERIAL_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A material type with the prefix $prefix already exists",
|
||||
prefix,
|
||||
"prefix"
|
||||
)
|
||||
|
||||
fun cannotDeleteMaterialTypeException(materialType: MaterialType) =
|
||||
CannotDeleteException(
|
||||
MATERIAL_TYPE_EXCEPTION_ERROR_CODE,
|
||||
MATERIAL_TYPE_CANNOT_DELETE_EXCEPTION_TITLE,
|
||||
"Cannot delete material type ${materialType.name} because one or more materials depends on it"
|
||||
)
|
||||
|
@ -1,24 +1,144 @@
|
||||
package dev.fyloz.colorrecipesexplorer.model
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import javax.persistence.*
|
||||
import javax.validation.constraints.Min
|
||||
import javax.validation.constraints.NotBlank
|
||||
|
||||
|
||||
@Entity
|
||||
@Table(name = "mix")
|
||||
data class Mix(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
override val id: Long,
|
||||
override val id: Long?,
|
||||
|
||||
val location: String?,
|
||||
var location: String?,
|
||||
|
||||
@Column(name = "recipe_id")
|
||||
val recipeId: Long,
|
||||
@JsonIgnore
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "recipe_id")
|
||||
val recipe: Recipe,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "mix_type_id")
|
||||
val mixType: MixType,
|
||||
var mixType: MixType,
|
||||
|
||||
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
|
||||
@JoinColumn(name = "mix_id")
|
||||
val mixMaterials: List<MixMaterial>
|
||||
) : ModelEntity
|
||||
var mixMaterials: MutableSet<MixMaterial>,
|
||||
) : Model
|
||||
|
||||
open class MixSaveDto(
|
||||
@field:NotBlank
|
||||
val name: String,
|
||||
|
||||
val recipeId: Long,
|
||||
|
||||
val materialTypeId: Long,
|
||||
|
||||
val mixMaterials: Set<MixMaterialDto>?
|
||||
) : EntityDto<Mix>
|
||||
|
||||
open class MixUpdateDto(
|
||||
val id: Long,
|
||||
|
||||
@field:NotBlank
|
||||
val name: String?,
|
||||
|
||||
val materialTypeId: Long?,
|
||||
|
||||
var mixMaterials: Set<MixMaterialDto>?
|
||||
) : EntityDto<Mix>
|
||||
|
||||
data class MixOutputDto(
|
||||
val id: Long,
|
||||
val location: String?,
|
||||
val mixType: MixType,
|
||||
val mixMaterials: Set<MixMaterialOutputDto>
|
||||
)
|
||||
|
||||
data class MixDeductDto(
|
||||
val id: Long,
|
||||
|
||||
@field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
|
||||
val ratio: Float
|
||||
)
|
||||
|
||||
data class MixLocationDto(
|
||||
val mixId: Long,
|
||||
|
||||
val location: String?
|
||||
)
|
||||
|
||||
//fun Mix.toOutput() =
|
||||
|
||||
// ==== DSL ====
|
||||
fun mix(
|
||||
id: Long? = null,
|
||||
location: String? = "location",
|
||||
recipe: Recipe = recipe(),
|
||||
mixType: MixType = mixType(),
|
||||
mixMaterials: MutableSet<MixMaterial> = mutableSetOf(),
|
||||
op: Mix.() -> Unit = {}
|
||||
) = Mix(id, location, recipe, mixType, mixMaterials).apply(op)
|
||||
|
||||
fun mixSaveDto(
|
||||
name: String = "name",
|
||||
recipeId: Long = 0L,
|
||||
materialTypeId: Long = 0L,
|
||||
mixMaterials: Set<MixMaterialDto>? = setOf(),
|
||||
op: MixSaveDto.() -> Unit = {}
|
||||
) = MixSaveDto(name, recipeId, materialTypeId, mixMaterials).apply(op)
|
||||
|
||||
fun mixUpdateDto(
|
||||
id: Long = 0L,
|
||||
name: String? = "name",
|
||||
materialTypeId: Long? = 0L,
|
||||
mixMaterials: Set<MixMaterialDto>? = setOf(),
|
||||
op: MixUpdateDto.() -> Unit = {}
|
||||
) = MixUpdateDto(id, name, materialTypeId, mixMaterials).apply(op)
|
||||
|
||||
fun mixRatio(
|
||||
id: Long = 0L,
|
||||
ratio: Float = 1f,
|
||||
op: MixDeductDto.() -> Unit = {}
|
||||
) = MixDeductDto(id, ratio).apply(op)
|
||||
|
||||
fun mixLocationDto(
|
||||
mixId: Long = 0L,
|
||||
location: String? = "location",
|
||||
op: MixLocationDto.() -> Unit = {}
|
||||
) = MixLocationDto(mixId, location).apply(op)
|
||||
|
||||
// ==== Exceptions ====
|
||||
private const val MIX_NOT_FOUND_EXCEPTION_TITLE = "Mix not found"
|
||||
private const val MIX_ALREADY_EXISTS_EXCEPTION_TITLE = "Mix already exists"
|
||||
private const val MIX_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete mix"
|
||||
private const val MIX_EXCEPTION_ERROR_CODE = "mix"
|
||||
|
||||
fun mixIdNotFoundException(id: Long) =
|
||||
NotFoundException(
|
||||
MIX_EXCEPTION_ERROR_CODE,
|
||||
MIX_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"A mix with the id $id could not be found",
|
||||
id
|
||||
)
|
||||
|
||||
fun mixIdAlreadyExistsException(id: Long) =
|
||||
AlreadyExistsException(
|
||||
MIX_EXCEPTION_ERROR_CODE,
|
||||
MIX_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A mix with the id $id already exists",
|
||||
id
|
||||
)
|
||||
|
||||
fun cannotDeleteMixException(mix: Mix) =
|
||||
CannotDeleteException(
|
||||
MIX_EXCEPTION_ERROR_CODE,
|
||||
MIX_CANNOT_DELETE_EXCEPTION_TITLE,
|
||||
"Cannot delete the mix ${mix.mixType.name} because one or more mixes depends on it"
|
||||
)
|
||||
|
@ -1,19 +1,76 @@
|
||||
package dev.fyloz.colorrecipesexplorer.model
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import javax.persistence.*
|
||||
import javax.validation.constraints.Min
|
||||
import javax.validation.constraints.NotNull
|
||||
|
||||
@Entity
|
||||
@Table(name = "mix_material")
|
||||
data class MixMaterial(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
override val id: Long,
|
||||
override val id: Long?,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "material_id")
|
||||
val material: Material,
|
||||
|
||||
var quantity: Float,
|
||||
|
||||
var position: Int
|
||||
) : Model
|
||||
|
||||
data class MixMaterialDto(
|
||||
val materialId: Long,
|
||||
|
||||
@field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
|
||||
val quantity: Float,
|
||||
|
||||
val position: Int
|
||||
) : ModelEntity
|
||||
)
|
||||
|
||||
data class MixMaterialOutputDto(
|
||||
val id: Long,
|
||||
val material: MaterialOutputDto,
|
||||
val quantity: Float,
|
||||
val position: Int
|
||||
)
|
||||
|
||||
// ==== DSL ====
|
||||
fun mixMaterial(
|
||||
id: Long? = null,
|
||||
material: Material = material(),
|
||||
quantity: Float = 0f,
|
||||
position: Int = 0,
|
||||
op: MixMaterial.() -> Unit = {}
|
||||
) = MixMaterial(id, material, quantity, position).apply(op)
|
||||
|
||||
fun mixMaterialDto(
|
||||
materialId: Long = 0L,
|
||||
quantity: Float = 0f,
|
||||
position: Int = 0,
|
||||
op: MixMaterialDto.() -> Unit = {}
|
||||
) = MixMaterialDto(materialId, quantity, position).apply(op)
|
||||
|
||||
// ==== Exceptions ====
|
||||
private const val MIX_MATERIAL_NOT_FOUND_EXCEPTION_TITLE = "Mix material not found"
|
||||
private const val MIX_MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE = "Mix material already exists"
|
||||
private const val MIX_MATERIAL_EXCEPTION_ERROR_CODE = "mixmaterial"
|
||||
|
||||
fun mixMaterialIdNotFoundException(id: Long) =
|
||||
NotFoundException(
|
||||
MIX_MATERIAL_EXCEPTION_ERROR_CODE,
|
||||
MIX_MATERIAL_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"A mix material with the id $id could not be found",
|
||||
id
|
||||
)
|
||||
|
||||
fun mixMaterialIdAlreadyExistsException(id: Long) =
|
||||
AlreadyExistsException(
|
||||
MIX_MATERIAL_EXCEPTION_ERROR_CODE,
|
||||
MIX_MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A mix material with the id $id already exists",
|
||||
id
|
||||
)
|
||||
|
@ -1,19 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.model
|
||||
|
||||
import javax.persistence.*
|
||||
|
||||
@Entity
|
||||
@Table(name = "mix_mix_type")
|
||||
data class MixMixType(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
override val id: Long,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "mix_type_id")
|
||||
val mixType: MixType,
|
||||
|
||||
val quantity: Float,
|
||||
|
||||
val position: Int
|
||||
) : ModelEntity
|
@ -1,21 +1,100 @@
|
||||
package dev.fyloz.colorrecipesexplorer.model
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.RestException
|
||||
import org.springframework.http.HttpStatus
|
||||
import javax.persistence.*
|
||||
|
||||
@Entity
|
||||
@Table(name = "mix_type")
|
||||
data class MixType(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
override val id: Long,
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
override val id: Long?,
|
||||
|
||||
val name: String,
|
||||
@Column(unique = true)
|
||||
override var name: String,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "material_type_id")
|
||||
val materialType: MaterialType,
|
||||
@OneToOne(cascade = [CascadeType.ALL])
|
||||
@JoinColumn(name = "material_id")
|
||||
var material: Material
|
||||
) : NamedModel
|
||||
|
||||
@OneToOne(cascade = [CascadeType.ALL])
|
||||
@JoinColumn(name = "material_id")
|
||||
val material: Material?
|
||||
) : ModelEntity
|
||||
// ==== DSL ====
|
||||
fun mixType(
|
||||
id: Long? = null,
|
||||
name: String = "name",
|
||||
material: Material = material(),
|
||||
op: MixType.() -> Unit = {}
|
||||
) = MixType(id, name, material).apply(op)
|
||||
|
||||
fun mixType(
|
||||
name: String = "name",
|
||||
materialType: MaterialType = materialType(),
|
||||
op: MixType.() -> Unit = {}
|
||||
) = mixType(
|
||||
id = null,
|
||||
name,
|
||||
material = material(name = name, inventoryQuantity = 0f, isMixType = true, materialType = materialType)
|
||||
).apply(op)
|
||||
|
||||
// ==== Exceptions ====
|
||||
private const val MIX_TYPE_NOT_FOUND_EXCEPTION_TITLE = "Mix type not found"
|
||||
private const val MIX_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE = "Mix type already exists"
|
||||
private const val MIX_TYPE_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete mix type"
|
||||
private const val MIX_TYPE_EXCEPTION_ERROR_CODE = "mixtype"
|
||||
|
||||
class MixTypeNameAndMaterialTypeNotFoundException(name: String, materialType: MaterialType) :
|
||||
RestException(
|
||||
"notfound-mixtype-namematerialtype",
|
||||
MIX_TYPE_NOT_FOUND_EXCEPTION_TITLE,
|
||||
HttpStatus.NOT_FOUND,
|
||||
"A mix type with the name $name and material type ${materialType.name} could not be found",
|
||||
mapOf(
|
||||
"name" to name,
|
||||
"materialType" to materialType.name
|
||||
)
|
||||
)
|
||||
|
||||
fun mixTypeIdNotFoundException(id: Long) =
|
||||
NotFoundException(
|
||||
MIX_TYPE_EXCEPTION_ERROR_CODE,
|
||||
MIX_TYPE_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"A mix type with the id $id could not be found",
|
||||
id
|
||||
)
|
||||
|
||||
fun mixTypeIdAlreadyExistsException(id: Long) =
|
||||
AlreadyExistsException(
|
||||
MIX_TYPE_EXCEPTION_ERROR_CODE,
|
||||
MIX_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A mix type with the id $id already exists",
|
||||
id
|
||||
)
|
||||
|
||||
fun mixTypeNameNotFoundException(name: String) =
|
||||
NotFoundException(
|
||||
MIX_TYPE_EXCEPTION_ERROR_CODE,
|
||||
MIX_TYPE_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"A mix type with the name $name could not be found",
|
||||
name,
|
||||
"name"
|
||||
)
|
||||
|
||||
fun mixTypeNameAlreadyExistsException(name: String) =
|
||||
AlreadyExistsException(
|
||||
MIX_TYPE_EXCEPTION_ERROR_CODE,
|
||||
MIX_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A mix type with the name $name already exists",
|
||||
name,
|
||||
"name"
|
||||
)
|
||||
|
||||
fun cannotDeleteMixTypeException(mixType: MixType) =
|
||||
CannotDeleteException(
|
||||
MIX_TYPE_EXCEPTION_ERROR_CODE,
|
||||
MIX_TYPE_CANNOT_DELETE_EXCEPTION_TITLE,
|
||||
"Cannot delete the mix type ${mixType.name} because one or more mixes depends on it"
|
||||
)
|
||||
|
@ -0,0 +1,22 @@
|
||||
package dev.fyloz.colorrecipesexplorer.model
|
||||
|
||||
/** The model of a stored entity. Each model should implements its own equals and hashCode methods to keep compatibility with the legacy Java and Thymeleaf code. */
|
||||
interface Model {
|
||||
val id: Long?
|
||||
}
|
||||
|
||||
interface NamedModel : Model {
|
||||
val name: String
|
||||
}
|
||||
|
||||
interface EntityDto<out E> {
|
||||
/** Converts the dto to an actual entity. */
|
||||
fun toEntity(): E {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
|
||||
// GENERAL VALIDATION MESSAGES
|
||||
const val VALIDATION_SIZE_GE_ZERO = "Must be greater or equals to 0"
|
||||
const val VALIDATION_SIZE_GE_ONE = "Must be greater or equals to 1"
|
||||
const val VALIDATION_RANGE_PERCENTS = "Must be between 0 and 100"
|
@ -1,6 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.model
|
||||
|
||||
/** Represents an entity with an id, named differently to prevent conflicts with the JPA annotation. */
|
||||
interface ModelEntity {
|
||||
val id: Long
|
||||
}
|
@ -1,15 +1,28 @@
|
||||
package dev.fyloz.colorrecipesexplorer.model
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Group
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.group
|
||||
import dev.fyloz.colorrecipesexplorer.rest.CRE_PROPERTIES
|
||||
import dev.fyloz.colorrecipesexplorer.rest.files.FILE_CONTROLLER_PATH
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.time.LocalDate
|
||||
import javax.persistence.*
|
||||
import javax.validation.constraints.*
|
||||
|
||||
private const val VALIDATION_COLOR_PATTERN = "^#([0-9a-f]{6})$"
|
||||
|
||||
const val RECIPE_IMAGES_DIRECTORY = "images/recipes"
|
||||
|
||||
@Entity
|
||||
@Table(name = "recipe")
|
||||
data class Recipe(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
override val id: Long,
|
||||
override val id: Long?,
|
||||
|
||||
/** The name of the recipe. It is not unique in the entire system, but is unique in the scope of a [Company]. */
|
||||
val name: String,
|
||||
@ -34,28 +47,253 @@ data class Recipe(
|
||||
@JoinColumn(name = "company_id")
|
||||
val company: Company,
|
||||
|
||||
@OneToMany(cascade = [CascadeType.ALL], mappedBy = "recipeId")
|
||||
val mixes: List<Mix>,
|
||||
@OneToMany(cascade = [CascadeType.ALL], mappedBy = "recipe")
|
||||
val mixes: MutableList<Mix>,
|
||||
|
||||
@OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true)
|
||||
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
|
||||
@JoinColumn(name = "recipe_id")
|
||||
val groupsInformation: List<RecipeGroupInformation>
|
||||
) : ModelEntity
|
||||
val groupsInformation: Set<RecipeGroupInformation>
|
||||
) : Model {
|
||||
/** The mix types contained in this recipe. */
|
||||
val mixTypes: Collection<MixType>
|
||||
@JsonIgnore
|
||||
get() = mixes.map { it.mixType }
|
||||
|
||||
val imagesDirectoryPath
|
||||
@JsonIgnore
|
||||
@Transient
|
||||
get() = "$RECIPE_IMAGES_DIRECTORY/$id"
|
||||
|
||||
fun groupInformationForGroup(groupId: Long) =
|
||||
groupsInformation.firstOrNull { it.group.id == groupId }
|
||||
|
||||
fun imageUrl(name: String) =
|
||||
"${CRE_PROPERTIES.deploymentUrl}$FILE_CONTROLLER_PATH?path=${
|
||||
URLEncoder.encode(
|
||||
"${this.imagesDirectoryPath}/$name",
|
||||
StandardCharsets.UTF_8
|
||||
)
|
||||
}"
|
||||
}
|
||||
|
||||
open class RecipeSaveDto(
|
||||
@field:NotBlank
|
||||
val name: String,
|
||||
|
||||
@field:NotBlank
|
||||
val description: String,
|
||||
|
||||
@field:NotBlank
|
||||
@field:Pattern(regexp = VALIDATION_COLOR_PATTERN)
|
||||
val color: String,
|
||||
|
||||
@field:Min(0, message = VALIDATION_RANGE_PERCENTS)
|
||||
@field:Max(100, message = VALIDATION_RANGE_PERCENTS)
|
||||
val gloss: Byte,
|
||||
|
||||
@field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
|
||||
val sample: Int?,
|
||||
|
||||
val approbationDate: LocalDate?,
|
||||
|
||||
val remark: String?,
|
||||
|
||||
val companyId: Long
|
||||
) : EntityDto<Recipe> {
|
||||
override fun toEntity(): Recipe = recipe(
|
||||
name = name,
|
||||
description = description,
|
||||
sample = sample,
|
||||
approbationDate = approbationDate,
|
||||
remark = remark ?: "",
|
||||
company = company(id = companyId)
|
||||
)
|
||||
}
|
||||
|
||||
open class RecipeUpdateDto(
|
||||
val id: Long,
|
||||
|
||||
@field:NotBlank
|
||||
val name: String?,
|
||||
|
||||
@field:NotBlank
|
||||
val description: String?,
|
||||
|
||||
@field:NotBlank
|
||||
@field:Pattern(regexp = VALIDATION_COLOR_PATTERN)
|
||||
val color: String?,
|
||||
|
||||
@field:Min(0, message = VALIDATION_RANGE_PERCENTS)
|
||||
@field:Max(100, message = VALIDATION_RANGE_PERCENTS)
|
||||
val gloss: Byte?,
|
||||
|
||||
@field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
|
||||
val sample: Int?,
|
||||
|
||||
val approbationDate: LocalDate?,
|
||||
|
||||
val remark: String?,
|
||||
|
||||
val steps: Set<RecipeStepsDto>?
|
||||
) : EntityDto<Recipe>
|
||||
|
||||
data class RecipeOutputDto(
|
||||
override val id: Long,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val color: String,
|
||||
val gloss: Byte,
|
||||
val sample: Int?,
|
||||
val approbationDate: LocalDate?,
|
||||
val remark: String?,
|
||||
val company: Company,
|
||||
val mixes: Set<MixOutputDto>,
|
||||
val groupsInformation: Set<RecipeGroupInformation>,
|
||||
var imagesUrls: Set<String>
|
||||
) : Model
|
||||
|
||||
@Entity
|
||||
@Table(name = "recipe_group_information")
|
||||
data class RecipeGroupInformation(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
override val id: Long,
|
||||
val id: Long?,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "group_id")
|
||||
val group: Group,
|
||||
|
||||
val note: String?,
|
||||
var note: String?,
|
||||
|
||||
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
|
||||
@JoinColumn(name = "recipe_group_information_id")
|
||||
val steps: List<RecipeStep>?
|
||||
) : ModelEntity
|
||||
var steps: MutableSet<RecipeStep>?
|
||||
)
|
||||
|
||||
data class RecipeStepsDto(
|
||||
val groupId: Long,
|
||||
|
||||
val steps: Set<RecipeStep>
|
||||
)
|
||||
|
||||
data class RecipePublicDataDto(
|
||||
val recipeId: Long,
|
||||
|
||||
val notes: Set<NoteDto>?,
|
||||
|
||||
val mixesLocation: Set<MixLocationDto>?
|
||||
)
|
||||
|
||||
data class NoteDto(
|
||||
val groupId: Long,
|
||||
|
||||
val content: String?
|
||||
)
|
||||
|
||||
// ==== DSL ====
|
||||
fun recipe(
|
||||
id: Long? = null,
|
||||
name: String = "name",
|
||||
description: String = "description",
|
||||
color: String = "ffffff",
|
||||
gloss: Byte = 0,
|
||||
sample: Int? = -1,
|
||||
approbationDate: LocalDate? = LocalDate.MIN,
|
||||
remark: String = "remark",
|
||||
company: Company = company(),
|
||||
mixes: MutableList<Mix> = mutableListOf(),
|
||||
groupsInformation: Set<RecipeGroupInformation> = setOf(),
|
||||
op: Recipe.() -> Unit = {}
|
||||
) = Recipe(
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
color,
|
||||
gloss,
|
||||
sample,
|
||||
approbationDate,
|
||||
remark,
|
||||
company,
|
||||
mixes,
|
||||
groupsInformation
|
||||
).apply(op)
|
||||
|
||||
fun recipeSaveDto(
|
||||
name: String = "name",
|
||||
description: String = "description",
|
||||
color: String = "ffffff",
|
||||
gloss: Byte = 0,
|
||||
sample: Int? = -1,
|
||||
approbationDate: LocalDate? = LocalDate.MIN,
|
||||
remark: String = "remark",
|
||||
companyId: Long = 0L,
|
||||
op: RecipeSaveDto.() -> Unit = {}
|
||||
) = RecipeSaveDto(name, description, color, gloss, sample, approbationDate, remark, companyId).apply(op)
|
||||
|
||||
fun recipeUpdateDto(
|
||||
id: Long = 0L,
|
||||
name: String? = "name",
|
||||
description: String? = "description",
|
||||
color: String? = "ffffff",
|
||||
gloss: Byte? = 0,
|
||||
sample: Int? = -1,
|
||||
approbationDate: LocalDate? = LocalDate.MIN,
|
||||
remark: String? = "remark",
|
||||
steps: Set<RecipeStepsDto>? = setOf(),
|
||||
op: RecipeUpdateDto.() -> Unit = {}
|
||||
) = RecipeUpdateDto(id, name, description, color, gloss, sample, approbationDate, remark, steps).apply(op)
|
||||
|
||||
fun recipeGroupInformation(
|
||||
id: Long? = null,
|
||||
group: Group = group(),
|
||||
note: String? = null,
|
||||
steps: MutableSet<RecipeStep>? = mutableSetOf(),
|
||||
op: RecipeGroupInformation.() -> Unit = {}
|
||||
) = RecipeGroupInformation(id, group, note, steps).apply(op)
|
||||
|
||||
fun recipePublicDataDto(
|
||||
recipeId: Long = 0L,
|
||||
notes: Set<NoteDto>? = null,
|
||||
mixesLocation: Set<MixLocationDto>? = null,
|
||||
op: RecipePublicDataDto.() -> Unit = {}
|
||||
) = RecipePublicDataDto(recipeId, notes, mixesLocation).apply(op)
|
||||
|
||||
fun noteDto(
|
||||
groupId: Long = 0L,
|
||||
content: String? = "note",
|
||||
op: NoteDto.() -> Unit = {}
|
||||
) = NoteDto(groupId, content).apply(op)
|
||||
|
||||
// ==== Exceptions ====
|
||||
private const val RECIPE_NOT_FOUND_EXCEPTION_TITLE = "Recipe not found"
|
||||
private const val RECIPE_ALREADY_EXISTS_EXCEPTION_TITLE = "Recipe already exists"
|
||||
private const val RECIPE_EXCEPTION_ERROR_CODE = "recipe"
|
||||
|
||||
fun recipeIdNotFoundException(id: Long) =
|
||||
NotFoundException(
|
||||
RECIPE_EXCEPTION_ERROR_CODE,
|
||||
RECIPE_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"A recipe with the id $id could not be found",
|
||||
id
|
||||
)
|
||||
|
||||
fun recipeIdAlreadyExistsException(id: Long) =
|
||||
AlreadyExistsException(
|
||||
RECIPE_EXCEPTION_ERROR_CODE,
|
||||
RECIPE_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A recipe with the id $id already exists",
|
||||
id
|
||||
)
|
||||
|
||||
fun recipeNameAlreadyExistsForCompanyException(name: String, company: Company) =
|
||||
AlreadyExistsException(
|
||||
"${RECIPE_EXCEPTION_ERROR_CODE}-company",
|
||||
RECIPE_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A recipe with the name $name already exists for the company ${company.name}",
|
||||
name,
|
||||
"name",
|
||||
mutableMapOf(
|
||||
"company" to company.name,
|
||||
"companyId" to company.id!!
|
||||
)
|
||||
)
|
||||
|
@ -1,5 +1,7 @@
|
||||
package dev.fyloz.colorrecipesexplorer.model
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import javax.persistence.*
|
||||
|
||||
@Entity
|
||||
@ -7,9 +9,38 @@ import javax.persistence.*
|
||||
data class RecipeStep(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
override val id: Long,
|
||||
override val id: Long?,
|
||||
|
||||
val position: Int,
|
||||
|
||||
val message: String
|
||||
) : ModelEntity
|
||||
) : Model
|
||||
|
||||
// ==== DSL ====
|
||||
fun recipeStep(
|
||||
id: Long? = null,
|
||||
position: Int = 0,
|
||||
message: String = "message",
|
||||
op: RecipeStep.() -> Unit = {}
|
||||
) = RecipeStep(id, position, message).apply(op)
|
||||
|
||||
// ==== Exceptions ====
|
||||
private const val RECIPE_STEP_NOT_FOUND_EXCEPTION_TITLE = "Recipe step not found"
|
||||
private const val RECIPE_STEP_ALREADY_EXISTS_EXCEPTION_TITLE = "Recipe step already exists"
|
||||
private const val RECIPE_STEP_EXCEPTION_ERROR_CODE = "recipestep"
|
||||
|
||||
fun recipeStepIdNotFoundException(id: Long) =
|
||||
NotFoundException(
|
||||
RECIPE_STEP_EXCEPTION_ERROR_CODE,
|
||||
RECIPE_STEP_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"A recipe step with the id $id could not be found",
|
||||
id
|
||||
)
|
||||
|
||||
fun recipeStepIdAlreadyExistsException(id: Long) =
|
||||
AlreadyExistsException(
|
||||
RECIPE_STEP_EXCEPTION_ERROR_CODE,
|
||||
RECIPE_STEP_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A recipe step with the id $id already exists",
|
||||
id
|
||||
)
|
||||
|
@ -1,24 +1,135 @@
|
||||
package dev.fyloz.colorrecipesexplorer.model.account
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.ModelEntity
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.RestException
|
||||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import org.hibernate.annotations.Fetch
|
||||
import org.hibernate.annotations.FetchMode
|
||||
import org.springframework.http.HttpStatus
|
||||
import javax.persistence.*
|
||||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.NotEmpty
|
||||
import javax.validation.constraints.NotNull
|
||||
import javax.validation.constraints.Size
|
||||
|
||||
@Entity
|
||||
@Table(name = "user_group")
|
||||
data class Group(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
override val id: Long,
|
||||
override var id: Long? = null,
|
||||
|
||||
@Column(unique = true)
|
||||
val name: String,
|
||||
override val name: String = "",
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@ElementCollection(fetch = FetchType.EAGER)
|
||||
@CollectionTable(name = "group_permission", joinColumns = [JoinColumn(name = "group_id")])
|
||||
@Column(name = "permission")
|
||||
@Fetch(FetchMode.SUBSELECT)
|
||||
val permissions: List<Permission>,
|
||||
) : ModelEntity
|
||||
val permissions: MutableSet<Permission> = mutableSetOf(),
|
||||
) : NamedModel {
|
||||
val flatPermissions: Set<Permission>
|
||||
get() = this.permissions
|
||||
.flatMap { it.flat() }
|
||||
.filter { !it.deprecated }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
open class GroupSaveDto(
|
||||
@field:NotBlank
|
||||
val name: String,
|
||||
|
||||
@field:NotEmpty
|
||||
val permissions: MutableSet<Permission>
|
||||
) : EntityDto<Group> {
|
||||
override fun toEntity(): Group =
|
||||
Group(null, name, permissions)
|
||||
}
|
||||
|
||||
open class GroupUpdateDto(
|
||||
val id: Long,
|
||||
|
||||
@field:NotBlank
|
||||
val name: String,
|
||||
|
||||
@field:NotEmpty
|
||||
val permissions: MutableSet<Permission>
|
||||
) : EntityDto<Group> {
|
||||
override fun toEntity(): Group =
|
||||
Group(id, name, permissions)
|
||||
}
|
||||
|
||||
data class GroupOutputDto(
|
||||
override val id: Long,
|
||||
val name: String,
|
||||
val permissions: Set<Permission>,
|
||||
val explicitPermissions: Set<Permission>
|
||||
): Model
|
||||
|
||||
fun group(
|
||||
id: Long? = null,
|
||||
name: String = "name",
|
||||
permissions: MutableSet<Permission> = mutableSetOf(),
|
||||
op: Group.() -> Unit = {}
|
||||
) = Group(id, name, permissions).apply(op)
|
||||
|
||||
fun groupSaveDto(
|
||||
name: String = "name",
|
||||
permissions: MutableSet<Permission> = mutableSetOf(),
|
||||
op: GroupSaveDto.() -> Unit = {}
|
||||
) = GroupSaveDto(name, permissions).apply(op)
|
||||
|
||||
fun groupUpdateDto(
|
||||
id: Long = 0L,
|
||||
name: String = "name",
|
||||
permissions: MutableSet<Permission> = mutableSetOf(),
|
||||
op: GroupUpdateDto.() -> Unit = {}
|
||||
) = GroupUpdateDto(id, name, permissions).apply(op)
|
||||
|
||||
// ==== Exceptions ====
|
||||
private const val GROUP_NOT_FOUND_EXCEPTION_TITLE = "Group not found"
|
||||
private const val GROUP_ALREADY_EXISTS_EXCEPTION_TITLE = "Group already exists"
|
||||
private const val GROUP_EXCEPTION_ERROR_CODE = "group"
|
||||
|
||||
class NoDefaultGroupException : RestException(
|
||||
"nodefaultgroup",
|
||||
"No default group",
|
||||
HttpStatus.NOT_FOUND,
|
||||
"No default group cookie is defined in the current request"
|
||||
)
|
||||
|
||||
fun groupIdNotFoundException(id: Long) =
|
||||
NotFoundException(
|
||||
GROUP_EXCEPTION_ERROR_CODE,
|
||||
GROUP_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"A group with the id $id could not be found",
|
||||
id
|
||||
)
|
||||
|
||||
fun groupNameNotFoundException(name: String) =
|
||||
NotFoundException(
|
||||
GROUP_EXCEPTION_ERROR_CODE,
|
||||
GROUP_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"A group with the name $name could not be found",
|
||||
name,
|
||||
"name"
|
||||
)
|
||||
|
||||
fun groupIdAlreadyExistsException(id: Long) =
|
||||
AlreadyExistsException(
|
||||
GROUP_EXCEPTION_ERROR_CODE,
|
||||
GROUP_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A group with the id $id already exists",
|
||||
id,
|
||||
)
|
||||
|
||||
fun groupNameAlreadyExistsException(name: String) =
|
||||
AlreadyExistsException(
|
||||
GROUP_EXCEPTION_ERROR_CODE,
|
||||
GROUP_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A group with the name $name already exists",
|
||||
name,
|
||||
"name"
|
||||
)
|
||||
|
@ -1,10 +1,20 @@
|
||||
package dev.fyloz.colorrecipesexplorer.model.account
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.ModelEntity
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.model.EntityDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.Model
|
||||
import org.hibernate.annotations.Fetch
|
||||
import org.hibernate.annotations.FetchMode
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import java.time.LocalDateTime
|
||||
import javax.persistence.*
|
||||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.Size
|
||||
|
||||
private const val VALIDATION_PASSWORD_LENGTH = "Must contains at least 8 characters"
|
||||
|
||||
@Entity
|
||||
@Table(name = "user")
|
||||
@ -13,31 +23,164 @@ data class User(
|
||||
override val id: Long,
|
||||
|
||||
@Column(name = "first_name")
|
||||
val firstName: String,
|
||||
val firstName: String = "",
|
||||
|
||||
@Column(name = "last_name")
|
||||
val lastName: String,
|
||||
val lastName: String = "",
|
||||
|
||||
val password: String,
|
||||
val password: String = "",
|
||||
|
||||
@Column(name = "default_group_user")
|
||||
val isDefaultGroupUser: Boolean,
|
||||
val isDefaultGroupUser: Boolean = false,
|
||||
|
||||
@Column(name = "system_user")
|
||||
val isSystemUser: Boolean,
|
||||
val isSystemUser: Boolean = false,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "group_id")
|
||||
@Fetch(FetchMode.SELECT)
|
||||
val group: Group?,
|
||||
var group: Group? = null,
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@ElementCollection(fetch = FetchType.EAGER)
|
||||
@CollectionTable(name = "user_permission", joinColumns = [JoinColumn(name = "user_id")])
|
||||
@Column(name = "permission")
|
||||
@Fetch(FetchMode.SUBSELECT)
|
||||
val permissions: List<Permission>,
|
||||
val permissions: MutableSet<Permission> = mutableSetOf(),
|
||||
|
||||
@Column(name = "last_login_time")
|
||||
var lastLoginTime: LocalDateTime? = null
|
||||
) : Model {
|
||||
val flatPermissions: Set<Permission>
|
||||
get() = permissions
|
||||
.flatMap { it.flat() }
|
||||
.filter { !it.deprecated }
|
||||
.toMutableSet()
|
||||
.apply {
|
||||
if (group != null) this.addAll(group!!.flatPermissions)
|
||||
}
|
||||
|
||||
val authorities: Set<GrantedAuthority>
|
||||
get() = flatPermissions.map { it.toAuthority() }.toMutableSet()
|
||||
}
|
||||
|
||||
open class UserSaveDto(
|
||||
val id: Long,
|
||||
|
||||
@field:NotBlank
|
||||
val firstName: String,
|
||||
|
||||
@field:NotBlank
|
||||
val lastName: String,
|
||||
|
||||
@field:NotBlank
|
||||
@field:Size(min = 8, message = VALIDATION_PASSWORD_LENGTH)
|
||||
val password: String,
|
||||
|
||||
val groupId: Long?,
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
val permissions: MutableSet<Permission> = mutableSetOf()
|
||||
) : EntityDto<User>
|
||||
|
||||
open class UserUpdateDto(
|
||||
val id: Long,
|
||||
|
||||
@field:NotBlank
|
||||
val firstName: String?,
|
||||
|
||||
@field:NotBlank
|
||||
val lastName: String?,
|
||||
|
||||
val groupId: Long?,
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
val permissions: Set<Permission>?
|
||||
) : EntityDto<User>
|
||||
|
||||
data class UserOutputDto(
|
||||
override val id: Long,
|
||||
val firstName: String,
|
||||
val lastName: String,
|
||||
val group: Group?,
|
||||
val permissions: Set<Permission>,
|
||||
val explicitPermissions: Set<Permission>,
|
||||
val lastLoginTime: LocalDateTime?
|
||||
) : ModelEntity
|
||||
) : Model
|
||||
|
||||
data class UserLoginRequest(val id: Long, val password: String)
|
||||
|
||||
// ==== DSL ====
|
||||
fun user(
|
||||
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
|
||||
id: Long = 0L,
|
||||
firstName: String = "firstName",
|
||||
lastName: String = "lastName",
|
||||
password: String = passwordEncoder.encode("password"),
|
||||
isDefaultGroupUser: Boolean = false,
|
||||
isSystemUser: Boolean = false,
|
||||
group: Group? = null,
|
||||
permissions: MutableSet<Permission> = mutableSetOf(),
|
||||
lastLoginTime: LocalDateTime? = null,
|
||||
op: User.() -> Unit = {}
|
||||
) = User(
|
||||
id,
|
||||
firstName,
|
||||
lastName,
|
||||
password,
|
||||
isDefaultGroupUser,
|
||||
isSystemUser,
|
||||
group,
|
||||
permissions,
|
||||
lastLoginTime
|
||||
).apply(op)
|
||||
|
||||
fun userSaveDto(
|
||||
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
|
||||
id: Long = 0L,
|
||||
firstName: String = "firstName",
|
||||
lastName: String = "lastName",
|
||||
password: String = passwordEncoder.encode("password"),
|
||||
groupId: Long? = null,
|
||||
permissions: MutableSet<Permission> = mutableSetOf(),
|
||||
op: UserSaveDto.() -> Unit = {}
|
||||
) = UserSaveDto(id, firstName, lastName, password, groupId, permissions).apply(op)
|
||||
|
||||
fun userUpdateDto(
|
||||
id: Long = 0L,
|
||||
firstName: String = "firstName",
|
||||
lastName: String = "lastName",
|
||||
groupId: Long? = null,
|
||||
permissions: MutableSet<Permission> = mutableSetOf(),
|
||||
op: UserUpdateDto.() -> Unit = {}
|
||||
) = UserUpdateDto(id, firstName, lastName, groupId, permissions).apply(op)
|
||||
|
||||
// ==== Exceptions ====
|
||||
private const val USER_NOT_FOUND_EXCEPTION_TITLE = "User not found"
|
||||
private const val USER_ALREADY_EXISTS_EXCEPTION_TITLE = "User already exists"
|
||||
private const val USER_EXCEPTION_ERROR_CODE = "user"
|
||||
|
||||
fun userIdNotFoundException(id: Long) =
|
||||
NotFoundException(
|
||||
USER_EXCEPTION_ERROR_CODE,
|
||||
USER_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"An user with the id $id could not be found",
|
||||
id
|
||||
)
|
||||
|
||||
fun userIdAlreadyExistsException(id: Long) =
|
||||
AlreadyExistsException(
|
||||
USER_EXCEPTION_ERROR_CODE,
|
||||
USER_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"An user with the id $id already exists",
|
||||
id
|
||||
)
|
||||
|
||||
fun userFullNameAlreadyExistsException(firstName: String, lastName: String) =
|
||||
AlreadyExistsException(
|
||||
USER_EXCEPTION_ERROR_CODE,
|
||||
USER_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"An user with the name '$firstName $lastName' already exists",
|
||||
"$firstName $lastName",
|
||||
"fullName"
|
||||
)
|
||||
|
@ -1,15 +1,24 @@
|
||||
package dev.fyloz.colorrecipesexplorer.model.touchupkit
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.ModelEntity
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.model.EntityDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.Model
|
||||
import dev.fyloz.colorrecipesexplorer.model.VALIDATION_SIZE_GE_ONE
|
||||
import java.time.LocalDate
|
||||
import javax.persistence.*
|
||||
import javax.validation.constraints.Min
|
||||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.NotEmpty
|
||||
|
||||
const val TOUCH_UP_KIT_DELIMITER = ';'
|
||||
|
||||
@Entity
|
||||
@Table(name = "touch_up_kit")
|
||||
data class TouchUpKit(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
override val id: Long,
|
||||
override val id: Long?,
|
||||
|
||||
val project: String,
|
||||
|
||||
@ -22,32 +31,181 @@ data class TouchUpKit(
|
||||
@Column(name = "shipping_date")
|
||||
val shippingDate: LocalDate,
|
||||
|
||||
@Column(name = "completion_date")
|
||||
val completionDate: LocalDate?,
|
||||
|
||||
@Column(name = "finish")
|
||||
val finish: String,
|
||||
private val finishConcatenated: String,
|
||||
|
||||
@Column(name = "material")
|
||||
val material: String,
|
||||
private val materialConcatenated: String,
|
||||
|
||||
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
|
||||
@JoinColumn(name = "touch_up_kit_id")
|
||||
val content: List<TouchUpKitProduct>
|
||||
) : ModelEntity
|
||||
val content: Set<TouchUpKitProduct>
|
||||
) : Model {
|
||||
val finish
|
||||
get() = finishConcatenated.split(TOUCH_UP_KIT_DELIMITER)
|
||||
|
||||
val material
|
||||
get() = materialConcatenated.split(TOUCH_UP_KIT_DELIMITER)
|
||||
}
|
||||
|
||||
@Entity
|
||||
@Table(name = "touch_up_kit_product")
|
||||
data class TouchUpKitProduct(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
override val id: Long,
|
||||
override val id: Long?,
|
||||
|
||||
val name: String,
|
||||
|
||||
val description: String?,
|
||||
|
||||
val quantity: Float,
|
||||
val quantity: Float
|
||||
) : Model
|
||||
|
||||
val ready: Boolean
|
||||
) : ModelEntity
|
||||
data class TouchUpKitSaveDto(
|
||||
@field:NotBlank
|
||||
val project: String,
|
||||
|
||||
@field:NotBlank
|
||||
val buggy: String,
|
||||
|
||||
@field:NotBlank
|
||||
val company: String,
|
||||
|
||||
@field:Min(1, message = VALIDATION_SIZE_GE_ONE)
|
||||
val quantity: Int,
|
||||
|
||||
val shippingDate: LocalDate,
|
||||
|
||||
@field:NotEmpty
|
||||
val finish: List<String>,
|
||||
|
||||
@field:NotEmpty
|
||||
val material: List<String>,
|
||||
|
||||
@field:NotEmpty
|
||||
val content: Set<TouchUpKitProductDto>
|
||||
) : EntityDto<TouchUpKit> {
|
||||
override fun toEntity() = touchUpKit(this)
|
||||
}
|
||||
|
||||
data class TouchUpKitUpdateDto(
|
||||
val id: Long,
|
||||
|
||||
@field:NotBlank
|
||||
val project: String?,
|
||||
|
||||
@field:NotBlank
|
||||
val buggy: String?,
|
||||
|
||||
@field:NotBlank
|
||||
val company: String?,
|
||||
|
||||
@field:Min(1, message = VALIDATION_SIZE_GE_ONE)
|
||||
val quantity: Int?,
|
||||
|
||||
val shippingDate: LocalDate?,
|
||||
|
||||
@field:NotEmpty
|
||||
val finish: List<String>?,
|
||||
|
||||
@field:NotEmpty
|
||||
val material: List<String>?,
|
||||
|
||||
@field:NotEmpty
|
||||
val content: Set<TouchUpKitProductDto>?
|
||||
) : EntityDto<TouchUpKit>
|
||||
|
||||
data class TouchUpKitOutputDto(
|
||||
override val id: Long,
|
||||
val project: String,
|
||||
val buggy: String,
|
||||
val company: String,
|
||||
val quantity: Int,
|
||||
val shippingDate: LocalDate,
|
||||
val finish: List<String>,
|
||||
val material: List<String>,
|
||||
val content: Set<TouchUpKitProduct>,
|
||||
val pdfUrl: String
|
||||
) : Model
|
||||
|
||||
data class TouchUpKitProductDto(
|
||||
val name: String,
|
||||
val description: String?,
|
||||
val quantity: Float
|
||||
)
|
||||
|
||||
// ==== DSL ====
|
||||
fun touchUpKit(
|
||||
id: Long? = null,
|
||||
project: String = "project",
|
||||
buggy: String = "buggy",
|
||||
company: String = "company",
|
||||
quantity: Int = 1,
|
||||
shippingDate: LocalDate = LocalDate.now(),
|
||||
finish: List<String>,
|
||||
material: List<String>,
|
||||
content: Set<TouchUpKitProduct>,
|
||||
op: TouchUpKit.() -> Unit = {}
|
||||
) = TouchUpKit(
|
||||
id,
|
||||
project,
|
||||
buggy,
|
||||
company,
|
||||
quantity,
|
||||
shippingDate,
|
||||
finish.reduce { acc, f -> "$acc$TOUCH_UP_KIT_DELIMITER$f" },
|
||||
material.reduce { acc, f -> "$acc$TOUCH_UP_KIT_DELIMITER$f" },
|
||||
content
|
||||
).apply(op)
|
||||
|
||||
fun touchUpKit(touchUpKitSaveDto: TouchUpKitSaveDto) =
|
||||
with(touchUpKitSaveDto) {
|
||||
touchUpKit(
|
||||
project = project,
|
||||
buggy = buggy,
|
||||
company = company,
|
||||
quantity = quantity,
|
||||
shippingDate = shippingDate,
|
||||
finish = finish,
|
||||
material = material,
|
||||
content = content.map { touchUpKitProduct(it) }.toSet()
|
||||
)
|
||||
}
|
||||
|
||||
fun touchUpKitProduct(
|
||||
id: Long? = null,
|
||||
name: String = "product",
|
||||
description: String? = "description",
|
||||
quantity: Float = 1f,
|
||||
op: TouchUpKitProduct.() -> Unit = {}
|
||||
) = TouchUpKitProduct(id, name, description, quantity)
|
||||
.apply(op)
|
||||
|
||||
fun touchUpKitProduct(touchUpKitProductDto: TouchUpKitProductDto) =
|
||||
touchUpKitProduct(
|
||||
name = touchUpKitProductDto.name,
|
||||
description = touchUpKitProductDto.description,
|
||||
quantity = touchUpKitProductDto.quantity
|
||||
)
|
||||
|
||||
// ==== Exceptions ====
|
||||
private const val TOUCH_UP_KIT_NOT_FOUND_EXCEPTION_TITLE = "Touch up kit not found"
|
||||
private const val TOUCH_UP_KIT_ALREADY_EXISTS_EXCEPTION_TITLE = "Touch up kit already exists"
|
||||
private const val TOUCH_UP_KIT_EXCEPTION_ERROR_CODE = "touchupkit"
|
||||
|
||||
fun touchUpKitIdNotFoundException(id: Long) =
|
||||
NotFoundException(
|
||||
TOUCH_UP_KIT_EXCEPTION_ERROR_CODE,
|
||||
TOUCH_UP_KIT_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"A touch up kit with the id $id could not be found",
|
||||
id
|
||||
)
|
||||
|
||||
fun touchUpKitIdAlreadyExistsException(id: Long) =
|
||||
AlreadyExistsException(
|
||||
TOUCH_UP_KIT_EXCEPTION_ERROR_CODE,
|
||||
TOUCH_UP_KIT_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A touch up kit with the id $id already exists",
|
||||
id
|
||||
)
|
||||
|
@ -0,0 +1,45 @@
|
||||
package dev.fyloz.colorrecipesexplorer.model.validation
|
||||
|
||||
import javax.validation.Constraint
|
||||
import javax.validation.ConstraintValidator
|
||||
import javax.validation.ConstraintValidatorContext
|
||||
import javax.validation.Payload
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.contract
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
private const val MESSAGE = "must be null or not blank"
|
||||
|
||||
@Target(AnnotationTarget.FIELD)
|
||||
@MustBeDocumented
|
||||
@Constraint(validatedBy = [NullOrNotBlankValidator::class])
|
||||
annotation class NullOrNotBlank(
|
||||
val message: String = MESSAGE,
|
||||
val groups: Array<KClass<*>> = [],
|
||||
@Suppress("unused") val payload: Array<KClass<out Payload>> = []
|
||||
)
|
||||
|
||||
class NullOrNotBlankValidator : ConstraintValidator<NullOrNotBlank, String> {
|
||||
var message = MESSAGE
|
||||
|
||||
override fun initialize(constraintAnnotation: NullOrNotBlank) {
|
||||
message = constraintAnnotation.message
|
||||
}
|
||||
|
||||
override fun isValid(value: String?, context: ConstraintValidatorContext): Boolean {
|
||||
return value.isNullOrNotBlank().apply {
|
||||
if (!this) context.buildConstraintViolationWithTemplate(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String?.isNullOrNotBlank(): Boolean = this == null || isNotBlank()
|
||||
|
||||
/** Checks if the given string [value] is not null and not blank. */
|
||||
@ExperimentalContracts
|
||||
fun isNotNullAndNotBlank(value: String?): Boolean {
|
||||
contract { returns(true) implies (value != null) }
|
||||
return value != null && value.isNotBlank()
|
||||
}
|
||||
|
||||
infix fun String?.or(alternative: String): String = if (isNotNullAndNotBlank(this)) this else alternative
|
@ -0,0 +1,46 @@
|
||||
package dev.fyloz.colorrecipesexplorer.model.validation
|
||||
|
||||
import javax.validation.Constraint
|
||||
import javax.validation.ConstraintValidator
|
||||
import javax.validation.ConstraintValidatorContext
|
||||
import javax.validation.Payload
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
private const val MIN_SIZE = Long.MIN_VALUE
|
||||
private const val MAX_SIZE = Long.MAX_VALUE
|
||||
private const val MESSAGE = "must be null or have a correct length"
|
||||
|
||||
@Target(AnnotationTarget.FIELD)
|
||||
@MustBeDocumented
|
||||
@Constraint(validatedBy = [NullOrSizeValidator::class])
|
||||
annotation class NullOrSize(
|
||||
val min: Long = MIN_SIZE,
|
||||
val max: Long = MAX_SIZE,
|
||||
val message: String = MESSAGE,
|
||||
val groups: Array<KClass<*>> = [],
|
||||
@Suppress("unused") val payload: Array<KClass<out Payload>> = []
|
||||
)
|
||||
|
||||
class NullOrSizeValidator : ConstraintValidator<NullOrSize, Any> {
|
||||
var min = MIN_SIZE
|
||||
var max = MAX_SIZE
|
||||
var message = MESSAGE
|
||||
|
||||
override fun initialize(constraintAnnotation: NullOrSize) {
|
||||
min = constraintAnnotation.min
|
||||
max = constraintAnnotation.max
|
||||
message = constraintAnnotation.message
|
||||
}
|
||||
|
||||
override fun isValid(value: Any?, context: ConstraintValidatorContext): Boolean {
|
||||
if (value == null) return true
|
||||
return when (value) {
|
||||
is Number -> value.toLong() in min..max
|
||||
is String -> value.length in min..max
|
||||
is Collection<*> -> value.size in min..max
|
||||
else -> throw IllegalStateException("Cannot use @NullOrSize on type ${value::class}")
|
||||
}.apply {
|
||||
if (!this) context.buildConstraintViolationWithTemplate(message)
|
||||
}
|
||||
}
|
||||
}
|
@ -3,28 +3,18 @@ package dev.fyloz.colorrecipesexplorer.repository
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Group
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.User
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface UserRepository : JpaRepository<User, Long> {
|
||||
/** Checks if a user with the given [firstName], [lastName] and a different [id] exists. */
|
||||
fun existsByFirstNameAndLastNameAndIdNot(firstName: String, lastName: String, id: Long): Boolean
|
||||
fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean
|
||||
|
||||
/** Finds all users for the given [group]. */
|
||||
@Query("SELECT u FROM User u WHERE u.group = :group AND u.isSystemUser IS FALSE AND u.isDefaultGroupUser IS FALSE")
|
||||
fun findAllByGroup(group: Group): Collection<User>
|
||||
|
||||
/** Finds the user with the given [firstName] and [lastName]. */
|
||||
fun findByFirstNameAndLastName(firstName: String, lastName: String): User?
|
||||
|
||||
/** Finds the default user for the given [group]. */
|
||||
@Query("SELECT u FROM User u WHERE u.group = :group AND u.isDefaultGroupUser IS TRUE")
|
||||
fun findDefaultGroupUser(group: Group): User?
|
||||
fun findAllByGroup(group: Group): Collection<User>
|
||||
|
||||
fun findByIsDefaultGroupUserIsTrueAndGroupIs(group: Group): User
|
||||
}
|
||||
|
||||
@Repository
|
||||
interface GroupRepository : JpaRepository<Group, Long> {
|
||||
/** Checks if a group with the given [name] and a different [id] exists. */
|
||||
fun existsByNameAndIdNot(name: String, id: Long): Boolean
|
||||
}
|
||||
interface GroupRepository : NamedJpaRepository<Group>
|
||||
|
@ -1,21 +1,18 @@
|
||||
package dev.fyloz.colorrecipesexplorer.repository
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.Company
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface CompanyRepository : JpaRepository<Company, Long> {
|
||||
/** Checks if a company with the given [name] and a different [id] exists. */
|
||||
fun existsByNameAndIdNot(name: String, id: Long): Boolean
|
||||
|
||||
/** Checks if a recipe depends on the company with the given [id]. */
|
||||
interface CompanyRepository : NamedJpaRepository<Company> {
|
||||
@Query(
|
||||
"""
|
||||
select case when(count(r) > 0) then true else false end
|
||||
from Recipe r where r.company.id = :id
|
||||
"""
|
||||
select case when(count(r.id) > 0) then false else true end
|
||||
from Company c
|
||||
left join Recipe r on c.id = r.company.id
|
||||
where c.id = :id
|
||||
"""
|
||||
)
|
||||
fun isUsedByRecipe(id: Long): Boolean
|
||||
fun canBeDeleted(id: Long): Boolean
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.repository
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.ConfigurationEntity
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
interface ConfigurationRepository : JpaRepository<ConfigurationEntity, String> {
|
||||
}
|
@ -1,33 +1,29 @@
|
||||
package dev.fyloz.colorrecipesexplorer.repository
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.Material
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import dev.fyloz.colorrecipesexplorer.model.MaterialType
|
||||
import org.springframework.data.jpa.repository.Modifying
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface MaterialRepository : JpaRepository<Material, Long> {
|
||||
/** Checks if a material with the given [name] and a different [id] exists. */
|
||||
fun existsByNameAndIdNot(name: String, id: Long): Boolean
|
||||
|
||||
/** Gets all non mix type materials. */
|
||||
fun findAllByIsMixTypeIsFalse(): Collection<Material>
|
||||
interface MaterialRepository : NamedJpaRepository<Material> {
|
||||
/** Checks if one or more materials have the given [materialType]. */
|
||||
fun existsByMaterialType(materialType: MaterialType): Boolean
|
||||
|
||||
/** Updates the [inventoryQuantity] of the [Material] with the given [id]. */
|
||||
@Modifying
|
||||
@Query("UPDATE Material m SET m.inventoryQuantity = :inventoryQuantity WHERE m.id = :id")
|
||||
fun updateInventoryQuantityById(id: Long, inventoryQuantity: Float)
|
||||
|
||||
/** Checks if a mix material or a mix type depends on the material with the given [id]. */
|
||||
@Query(
|
||||
"""
|
||||
"""
|
||||
select case when(count(mm.id) + count(mt.id) > 0) then false else true end
|
||||
from Material m
|
||||
left join MixMaterial mm on mm.material.id = m.id
|
||||
left join MixType mt on mt.material.id = m.id
|
||||
left join MixMaterial mm on m.id = mm.material.id
|
||||
left join MixType mt on m.id = mt.material.id
|
||||
where m.id = :id
|
||||
"""
|
||||
"""
|
||||
)
|
||||
fun isUsedByMixMaterialOrMixType(id: Long): Boolean
|
||||
fun canBeDeleted(id: Long): Boolean
|
||||
}
|
||||
|
@ -1,33 +1,27 @@
|
||||
package dev.fyloz.colorrecipesexplorer.repository
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.MaterialType
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface MaterialTypeRepository : JpaRepository<MaterialType, Long> {
|
||||
/** Checks if a system material type with the given [id] exists. */
|
||||
fun existsByIdAndSystemTypeIs(id: Long, systemType: Boolean): Boolean
|
||||
interface MaterialTypeRepository : NamedJpaRepository<MaterialType> {
|
||||
/** Checks if a material type exists with the given [prefix]. */
|
||||
fun existsByPrefix(prefix: String): Boolean
|
||||
|
||||
/** Checks if a material type with the given [name] and a different [id] exists. */
|
||||
fun existsByNameAndIdNot(name: String, id: Long): Boolean
|
||||
/** Gets all material types which are not system types. */
|
||||
fun findAllBySystemTypeIs(value: Boolean): Collection<MaterialType>
|
||||
|
||||
/** Checks if a material type with the given [prefix] and a different [id] exists. */
|
||||
fun existsByPrefixAndIdNot(prefix: String, id: Long): Boolean
|
||||
/** Gets the material type with the given [prefix]. */
|
||||
fun findByPrefix(prefix: String): MaterialType?
|
||||
|
||||
/** Find all material types which are or not [systemType]s. */
|
||||
fun findAllBySystemTypeIs(systemType: Boolean): Collection<MaterialType>
|
||||
|
||||
/** Find the material type with the given [name]. */
|
||||
fun findByName(name: String): MaterialType?
|
||||
|
||||
/** Checks if a material depends on the material type with the given [id]. */
|
||||
@Query(
|
||||
"""
|
||||
select case when(count(m) > 0) then true else false end
|
||||
from Material m where m.materialType.id = :id
|
||||
"""
|
||||
"""
|
||||
select case when(count(m.id) > 0) then false else true end
|
||||
from MaterialType t
|
||||
left join Material m on t.id = m.materialType.id
|
||||
where t.id = :id
|
||||
"""
|
||||
)
|
||||
fun isUsedByMaterial(id: Long): Boolean
|
||||
fun canBeDeleted(id: Long): Boolean
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
package dev.fyloz.colorrecipesexplorer.repository
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.Material
|
||||
import dev.fyloz.colorrecipesexplorer.model.MixMaterial
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface MixMaterialRepository : JpaRepository<MixMaterial, Long> {
|
||||
/** Checks if one or more mix materials have the given [materialId]. */
|
||||
fun existsByMaterialId(materialId: Long): Boolean
|
||||
/** Checks if one or more mix materials have the given [material]. */
|
||||
fun existsByMaterial(material: Material): Boolean
|
||||
}
|
||||
|
@ -1,34 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.repository
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.MixMixType
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Modifying
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface MixMixTypeRepository : JpaRepository<MixMixType, Long> {
|
||||
@Query(
|
||||
nativeQuery = true, value = """
|
||||
SELECT * FROM mix_mix_type mmt WHERE mmt.mix_id = :mixId
|
||||
"""
|
||||
)
|
||||
fun findAllByMixId(mixId: Long): List<MixMixType>
|
||||
|
||||
@Modifying
|
||||
@Query(
|
||||
nativeQuery = true, value = """
|
||||
INSERT INTO mix_mix_type (id, mix_type_id, mix_id, quantity, position)
|
||||
VALUES (:id, :mixTypeId, :mixId, :quantity, :position)
|
||||
"""
|
||||
)
|
||||
fun saveForMixId(id: Long?, mixTypeId: Long, mixId: Long, quantity: Float, position: Int)
|
||||
|
||||
@Modifying
|
||||
@Query(
|
||||
nativeQuery = true, value = """
|
||||
DELETE FROM mix_mix_type mmt WHERE mmt.mix_id = :mixId
|
||||
"""
|
||||
)
|
||||
fun deleteAllByMixId(mixId: Long)
|
||||
}
|
@ -7,11 +7,21 @@ import org.springframework.data.jpa.repository.Modifying
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
|
||||
interface MixRepository : JpaRepository<Mix, Long> {
|
||||
/** Finds all mixes with the mix type with the given [mixTypeId]. */
|
||||
fun findAllByMixTypeId(mixTypeId: Long): Collection<Mix>
|
||||
/** Finds all mixes with the given [mixType]. */
|
||||
fun findAllByMixType(mixType: MixType): Collection<Mix>
|
||||
|
||||
/** Updates the [location] of the [Mix] with the given [id]. */
|
||||
@Modifying
|
||||
@Query("UPDATE Mix m SET m.location = :location WHERE m.id = :id")
|
||||
fun updateLocationById(id: Long, location: String?)
|
||||
|
||||
@Query(
|
||||
"""
|
||||
select case when(count(mm.id) > 0) then false else true end
|
||||
from Mix m
|
||||
left join MixMaterial mm on m.mixType.material.id = mm.material.id
|
||||
where m.id = :id
|
||||
"""
|
||||
)
|
||||
fun canBeDeleted(id: Long): Boolean
|
||||
}
|
||||
|
@ -1,41 +1,30 @@
|
||||
package dev.fyloz.colorrecipesexplorer.repository
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.Material
|
||||
import dev.fyloz.colorrecipesexplorer.model.MaterialType
|
||||
import dev.fyloz.colorrecipesexplorer.model.MixType
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface MixTypeRepository : JpaRepository<MixType, Long> {
|
||||
/** Checks if a mix type with the given [name], [materialTypeId] and a different [id] exists. */
|
||||
interface MixTypeRepository : NamedJpaRepository<MixType> {
|
||||
@Query("select case when(count(m) > 0) then true else false end from MixType m where m.name = :name and m.material.materialType = :materialType")
|
||||
fun existsByNameAndMaterialType(name: String, materialType: MaterialType): Boolean
|
||||
|
||||
/** Gets the mix type with the given [material]. */
|
||||
fun findByMaterial(material: Material): MixType?
|
||||
|
||||
/** Gets the [MixType] with the given [name] and [materialType]. */
|
||||
@Query("select m from MixType m where m.name = :name and m.material.materialType = :materialType")
|
||||
fun findByNameAndMaterialType(name: String, materialType: MaterialType): MixType?
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT CASE WHEN(COUNT(mt.id)) > 1 THEN TRUE ELSE FALSE END
|
||||
FROM MixType mt
|
||||
WHERE mt.name = :name AND mt.materialType.id = :materialTypeId AND mt.id <> :id
|
||||
"""
|
||||
select case when(count(m.id) > 0) then false else true end
|
||||
from MixType t
|
||||
left join Mix m on t.id = m.mixType.id
|
||||
where t.id = :id
|
||||
"""
|
||||
)
|
||||
fun existsByNameAndMaterialTypeAndIdNot(name: String, materialTypeId: Long, id: Long): Boolean
|
||||
|
||||
/** Finds the mix type with the given [name] and [materialTypeId]. */
|
||||
@Query("SELECT m FROM MixType m WHERE m.name = :name AND m.material.materialType.id = :materialTypeId")
|
||||
fun findByNameAndMaterialType(name: String, materialTypeId: Long): MixType?
|
||||
|
||||
/** Checks if a mix depends on the mix type with the given [id]. */
|
||||
@Query(
|
||||
"""
|
||||
select case when(count(m.id) > 0) then false else true end
|
||||
from Mix m where m.mixType.id = :id
|
||||
"""
|
||||
)
|
||||
fun isUsedByMixes(id: Long): Boolean
|
||||
|
||||
/** Checks if the mix type with the given [id] is used by more than one mix. */
|
||||
@Query(
|
||||
"""
|
||||
select case when(count(m.id) > 1) then false else true end
|
||||
from Mix m where m.mixType.id = :id
|
||||
"""
|
||||
)
|
||||
fun isShared(id: Long): Boolean
|
||||
fun canBeDeleted(id: Long): Boolean
|
||||
}
|
||||
|
@ -1,19 +1,19 @@
|
||||
package dev.fyloz.colorrecipesexplorer.repository
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.Company
|
||||
import dev.fyloz.colorrecipesexplorer.model.Recipe
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
|
||||
interface RecipeRepository : JpaRepository<Recipe, Long> {
|
||||
/** Checks if a recipe with the given [name], [companyId] and a different [id] exists. */
|
||||
@Query(
|
||||
"""
|
||||
SELECT CASE WHEN(COUNT(r) > 0) THEN TRUE ELSE FALSE END
|
||||
FROM Recipe r WHERE r.name = :name AND r.company.id = :companyId AND r.id <> :id
|
||||
"""
|
||||
)
|
||||
fun existsByNameAndCompanyAndIdNot(name: String, companyId: Long, id: Long): Boolean
|
||||
/** Checks if one or more recipes have the given [company]. */
|
||||
fun existsByCompany(company: Company): Boolean
|
||||
|
||||
/** Checks if a recipe exists with the given [name] and [company]. */
|
||||
fun existsByNameAndCompany(name: String, company: Company): Boolean
|
||||
|
||||
/** Gets all recipes with the given [name]. */
|
||||
fun findAllByName(name: String): Collection<Recipe>
|
||||
|
||||
/** Gets all recipes with the given [company]. */
|
||||
fun findAllByCompany(company: Company): Collection<Recipe>
|
||||
}
|
||||
|
@ -0,0 +1,18 @@
|
||||
package dev.fyloz.colorrecipesexplorer.repository
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.NamedModel
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.repository.NoRepositoryBean
|
||||
|
||||
/** Adds support for entities using a name identifier. */
|
||||
@NoRepositoryBean
|
||||
interface NamedJpaRepository<E : NamedModel> : JpaRepository<E, Long> {
|
||||
/** Checks if an entity with the given [name]. */
|
||||
fun existsByName(name: String): Boolean
|
||||
|
||||
/** Gets the entity with the given [name]. */
|
||||
fun findByName(name: String): E?
|
||||
|
||||
/** Removes the entity with the given [name]. */
|
||||
fun deleteByName(name: String)
|
||||
}
|
@ -2,13 +2,5 @@ package dev.fyloz.colorrecipesexplorer.repository
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKit
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Modifying
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import java.time.LocalDate
|
||||
|
||||
interface TouchUpKitRepository : JpaRepository<TouchUpKit, Long> {
|
||||
/** Updates the [completionDate] of the touch up kit with the given [id]. */
|
||||
@Modifying
|
||||
@Query("UPDATE TouchUpKit t SET t.completionDate = :completionDate WHERE t.id = :id")
|
||||
fun updateCompletionDateById(id: Long, completionDate: LocalDate)
|
||||
}
|
||||
interface TouchUpKitRepository : JpaRepository<TouchUpKit, Long>
|
||||
|
@ -1,56 +1,70 @@
|
||||
package dev.fyloz.colorrecipesexplorer.rest
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditUsers
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewUsers
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserSaveDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.UserUpdateDto
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.UserLogic
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import org.springframework.context.annotation.Profile
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.*
|
||||
import dev.fyloz.colorrecipesexplorer.service.UserService
|
||||
import dev.fyloz.colorrecipesexplorer.service.GroupService
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import java.security.Principal
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
import javax.validation.Valid
|
||||
|
||||
private const val USER_CONTROLLER_PATH = "api/user"
|
||||
private const val GROUP_CONTROLLER_PATH = "api/user/group"
|
||||
|
||||
@RestController
|
||||
@RequestMapping(Constants.ControllerPaths.USER)
|
||||
@Profile("!emergency")
|
||||
class UserController(private val userLogic: UserLogic) {
|
||||
@RequestMapping(USER_CONTROLLER_PATH)
|
||||
class UserController(private val userService: UserService) {
|
||||
@GetMapping
|
||||
@PreAuthorizeViewUsers
|
||||
fun getAll() =
|
||||
ok(userLogic.getAll())
|
||||
ok(userService.getAllForOutput())
|
||||
|
||||
@GetMapping("{id}")
|
||||
@PreAuthorizeViewUsers
|
||||
fun getById(@PathVariable id: Long) =
|
||||
ok(userLogic.getById(id))
|
||||
ok(userService.getByIdForOutput(id))
|
||||
|
||||
@GetMapping("current")
|
||||
fun getCurrent(loggedInUser: Principal?) =
|
||||
if (loggedInUser != null)
|
||||
ok(
|
||||
with(userService) {
|
||||
getById(
|
||||
loggedInUser.name.toLong(),
|
||||
ignoreDefaultGroupUsers = false,
|
||||
ignoreSystemUsers = false
|
||||
).toOutput()
|
||||
}
|
||||
)
|
||||
else
|
||||
forbidden()
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorizeEditUsers
|
||||
fun save(@Valid @RequestBody user: UserSaveDto) =
|
||||
created<UserDto>(Constants.ControllerPaths.USER) {
|
||||
userLogic.save(user)
|
||||
created<UserOutputDto>(USER_CONTROLLER_PATH) {
|
||||
with(userService) {
|
||||
save(user).toOutput()
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@PreAuthorizeEditUsers
|
||||
fun update(@Valid @RequestBody user: UserUpdateDto) =
|
||||
noContent {
|
||||
userLogic.update(user)
|
||||
userService.update(user)
|
||||
}
|
||||
|
||||
@PutMapping("{id}/password", consumes = [MediaType.TEXT_PLAIN_VALUE])
|
||||
@PreAuthorizeEditUsers
|
||||
fun updatePassword(@PathVariable id: Long, @RequestBody password: String) =
|
||||
noContent {
|
||||
userLogic.updatePassword(id, password)
|
||||
userService.updatePassword(id, password)
|
||||
}
|
||||
|
||||
@PutMapping("{userId}/permissions/{permission}")
|
||||
@ -59,7 +73,7 @@ class UserController(private val userLogic: UserLogic) {
|
||||
@PathVariable userId: Long,
|
||||
@PathVariable permission: Permission
|
||||
) = noContent {
|
||||
userLogic.addPermission(userId, permission)
|
||||
userService.addPermission(userId, permission)
|
||||
}
|
||||
|
||||
@DeleteMapping("{userId}/permissions/{permission}")
|
||||
@ -68,87 +82,83 @@ class UserController(private val userLogic: UserLogic) {
|
||||
@PathVariable userId: Long,
|
||||
@PathVariable permission: Permission
|
||||
) = noContent {
|
||||
userLogic.removePermission(userId, permission)
|
||||
userService.removePermission(userId, permission)
|
||||
}
|
||||
|
||||
@DeleteMapping("{id}")
|
||||
@PreAuthorizeEditUsers
|
||||
fun deleteById(@PathVariable id: Long) =
|
||||
userLogic.deleteById(id)
|
||||
userService.deleteById(id)
|
||||
}
|
||||
|
||||
@RestController
|
||||
@RequestMapping(Constants.ControllerPaths.GROUP)
|
||||
@Profile("!emergency")
|
||||
@RequestMapping(GROUP_CONTROLLER_PATH)
|
||||
class GroupsController(
|
||||
private val groupLogic: GroupLogic,
|
||||
private val userLogic: UserLogic
|
||||
private val groupService: GroupService,
|
||||
private val userService: UserService
|
||||
) {
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAnyAuthority('VIEW_RECIPES', 'VIEW_USERS')")
|
||||
fun getAll() =
|
||||
ok(groupLogic.getAll())
|
||||
ok(groupService.getAllForOutput())
|
||||
|
||||
@GetMapping("{id}")
|
||||
@PreAuthorizeViewUsers
|
||||
fun getById(@PathVariable id: Long) =
|
||||
ok(groupLogic.getById(id))
|
||||
ok(groupService.getByIdForOutput(id))
|
||||
|
||||
@GetMapping("{id}/users")
|
||||
@PreAuthorizeViewUsers
|
||||
fun getUsersForGroup(@PathVariable id: Long) =
|
||||
ok(groupLogic.getUsersForGroup(id))
|
||||
ok(with(userService) {
|
||||
groupService.getUsersForGroup(id)
|
||||
.map { it.toOutput() }
|
||||
})
|
||||
|
||||
@PostMapping("default/{groupId}")
|
||||
@PreAuthorizeViewUsers
|
||||
fun setDefaultGroup(@PathVariable groupId: Long, response: HttpServletResponse) =
|
||||
noContent {
|
||||
groupLogic.setResponseDefaultGroup(groupId, response)
|
||||
groupService.setResponseDefaultGroup(groupId, response)
|
||||
}
|
||||
|
||||
@GetMapping("default")
|
||||
@PreAuthorizeViewUsers
|
||||
fun getRequestDefaultGroup(request: HttpServletRequest) =
|
||||
ok(with(groupLogic) {
|
||||
getRequestDefaultGroup(request)
|
||||
})
|
||||
|
||||
@GetMapping("currentuser")
|
||||
fun getCurrentGroupUser(request: HttpServletRequest) =
|
||||
ok(with(groupLogic.getRequestDefaultGroup(request)) {
|
||||
userLogic.getDefaultGroupUser(this)
|
||||
ok(with(groupService) {
|
||||
getRequestDefaultGroup(request).toOutput()
|
||||
})
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorizeEditUsers
|
||||
fun save(@Valid @RequestBody group: GroupDto) =
|
||||
created<GroupDto>(Constants.ControllerPaths.GROUP) {
|
||||
groupLogic.save(group)
|
||||
fun save(@Valid @RequestBody group: GroupSaveDto) =
|
||||
created<GroupOutputDto>(GROUP_CONTROLLER_PATH) {
|
||||
with(groupService) {
|
||||
save(group).toOutput()
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@PreAuthorizeEditUsers
|
||||
fun update(@Valid @RequestBody group: GroupDto) =
|
||||
fun update(@Valid @RequestBody group: GroupUpdateDto) =
|
||||
noContent {
|
||||
groupLogic.update(group)
|
||||
groupService.update(group)
|
||||
}
|
||||
|
||||
@DeleteMapping("{id}")
|
||||
@PreAuthorizeEditUsers
|
||||
fun deleteById(@PathVariable id: Long) =
|
||||
noContent {
|
||||
groupLogic.deleteById(id)
|
||||
groupService.deleteById(id)
|
||||
}
|
||||
}
|
||||
|
||||
@RestController
|
||||
@RequestMapping("api")
|
||||
@Profile("!emergency")
|
||||
class LogoutController(private val userLogic: UserLogic) {
|
||||
class LogoutController(private val userService: UserService) {
|
||||
@GetMapping("logout")
|
||||
@PreAuthorize("isFullyAuthenticated()")
|
||||
fun logout(request: HttpServletRequest) =
|
||||
ok {
|
||||
userLogic.logout(request)
|
||||
ok<Void> {
|
||||
userService.logout(request)
|
||||
}
|
||||
}
|
||||
|
@ -1,45 +1,46 @@
|
||||
package dev.fyloz.colorrecipesexplorer.rest
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewCatalog
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto
|
||||
import dev.fyloz.colorrecipesexplorer.logic.CompanyLogic
|
||||
import dev.fyloz.colorrecipesexplorer.model.Company
|
||||
import dev.fyloz.colorrecipesexplorer.model.CompanySaveDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.CompanyUpdateDto
|
||||
import dev.fyloz.colorrecipesexplorer.service.CompanyService
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import javax.validation.Valid
|
||||
|
||||
private const val COMPANY_CONTROLLER_PATH = "api/company"
|
||||
|
||||
@RestController
|
||||
@RequestMapping(Constants.ControllerPaths.COMPANY)
|
||||
@RequireDatabase
|
||||
@RequestMapping(COMPANY_CONTROLLER_PATH)
|
||||
@PreAuthorizeViewCatalog
|
||||
class CompanyController(private val companyLogic: CompanyLogic) {
|
||||
class CompanyController(private val companyService: CompanyService) {
|
||||
@GetMapping
|
||||
fun getAll() =
|
||||
ok(companyLogic.getAll())
|
||||
ok(companyService.getAllForOutput())
|
||||
|
||||
@GetMapping("{id}")
|
||||
fun getById(@PathVariable id: Long) =
|
||||
ok(companyLogic.getById(id))
|
||||
ok(companyService.getByIdForOutput(id))
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('EDIT_COMPANIES')")
|
||||
fun save(@Valid @RequestBody company: CompanyDto) =
|
||||
created<CompanyDto>(Constants.ControllerPaths.COMPANY) {
|
||||
companyLogic.save(company)
|
||||
}
|
||||
fun save(@Valid @RequestBody company: CompanySaveDto) =
|
||||
created<Company>(COMPANY_CONTROLLER_PATH) {
|
||||
companyService.save(company)
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@PreAuthorize("hasAuthority('EDIT_COMPANIES')")
|
||||
fun update(@Valid @RequestBody company: CompanyDto) =
|
||||
noContent {
|
||||
companyLogic.update(company)
|
||||
}
|
||||
fun update(@Valid @RequestBody company: CompanyUpdateDto) =
|
||||
noContent {
|
||||
companyService.update(company)
|
||||
}
|
||||
|
||||
@DeleteMapping("{id}")
|
||||
@PreAuthorize("hasAuthority('EDIT_COMPANIES')")
|
||||
fun deleteById(@PathVariable id: Long) =
|
||||
noContent {
|
||||
companyLogic.deleteById(id)
|
||||
}
|
||||
noContent {
|
||||
companyService.deleteById(id)
|
||||
}
|
||||
}
|
||||
|
@ -1,71 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.rest
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic
|
||||
import dev.fyloz.colorrecipesexplorer.model.ConfigurationBase
|
||||
import dev.fyloz.colorrecipesexplorer.model.ConfigurationDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.toAuthority
|
||||
import dev.fyloz.colorrecipesexplorer.restartApplication
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
|
||||
@RestController
|
||||
@RequestMapping("api/config")
|
||||
class ConfigurationController(val configurationLogic: ConfigurationLogic) {
|
||||
@GetMapping
|
||||
fun getAll(@RequestParam(required = false) keys: String?, authentication: Authentication?) =
|
||||
ok(with(configurationLogic) {
|
||||
if (keys != null) getAll(keys) else getAll()
|
||||
}.filter { authentication.hasAuthority(it) })
|
||||
|
||||
@GetMapping("{key}")
|
||||
fun get(@PathVariable key: String, authentication: Authentication?) = with(configurationLogic.get(key)) {
|
||||
if (authentication.hasAuthority(this)) ok(this) else forbidden()
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@PreAuthorize("hasAuthority('ADMIN')")
|
||||
fun set(@RequestBody configurations: List<ConfigurationDto>) = noContent {
|
||||
configurationLogic.set(configurations)
|
||||
}
|
||||
|
||||
@PostMapping("restart")
|
||||
@PreAuthorize("hasAuthority('ADMIN')")
|
||||
fun restart() = noContent {
|
||||
restartApplication()
|
||||
}
|
||||
|
||||
// Icon
|
||||
|
||||
@GetMapping("icon")
|
||||
fun getIcon() =
|
||||
okFile(configurationLogic.getConfiguredIcon(), MediaType.IMAGE_PNG_VALUE)
|
||||
|
||||
@PutMapping("icon")
|
||||
@PreAuthorize("hasAuthority('ADMIN')")
|
||||
fun setIcon(@RequestParam icon: MultipartFile) = noContent {
|
||||
configurationLogic.setConfiguredIcon(icon)
|
||||
}
|
||||
|
||||
// Logo
|
||||
|
||||
@GetMapping("logo")
|
||||
fun getLogo() =
|
||||
okFile(configurationLogic.getConfiguredLogo(), MediaType.IMAGE_PNG_VALUE)
|
||||
|
||||
@PutMapping("logo")
|
||||
@PreAuthorize("hasAuthority('ADMIN')")
|
||||
fun setLogo(@RequestParam logo: MultipartFile) = noContent {
|
||||
configurationLogic.setConfiguredLogo(logo)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Authentication?.hasAuthority(configuration: ConfigurationBase) = when {
|
||||
configuration.type.public -> true
|
||||
this != null && Permission.ADMIN.toAuthority() in this.authorities -> true
|
||||
else -> false
|
||||
}
|
||||
|
@ -1,48 +0,0 @@
|
||||
package dev.fyloz.colorrecipesexplorer.rest
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic
|
||||
import dev.fyloz.colorrecipesexplorer.model.ConfigurationType
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.net.URI
|
||||
|
||||
@RestController
|
||||
@RequestMapping(Constants.ControllerPaths.FILE)
|
||||
class FileController(
|
||||
private val fileLogic: WriteableFileLogic,
|
||||
private val configurationLogic: ConfigurationLogic
|
||||
) {
|
||||
@GetMapping(produces = [MediaType.APPLICATION_OCTET_STREAM_VALUE])
|
||||
fun upload(
|
||||
@RequestParam path: String,
|
||||
@RequestParam(required = false) mediaType: String?
|
||||
) = okFile(fileLogic.read(path), mediaType)
|
||||
|
||||
@PutMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
|
||||
@PreAuthorize("hasAnyAuthority('WRITE_FILE')")
|
||||
fun download(
|
||||
file: MultipartFile,
|
||||
@RequestParam path: String,
|
||||
@RequestParam(required = false) overwrite: Boolean = false
|
||||
): ResponseEntity<Void> {
|
||||
fileLogic.write(file, path, overwrite)
|
||||
return created(path)
|
||||
}
|
||||
|
||||
@DeleteMapping
|
||||
@PreAuthorize("hasAnyAuthority('WRITE_FILE')")
|
||||
fun delete(@RequestParam path: String): ResponseEntity<Void> =
|
||||
noContent {
|
||||
fileLogic.delete(path)
|
||||
}
|
||||
|
||||
private fun created(path: String): ResponseEntity<Void> =
|
||||
ResponseEntity
|
||||
.created(URI.create("${configurationLogic.get(ConfigurationType.INSTANCE_URL)}${Constants.ControllerPaths.FILE}?path=$path"))
|
||||
.build()
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user