Initial version of smartbooking generated by generator-jhipster@9.0.0-beta.0
This commit is contained in:
29
src/main/docker/app.yml
Normal file
29
src/main/docker/app.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
# This configuration is intended for development purpose, it's **your** responsibility to harden it for production
|
||||
name: smartbooking
|
||||
services:
|
||||
app:
|
||||
image: smartbooking
|
||||
environment:
|
||||
- _JAVA_OPTIONS=-Xmx512m -Xms256m
|
||||
- SPRING_PROFILES_ACTIVE=prod,api-docs
|
||||
- MANAGEMENT_PROMETHEUS_METRICS_EXPORT_ENABLED=true
|
||||
- SPRING_DATASOURCE_URL=jdbc:postgresql://postgresql:5432/smartbooking
|
||||
- SPRING_LIQUIBASE_URL=jdbc:postgresql://postgresql:5432/smartbooking
|
||||
ports:
|
||||
- 127.0.0.1:8080:8080
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- curl
|
||||
- -f
|
||||
- http://localhost:8080/management/health
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 40
|
||||
depends_on:
|
||||
postgresql:
|
||||
condition: service_healthy
|
||||
postgresql:
|
||||
extends:
|
||||
file: ./postgresql.yml
|
||||
service: postgresql
|
||||
3778
src/main/docker/grafana/provisioning/dashboards/JVM.json
Normal file
3778
src/main/docker/grafana/provisioning/dashboards/JVM.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: 'Prometheus'
|
||||
orgId: 1
|
||||
folder: ''
|
||||
type: file
|
||||
disableDeletion: false
|
||||
editable: true
|
||||
options:
|
||||
path: /etc/grafana/provisioning/dashboards
|
||||
@@ -0,0 +1,50 @@
|
||||
apiVersion: 1
|
||||
|
||||
# list of datasources that should be deleted from the database
|
||||
deleteDatasources:
|
||||
- name: Prometheus
|
||||
orgId: 1
|
||||
|
||||
# list of datasources to insert/update depending
|
||||
# whats available in the database
|
||||
datasources:
|
||||
# <string, required> name of the datasource. Required
|
||||
- name: Prometheus
|
||||
# <string, required> datasource type. Required
|
||||
type: prometheus
|
||||
# <string, required> access mode. direct or proxy. Required
|
||||
access: proxy
|
||||
# <int> org id. will default to orgId 1 if not specified
|
||||
orgId: 1
|
||||
# <string> url
|
||||
# On MacOS, replace localhost by host.docker.internal
|
||||
url: http://localhost:9090
|
||||
# <string> database password, if used
|
||||
password:
|
||||
# <string> database user, if used
|
||||
user:
|
||||
# <string> database name, if used
|
||||
database:
|
||||
# <bool> enable/disable basic auth
|
||||
basicAuth: false
|
||||
# <string> basic auth username
|
||||
basicAuthUser: admin
|
||||
# <string> basic auth password
|
||||
basicAuthPassword: admin
|
||||
# <bool> enable/disable with credentials headers
|
||||
withCredentials:
|
||||
# <bool> mark as default datasource. Max one per org
|
||||
isDefault: true
|
||||
# <map> fields that will be converted to json and stored in json_data
|
||||
jsonData:
|
||||
graphiteVersion: '1.1'
|
||||
tlsAuth: false
|
||||
tlsAuthWithCACert: false
|
||||
# <string> json object of data that will be encrypted.
|
||||
secureJsonData:
|
||||
tlsCACert: '...'
|
||||
tlsClientCert: '...'
|
||||
tlsClientKey: '...'
|
||||
version: 1
|
||||
# <bool> allow users to edit datasources from the UI.
|
||||
editable: true
|
||||
48
src/main/docker/jhipster-control-center.yml
Normal file
48
src/main/docker/jhipster-control-center.yml
Normal file
@@ -0,0 +1,48 @@
|
||||
## How to use JHCC docker compose
|
||||
# To allow JHCC to reach JHipster application from a docker container note that we set the host as host.docker.internal
|
||||
# To reach the application from a browser, you need to add '127.0.0.1 host.docker.internal' to your hosts file.
|
||||
### Discovery mode
|
||||
# JHCC supports 3 kinds of discovery mode: Consul, Eureka and static
|
||||
# In order to use one, please set SPRING_PROFILES_ACTIVE to one (and only one) of this values: consul,eureka,static
|
||||
### Discovery properties
|
||||
# According to the discovery mode choose as Spring profile, you have to set the right properties
|
||||
# please note that current properties are set to run JHCC with default values, personalize them if needed
|
||||
# and remove those from other modes. You can only have one mode active.
|
||||
#### Eureka
|
||||
# - EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE=http://admin:admin@host.docker.internal:8761/eureka/
|
||||
#### Consul
|
||||
# - SPRING_CLOUD_CONSUL_HOST=host.docker.internal
|
||||
# - SPRING_CLOUD_CONSUL_PORT=8500
|
||||
#### Static
|
||||
# Add instances to "MyApp"
|
||||
# - SPRING_CLOUD_DISCOVERY_CLIENT_SIMPLE_INSTANCES_MYAPP_0_URI=http://host.docker.internal:8081
|
||||
# - SPRING_CLOUD_DISCOVERY_CLIENT_SIMPLE_INSTANCES_MYAPP_1_URI=http://host.docker.internal:8082
|
||||
# Or add a new application named MyNewApp
|
||||
# - SPRING_CLOUD_DISCOVERY_CLIENT_SIMPLE_INSTANCES_MYNEWAPP_0_URI=http://host.docker.internal:8080
|
||||
# This configuration is intended for development purpose, it's **your** responsibility to harden it for production
|
||||
|
||||
#### IMPORTANT
|
||||
# If you choose Consul or Eureka mode:
|
||||
# Do not forget to remove the prefix "127.0.0.1" in front of their port in order to expose them.
|
||||
# This is required because JHCC needs to communicate with Consul or Eureka.
|
||||
# - In Consul mode, the ports are in the consul.yml file.
|
||||
# - In Eureka mode, the ports are in the jhipster-registry.yml file.
|
||||
|
||||
name: smartbooking
|
||||
services:
|
||||
jhipster-control-center:
|
||||
image: 'jhipster/jhipster-control-center:v0.5.0'
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
# Patch /etc/hosts to support resolving host.docker.internal to the internal IP address used by the host in all OSes
|
||||
- echo "`ip route | grep default | cut -d ' ' -f3` host.docker.internal" | tee -a /etc/hosts > /dev/null && java -jar /jhipster-control-center.jar
|
||||
environment:
|
||||
- _JAVA_OPTIONS=-Xmx512m -Xms256m
|
||||
- SPRING_PROFILES_ACTIVE=prod,api-docs,static
|
||||
- SPRING_CLOUD_DISCOVERY_CLIENT_SIMPLE_INSTANCES_SMARTBOOKING_0_URI=http://host.docker.internal:8080
|
||||
- LOGGING_FILE_NAME=/tmp/jhipster-control-center.log
|
||||
# If you want to expose these ports outside your dev PC,
|
||||
# remove the "127.0.0.1:" prefix
|
||||
ports:
|
||||
- 127.0.0.1:7419:7419
|
||||
40
src/main/docker/jib/entrypoint.sh
Normal file
40
src/main/docker/jib/entrypoint.sh
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "The application will start in ${JHIPSTER_SLEEP}s..." && sleep ${JHIPSTER_SLEEP}
|
||||
|
||||
# usage: file_env VAR [DEFAULT]
|
||||
# ie: file_env 'XYZ_DB_PASSWORD' 'example'
|
||||
# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
|
||||
# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature)
|
||||
file_env() {
|
||||
local var="$1"
|
||||
local file_var="${var}_FILE"
|
||||
local def="${2:-}"
|
||||
if [[ ${!var:-} && ${!file_var:-} ]]; then
|
||||
echo >&2 "error: both $var and $file_var are set (but are exclusive)"
|
||||
exit 1
|
||||
fi
|
||||
local val="$def"
|
||||
if [[ ${!var:-} ]]; then
|
||||
val="${!var}"
|
||||
elif [[ ${!file_var:-} ]]; then
|
||||
val="$(< "${!file_var}")"
|
||||
fi
|
||||
|
||||
if [[ -n $val ]]; then
|
||||
export "$var"="$val"
|
||||
fi
|
||||
|
||||
unset "$file_var"
|
||||
return 0
|
||||
}
|
||||
|
||||
file_env 'SPRING_DATASOURCE_URL'
|
||||
file_env 'SPRING_DATASOURCE_USERNAME'
|
||||
file_env 'SPRING_DATASOURCE_PASSWORD'
|
||||
file_env 'SPRING_LIQUIBASE_URL'
|
||||
file_env 'SPRING_LIQUIBASE_USER'
|
||||
file_env 'SPRING_LIQUIBASE_PASSWORD'
|
||||
file_env 'JHIPSTER_REGISTRY_PASSWORD'
|
||||
|
||||
exec java ${JAVA_OPTS} -noverify -XX:+AlwaysPreTouch -cp /app/resources/:/app/classes/:/app/libs/* "it.sw.pa.comune.artegna.SmartbookingApp" "$@"
|
||||
31
src/main/docker/monitoring.yml
Normal file
31
src/main/docker/monitoring.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
# This configuration is intended for development purpose, it's **your** responsibility to harden it for production
|
||||
name: smartbooking
|
||||
services:
|
||||
prometheus:
|
||||
image: prom/prometheus:v3.8.0
|
||||
volumes:
|
||||
- ./prometheus/:/etc/prometheus/
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
# If you want to expose these ports outside your dev PC,
|
||||
# remove the "127.0.0.1:" prefix
|
||||
ports:
|
||||
- 127.0.0.1:9090:9090
|
||||
# On MacOS, remove next line and replace localhost by host.docker.internal in prometheus/prometheus.yml and
|
||||
# grafana/provisioning/datasources/datasource.yml
|
||||
network_mode: 'host' # to test locally running service
|
||||
grafana:
|
||||
image: grafana/grafana:12.3.0
|
||||
volumes:
|
||||
- ./grafana/provisioning/:/etc/grafana/provisioning/
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||
- GF_USERS_ALLOW_SIGN_UP=false
|
||||
- GF_INSTALL_PLUGINS=grafana-piechart-panel
|
||||
# If you want to expose these ports outside your dev PC,
|
||||
# remove the "127.0.0.1:" prefix
|
||||
ports:
|
||||
- 127.0.0.1:3000:3000
|
||||
# On MacOS, remove next line and replace localhost by host.docker.internal in prometheus/prometheus.yml and
|
||||
# grafana/provisioning/datasources/datasource.yml
|
||||
network_mode: 'host' # to test locally running service
|
||||
19
src/main/docker/postgresql.yml
Normal file
19
src/main/docker/postgresql.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
# This configuration is intended for development purpose, it's **your** responsibility to harden it for production
|
||||
name: smartbooking
|
||||
services:
|
||||
postgresql:
|
||||
image: postgres:18.1
|
||||
# volumes:
|
||||
# - ~/volumes/jhipster/smartbooking/postgresql/:/var/lib/postgresql/data/
|
||||
environment:
|
||||
- POSTGRES_USER=smartbooking
|
||||
- POSTGRES_HOST_AUTH_METHOD=trust
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER}']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
# If you want to expose these ports outside your dev PC,
|
||||
# remove the "127.0.0.1:" prefix
|
||||
ports:
|
||||
- 127.0.0.1:5432:5432
|
||||
31
src/main/docker/prometheus/prometheus.yml
Normal file
31
src/main/docker/prometheus/prometheus.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
# Sample global config for monitoring JHipster applications
|
||||
global:
|
||||
scrape_interval: 15s # By default, scrape targets every 15 seconds.
|
||||
evaluation_interval: 15s # By default, scrape targets every 15 seconds.
|
||||
# scrape_timeout is set to the global default (10s).
|
||||
|
||||
# Attach these labels to any time series or alerts when communicating with
|
||||
# external systems (federation, remote storage, Alertmanager).
|
||||
external_labels:
|
||||
monitor: 'jhipster'
|
||||
|
||||
# A scrape configuration containing exactly one endpoint to scrape:
|
||||
# Here it's Prometheus itself.
|
||||
scrape_configs:
|
||||
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
|
||||
- job_name: 'prometheus'
|
||||
|
||||
# Override the global default and scrape targets from this job every 5 seconds.
|
||||
scrape_interval: 5s
|
||||
|
||||
# scheme defaults to 'http' enable https in case your application is server via https
|
||||
#scheme: https
|
||||
# basic auth is not needed by default. See https://www.jhipster.tech/monitoring/#configuring-metrics-forwarding for details
|
||||
#basic_auth:
|
||||
# username: admin
|
||||
# password: admin
|
||||
metrics_path: /management/prometheus
|
||||
static_configs:
|
||||
- targets:
|
||||
# On MacOS, replace localhost by host.docker.internal
|
||||
- localhost:8080
|
||||
7
src/main/docker/services.yml
Normal file
7
src/main/docker/services.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
# This configuration is intended for development purpose, it's **your** responsibility to harden it for production
|
||||
name: smartbooking
|
||||
services:
|
||||
postgresql:
|
||||
extends:
|
||||
file: ./postgresql.yml
|
||||
service: postgresql
|
||||
15
src/main/docker/sonar.yml
Normal file
15
src/main/docker/sonar.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
# This configuration is intended for development purpose, it's **your** responsibility to harden it for production
|
||||
name: smartbooking
|
||||
services:
|
||||
sonar:
|
||||
container_name: sonarqube
|
||||
image: sonarqube:25.11.0.114957-community
|
||||
# Forced authentication redirect for UI is turned off for out of the box experience while trying out SonarQube
|
||||
# For real use cases delete SONAR_FORCEAUTHENTICATION variable or set SONAR_FORCEAUTHENTICATION=true
|
||||
environment:
|
||||
- SONAR_FORCEAUTHENTICATION=false
|
||||
# If you want to expose these ports outside your dev PC,
|
||||
# remove the "127.0.0.1:" prefix
|
||||
ports:
|
||||
- 127.0.0.1:9001:9000
|
||||
- 127.0.0.1:9000:9000
|
||||
19
src/main/java/it/sw/pa/comune/artegna/ApplicationWebXml.java
Normal file
19
src/main/java/it/sw/pa/comune/artegna/ApplicationWebXml.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package it.sw.pa.comune.artegna;
|
||||
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
|
||||
import tech.jhipster.config.DefaultProfileUtil;
|
||||
|
||||
/**
|
||||
* This is a helper Java class that provides an alternative to creating a {@code web.xml}.
|
||||
* This will be invoked only when the application is deployed to a Servlet container like Tomcat, JBoss etc.
|
||||
*/
|
||||
public class ApplicationWebXml extends SpringBootServletInitializer {
|
||||
|
||||
@Override
|
||||
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
|
||||
// set a default to use when no profile is configured.
|
||||
DefaultProfileUtil.addDefaultProfile(application.application());
|
||||
return application.sources(SmartbookingApp.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package it.sw.pa.comune.artegna;
|
||||
|
||||
import jakarta.annotation.Generated;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Generated(value = "JHipster", comments = "Generated by JHipster 9.0.0-beta.0")
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target({ ElementType.TYPE })
|
||||
public @interface GeneratedByJHipster {}
|
||||
110
src/main/java/it/sw/pa/comune/artegna/SmartbookingApp.java
Normal file
110
src/main/java/it/sw/pa/comune/artegna/SmartbookingApp.java
Normal file
@@ -0,0 +1,110 @@
|
||||
package it.sw.pa.comune.artegna;
|
||||
|
||||
import it.sw.pa.comune.artegna.config.ApplicationProperties;
|
||||
import it.sw.pa.comune.artegna.config.CRLFLogConverter;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.core.env.Environment;
|
||||
import tech.jhipster.config.DefaultProfileUtil;
|
||||
import tech.jhipster.config.JHipsterConstants;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableConfigurationProperties({ LiquibaseProperties.class, ApplicationProperties.class })
|
||||
public class SmartbookingApp {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SmartbookingApp.class);
|
||||
|
||||
private final Environment env;
|
||||
|
||||
public SmartbookingApp(Environment env) {
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes smartbooking.
|
||||
* <p>
|
||||
* Spring profiles can be configured with a program argument --spring.profiles.active=your-active-profile
|
||||
* <p>
|
||||
* You can find more information on how profiles work with JHipster on <a href="https://www.jhipster.tech/profiles/">https://www.jhipster.tech/profiles/</a>.
|
||||
*/
|
||||
@PostConstruct
|
||||
public void initApplication() {
|
||||
Collection<String> activeProfiles = Arrays.asList(env.getActiveProfiles());
|
||||
if (
|
||||
activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT) &&
|
||||
activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_PRODUCTION)
|
||||
) {
|
||||
LOG.error(
|
||||
"You have misconfigured your application! It should not run " + "with both the 'dev' and 'prod' profiles at the same time."
|
||||
);
|
||||
}
|
||||
if (
|
||||
activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT) &&
|
||||
activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_CLOUD)
|
||||
) {
|
||||
LOG.error(
|
||||
"You have misconfigured your application! It should not " + "run with both the 'dev' and 'cloud' profiles at the same time."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main method, used to run the application.
|
||||
*
|
||||
* @param args the command line arguments.
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
var app = new SpringApplication(SmartbookingApp.class);
|
||||
DefaultProfileUtil.addDefaultProfile(app);
|
||||
Environment env = app.run(args).getEnvironment();
|
||||
logApplicationStartup(env);
|
||||
}
|
||||
|
||||
private static void logApplicationStartup(Environment env) {
|
||||
String protocol = Optional.ofNullable(env.getProperty("server.ssl.key-store"))
|
||||
.map(key -> "https")
|
||||
.orElse("http");
|
||||
String applicationName = env.getProperty("spring.application.name");
|
||||
String serverPort = env.getProperty("server.port");
|
||||
String contextPath = Optional.ofNullable(env.getProperty("server.servlet.context-path"))
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.orElse("/");
|
||||
var hostAddress = "localhost";
|
||||
try {
|
||||
hostAddress = InetAddress.getLocalHost().getHostAddress();
|
||||
} catch (UnknownHostException e) {
|
||||
LOG.warn("The host name could not be determined, using `localhost` as fallback");
|
||||
}
|
||||
LOG.info(
|
||||
CRLFLogConverter.CRLF_SAFE_MARKER,
|
||||
"""
|
||||
|
||||
----------------------------------------------------------
|
||||
\tApplication '{}' is running! Access URLs:
|
||||
\tLocal: \t\t{}://localhost:{}{}
|
||||
\tExternal: \t{}://{}:{}{}
|
||||
\tProfile(s): \t{}
|
||||
----------------------------------------------------------""",
|
||||
applicationName,
|
||||
protocol,
|
||||
serverPort,
|
||||
contextPath,
|
||||
protocol,
|
||||
hostAddress,
|
||||
serverPort,
|
||||
contextPath,
|
||||
env.getActiveProfiles().length == 0 ? env.getDefaultProfiles() : env.getActiveProfiles()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package it.sw.pa.comune.artegna.aop.logging;
|
||||
|
||||
import java.util.Arrays;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.AfterThrowing;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.env.Profiles;
|
||||
import tech.jhipster.config.JHipsterConstants;
|
||||
|
||||
/**
|
||||
* Aspect for logging execution of service and repository Spring components.
|
||||
*
|
||||
* By default, it only runs with the "dev" profile.
|
||||
*/
|
||||
@Aspect
|
||||
public class LoggingAspect {
|
||||
|
||||
private final Environment env;
|
||||
|
||||
public LoggingAspect(Environment env) {
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pointcut that matches all repositories, services and Web REST endpoints.
|
||||
*/
|
||||
@Pointcut(
|
||||
"within(@org.springframework.stereotype.Repository *)" +
|
||||
" || within(@org.springframework.stereotype.Service *)" +
|
||||
" || within(@org.springframework.web.bind.annotation.RestController *)"
|
||||
)
|
||||
public void springBeanPointcut() {
|
||||
// Method is empty as this is just a Pointcut, the implementations are in the advices.
|
||||
}
|
||||
|
||||
/**
|
||||
* Pointcut that matches all Spring beans in the application's main packages.
|
||||
*/
|
||||
@Pointcut(
|
||||
"within(it.sw.pa.comune.artegna.repository..*)" +
|
||||
" || within(it.sw.pa.comune.artegna.service..*)" +
|
||||
" || within(it.sw.pa.comune.artegna.web.rest..*)"
|
||||
)
|
||||
public void applicationPackagePointcut() {
|
||||
// Method is empty as this is just a Pointcut, the implementations are in the advices.
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the {@link Logger} associated to the given {@link JoinPoint}.
|
||||
*
|
||||
* @param joinPoint join point we want the logger for.
|
||||
* @return {@link Logger} associated to the given {@link JoinPoint}.
|
||||
*/
|
||||
private Logger logger(JoinPoint joinPoint) {
|
||||
return LoggerFactory.getLogger(joinPoint.getSignature().getDeclaringTypeName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Advice that logs methods throwing exceptions.
|
||||
*
|
||||
* @param joinPoint join point for advice.
|
||||
* @param e exception.
|
||||
*/
|
||||
@AfterThrowing(pointcut = "applicationPackagePointcut() && springBeanPointcut()", throwing = "e")
|
||||
public void logAfterThrowing(JoinPoint joinPoint, Throwable e) {
|
||||
if (env.acceptsProfiles(Profiles.of(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT))) {
|
||||
logger(joinPoint).error(
|
||||
"Exception in {}() with cause = '{}' and exception = '{}'",
|
||||
joinPoint.getSignature().getName(),
|
||||
e.getCause() != null ? e.getCause() : "NULL",
|
||||
e.getMessage(),
|
||||
e
|
||||
);
|
||||
} else {
|
||||
logger(joinPoint).error(
|
||||
"Exception in {}() with cause = {}",
|
||||
joinPoint.getSignature().getName(),
|
||||
e.getCause() != null ? String.valueOf(e.getCause()) : "NULL"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Advice that logs when a method is entered and exited.
|
||||
*
|
||||
* @param joinPoint join point for advice.
|
||||
* @return result.
|
||||
* @throws Throwable throws {@link IllegalArgumentException}.
|
||||
*/
|
||||
@Around("applicationPackagePointcut() && springBeanPointcut()")
|
||||
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
var log = logger(joinPoint);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Enter: {}() with argument[s] = {}", joinPoint.getSignature().getName(), Arrays.toString(joinPoint.getArgs()));
|
||||
}
|
||||
try {
|
||||
Object result = joinPoint.proceed();
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Exit: {}() with result = {}", joinPoint.getSignature().getName(), result);
|
||||
}
|
||||
return result;
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Illegal argument: {} in {}()", Arrays.toString(joinPoint.getArgs()), joinPoint.getSignature().getName());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Logging aspect.
|
||||
*/
|
||||
package it.sw.pa.comune.artegna.aop.logging;
|
||||
@@ -0,0 +1,38 @@
|
||||
package it.sw.pa.comune.artegna.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Properties specific to Smartbooking.
|
||||
* <p>
|
||||
* Properties are configured in the {@code application.yml} file.
|
||||
* See {@link tech.jhipster.config.JHipsterProperties} for a good example.
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "application", ignoreUnknownFields = false)
|
||||
public class ApplicationProperties {
|
||||
|
||||
private final Liquibase liquibase = new Liquibase();
|
||||
|
||||
// jhipster-needle-application-properties-property
|
||||
|
||||
public Liquibase getLiquibase() {
|
||||
return liquibase;
|
||||
}
|
||||
|
||||
// jhipster-needle-application-properties-property-getter
|
||||
|
||||
public static class Liquibase {
|
||||
|
||||
private Boolean asyncStart = true;
|
||||
|
||||
public Boolean getAsyncStart() {
|
||||
return asyncStart;
|
||||
}
|
||||
|
||||
public void setAsyncStart(Boolean asyncStart) {
|
||||
this.asyncStart = asyncStart;
|
||||
}
|
||||
}
|
||||
|
||||
// jhipster-needle-application-properties-property-class
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package it.sw.pa.comune.artegna.config;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
|
||||
import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler;
|
||||
import org.springframework.boot.autoconfigure.task.TaskExecutionProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.scheduling.annotation.AsyncConfigurer;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import tech.jhipster.async.ExceptionHandlingAsyncTaskExecutor;
|
||||
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
@Profile("!testdev & !testprod")
|
||||
public class AsyncConfiguration implements AsyncConfigurer {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AsyncConfiguration.class);
|
||||
|
||||
private final TaskExecutionProperties taskExecutionProperties;
|
||||
|
||||
public AsyncConfiguration(TaskExecutionProperties taskExecutionProperties) {
|
||||
this.taskExecutionProperties = taskExecutionProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Bean(name = "taskExecutor")
|
||||
public Executor getAsyncExecutor() {
|
||||
LOG.debug("Creating Async Task Executor");
|
||||
var executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(taskExecutionProperties.getPool().getCoreSize());
|
||||
executor.setMaxPoolSize(taskExecutionProperties.getPool().getMaxSize());
|
||||
executor.setQueueCapacity(taskExecutionProperties.getPool().getQueueCapacity());
|
||||
executor.setThreadNamePrefix(taskExecutionProperties.getThreadNamePrefix());
|
||||
return new ExceptionHandlingAsyncTaskExecutor(executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
|
||||
return new SimpleAsyncUncaughtExceptionHandler();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package it.sw.pa.comune.artegna.config;
|
||||
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.core.pattern.CompositeConverter;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Marker;
|
||||
import org.slf4j.MarkerFactory;
|
||||
import org.springframework.boot.ansi.AnsiColor;
|
||||
import org.springframework.boot.ansi.AnsiElement;
|
||||
import org.springframework.boot.ansi.AnsiOutput;
|
||||
import org.springframework.boot.ansi.AnsiStyle;
|
||||
|
||||
/**
|
||||
* Log filter to prevent attackers from forging log entries by submitting input containing CRLF characters.
|
||||
* CRLF characters are replaced with a red colored _ character.
|
||||
*
|
||||
* @see <a href="https://owasp.org/www-community/attacks/Log_Injection">Log Forging Description</a>
|
||||
* @see <a href="https://github.com/jhipster/generator-jhipster/issues/14949">JHipster issue</a>
|
||||
*/
|
||||
public class CRLFLogConverter extends CompositeConverter<ILoggingEvent> {
|
||||
|
||||
public static final Marker CRLF_SAFE_MARKER = MarkerFactory.getMarker("CRLF_SAFE");
|
||||
|
||||
private static final String[] SAFE_LOGS = {
|
||||
"org.hibernate",
|
||||
"org.springframework.boot.autoconfigure",
|
||||
"org.springframework.boot.diagnostics",
|
||||
};
|
||||
private static final Map<String, AnsiElement> ELEMENTS;
|
||||
|
||||
static {
|
||||
Map<String, AnsiElement> ansiElements = new HashMap<>();
|
||||
ansiElements.put("faint", AnsiStyle.FAINT);
|
||||
ansiElements.put("red", AnsiColor.RED);
|
||||
ansiElements.put("green", AnsiColor.GREEN);
|
||||
ansiElements.put("yellow", AnsiColor.YELLOW);
|
||||
ansiElements.put("blue", AnsiColor.BLUE);
|
||||
ansiElements.put("magenta", AnsiColor.MAGENTA);
|
||||
ansiElements.put("cyan", AnsiColor.CYAN);
|
||||
ELEMENTS = Collections.unmodifiableMap(ansiElements);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String transform(ILoggingEvent event, String in) {
|
||||
AnsiElement element = ELEMENTS.get(getFirstOption());
|
||||
List<Marker> markers = event.getMarkerList();
|
||||
if ((markers != null && !markers.isEmpty() && markers.get(0).contains(CRLF_SAFE_MARKER)) || isLoggerSafe(event)) {
|
||||
return in;
|
||||
}
|
||||
String replacement = element == null ? "_" : toAnsiString("_", element);
|
||||
return in.replaceAll("[\n\r\t]", replacement);
|
||||
}
|
||||
|
||||
protected boolean isLoggerSafe(ILoggingEvent event) {
|
||||
for (String safeLogger : SAFE_LOGS) {
|
||||
if (event.getLoggerName().startsWith(safeLogger)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected String toAnsiString(String in, AnsiElement element) {
|
||||
return AnsiOutput.toString(element, in);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package it.sw.pa.comune.artegna.config;
|
||||
|
||||
import java.time.Duration;
|
||||
import org.ehcache.config.builders.*;
|
||||
import org.ehcache.jsr107.Eh107Configuration;
|
||||
import org.hibernate.cache.jcache.ConfigSettings;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer;
|
||||
import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer;
|
||||
import org.springframework.boot.info.BuildProperties;
|
||||
import org.springframework.boot.info.GitProperties;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.cache.interceptor.KeyGenerator;
|
||||
import org.springframework.context.annotation.*;
|
||||
import tech.jhipster.config.JHipsterProperties;
|
||||
import tech.jhipster.config.cache.PrefixedKeyGenerator;
|
||||
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class CacheConfiguration {
|
||||
|
||||
private GitProperties gitProperties;
|
||||
private BuildProperties buildProperties;
|
||||
private final javax.cache.configuration.Configuration<Object, Object> jcacheConfiguration;
|
||||
|
||||
public CacheConfiguration(JHipsterProperties jHipsterProperties) {
|
||||
var ehcache = jHipsterProperties.getCache().getEhcache();
|
||||
|
||||
jcacheConfiguration = Eh107Configuration.fromEhcacheCacheConfiguration(
|
||||
CacheConfigurationBuilder.newCacheConfigurationBuilder(
|
||||
Object.class,
|
||||
Object.class,
|
||||
ResourcePoolsBuilder.heap(ehcache.getMaxEntries())
|
||||
)
|
||||
.withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(ehcache.getTimeToLiveSeconds())))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HibernatePropertiesCustomizer hibernatePropertiesCustomizer(javax.cache.CacheManager cacheManager) {
|
||||
return hibernateProperties -> hibernateProperties.put(ConfigSettings.CACHE_MANAGER, cacheManager);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JCacheManagerCustomizer cacheManagerCustomizer() {
|
||||
return cm -> {
|
||||
createCache(cm, it.sw.pa.comune.artegna.repository.UserRepository.USERS_BY_LOGIN_CACHE);
|
||||
createCache(cm, it.sw.pa.comune.artegna.repository.UserRepository.USERS_BY_EMAIL_CACHE);
|
||||
createCache(cm, it.sw.pa.comune.artegna.domain.User.class.getName());
|
||||
createCache(cm, it.sw.pa.comune.artegna.domain.Authority.class.getName());
|
||||
createCache(cm, it.sw.pa.comune.artegna.domain.User.class.getName() + ".authorities");
|
||||
createCache(cm, it.sw.pa.comune.artegna.domain.PersistentToken.class.getName());
|
||||
createCache(cm, it.sw.pa.comune.artegna.domain.User.class.getName() + ".persistentTokens");
|
||||
// jhipster-needle-ehcache-add-entry
|
||||
};
|
||||
}
|
||||
|
||||
private void createCache(javax.cache.CacheManager cm, String cacheName) {
|
||||
javax.cache.Cache<Object, Object> cache = cm.getCache(cacheName);
|
||||
if (cache != null) {
|
||||
cache.clear();
|
||||
} else {
|
||||
cm.createCache(cacheName, jcacheConfiguration);
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setGitProperties(GitProperties gitProperties) {
|
||||
this.gitProperties = gitProperties;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setBuildProperties(BuildProperties buildProperties) {
|
||||
this.buildProperties = buildProperties;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public KeyGenerator keyGenerator() {
|
||||
return new PrefixedKeyGenerator(this.gitProperties, this.buildProperties);
|
||||
}
|
||||
}
|
||||
15
src/main/java/it/sw/pa/comune/artegna/config/Constants.java
Normal file
15
src/main/java/it/sw/pa/comune/artegna/config/Constants.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package it.sw.pa.comune.artegna.config;
|
||||
|
||||
/**
|
||||
* Application constants.
|
||||
*/
|
||||
public final class Constants {
|
||||
|
||||
// Regex for acceptable logins
|
||||
public static final String LOGIN_REGEX = "^(?>[a-zA-Z0-9!$&*+=?^_`{|}~.-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*)|(?>[_.@A-Za-z0-9-]+)$";
|
||||
|
||||
public static final String SYSTEM = "system";
|
||||
public static final String DEFAULT_LANGUAGE = "it";
|
||||
|
||||
private Constants() {}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package it.sw.pa.comune.artegna.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
@Configuration
|
||||
@EnableJpaRepositories({ "it.sw.pa.comune.artegna.repository" })
|
||||
@EnableJpaAuditing(auditorAwareRef = "springSecurityAuditorAware")
|
||||
@EnableTransactionManagement
|
||||
public class DatabaseConfiguration {}
|
||||
@@ -0,0 +1,20 @@
|
||||
package it.sw.pa.comune.artegna.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.format.FormatterRegistry;
|
||||
import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Configure the converters to use the ISO format for dates by default.
|
||||
*/
|
||||
@Configuration
|
||||
public class DateTimeFormatConfiguration implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addFormatters(FormatterRegistry registry) {
|
||||
var registrar = new DateTimeFormatterRegistrar();
|
||||
registrar.setUseIsoFormat(true);
|
||||
registrar.registerFormatters(registry);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package it.sw.pa.comune.artegna.config;
|
||||
|
||||
import com.fasterxml.jackson.datatype.hibernate6.Hibernate6Module;
|
||||
import com.fasterxml.jackson.datatype.hibernate6.Hibernate6Module.Feature;
|
||||
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class JacksonConfiguration {
|
||||
|
||||
/**
|
||||
* Support for Java date and time API.
|
||||
* @return the corresponding Jackson module.
|
||||
*/
|
||||
@Bean
|
||||
public JavaTimeModule javaTimeModule() {
|
||||
return new JavaTimeModule();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Jdk8Module jdk8TimeModule() {
|
||||
return new Jdk8Module();
|
||||
}
|
||||
|
||||
/*
|
||||
* Support for Hibernate types in Jackson.
|
||||
*/
|
||||
@Bean
|
||||
public Hibernate6Module hibernate6Module() {
|
||||
return new Hibernate6Module().configure(Feature.SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package it.sw.pa.comune.artegna.config;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import javax.sql.DataSource;
|
||||
import liquibase.integration.spring.SpringLiquibase;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
|
||||
import org.springframework.boot.autoconfigure.liquibase.LiquibaseDataSource;
|
||||
import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import tech.jhipster.config.JHipsterConstants;
|
||||
import tech.jhipster.config.liquibase.SpringLiquibaseUtil;
|
||||
|
||||
@Configuration
|
||||
public class LiquibaseConfiguration {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(LiquibaseConfiguration.class);
|
||||
|
||||
private final Environment env;
|
||||
|
||||
public LiquibaseConfiguration(Environment env) {
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SpringLiquibase liquibase(
|
||||
@Qualifier("taskExecutor") Executor executor,
|
||||
LiquibaseProperties liquibaseProperties,
|
||||
@LiquibaseDataSource ObjectProvider<DataSource> liquibaseDataSource,
|
||||
ObjectProvider<DataSource> dataSource,
|
||||
ApplicationProperties applicationProperties,
|
||||
DataSourceProperties dataSourceProperties
|
||||
) {
|
||||
SpringLiquibase liquibase;
|
||||
if (Boolean.TRUE.equals(applicationProperties.getLiquibase().getAsyncStart())) {
|
||||
liquibase = SpringLiquibaseUtil.createAsyncSpringLiquibase(
|
||||
this.env,
|
||||
executor,
|
||||
liquibaseDataSource.getIfAvailable(),
|
||||
liquibaseProperties,
|
||||
dataSource.getIfUnique(),
|
||||
dataSourceProperties
|
||||
);
|
||||
} else {
|
||||
liquibase = SpringLiquibaseUtil.createSpringLiquibase(
|
||||
liquibaseDataSource.getIfAvailable(),
|
||||
liquibaseProperties,
|
||||
dataSource.getIfUnique(),
|
||||
dataSourceProperties
|
||||
);
|
||||
}
|
||||
liquibase.setChangeLog("classpath:config/liquibase/master.xml");
|
||||
if (!CollectionUtils.isEmpty(liquibaseProperties.getContexts())) {
|
||||
liquibase.setContexts(StringUtils.collectionToCommaDelimitedString(liquibaseProperties.getContexts()));
|
||||
}
|
||||
liquibase.setDefaultSchema(liquibaseProperties.getDefaultSchema());
|
||||
liquibase.setLiquibaseSchema(liquibaseProperties.getLiquibaseSchema());
|
||||
liquibase.setLiquibaseTablespace(liquibaseProperties.getLiquibaseTablespace());
|
||||
liquibase.setDatabaseChangeLogLockTable(liquibaseProperties.getDatabaseChangeLogLockTable());
|
||||
liquibase.setDatabaseChangeLogTable(liquibaseProperties.getDatabaseChangeLogTable());
|
||||
liquibase.setDropFirst(liquibaseProperties.isDropFirst());
|
||||
if (!CollectionUtils.isEmpty(liquibaseProperties.getLabelFilter())) {
|
||||
liquibase.setLabelFilter(StringUtils.collectionToCommaDelimitedString(liquibaseProperties.getLabelFilter()));
|
||||
}
|
||||
liquibase.setChangeLogParameters(liquibaseProperties.getParameters());
|
||||
liquibase.setRollbackFile(liquibaseProperties.getRollbackFile());
|
||||
liquibase.setTestRollbackOnUpdate(liquibaseProperties.isTestRollbackOnUpdate());
|
||||
if (env.matchesProfiles(JHipsterConstants.SPRING_PROFILE_NO_LIQUIBASE)) {
|
||||
liquibase.setShouldRun(false);
|
||||
} else {
|
||||
liquibase.setShouldRun(liquibaseProperties.isEnabled());
|
||||
LOG.debug("Configuring Liquibase");
|
||||
}
|
||||
return liquibase;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package it.sw.pa.comune.artegna.config;
|
||||
|
||||
import it.sw.pa.comune.artegna.aop.logging.LoggingAspect;
|
||||
import org.springframework.context.annotation.*;
|
||||
import org.springframework.core.env.Environment;
|
||||
import tech.jhipster.config.JHipsterConstants;
|
||||
|
||||
@Configuration
|
||||
@EnableAspectJAutoProxy
|
||||
public class LoggingAspectConfiguration {
|
||||
|
||||
@Bean
|
||||
@Profile(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT)
|
||||
public LoggingAspect loggingAspect(Environment env) {
|
||||
return new LoggingAspect(env);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package it.sw.pa.comune.artegna.config;
|
||||
|
||||
import static tech.jhipster.config.logging.LoggingUtils.*;
|
||||
|
||||
import ch.qos.logback.classic.LoggerContext;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import tech.jhipster.config.JHipsterProperties;
|
||||
|
||||
/*
|
||||
* Configures the console and Logstash log appenders from the app properties
|
||||
*/
|
||||
@Configuration
|
||||
public class LoggingConfiguration {
|
||||
|
||||
public LoggingConfiguration(
|
||||
@Value("${spring.application.name}") String appName,
|
||||
@Value("${server.port}") String serverPort,
|
||||
JHipsterProperties jHipsterProperties,
|
||||
ObjectMapper mapper
|
||||
) throws JsonProcessingException {
|
||||
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
|
||||
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("app_name", appName);
|
||||
map.put("app_port", serverPort);
|
||||
var customFields = mapper.writeValueAsString(map);
|
||||
|
||||
var loggingProperties = jHipsterProperties.getLogging();
|
||||
var logstashProperties = loggingProperties.getLogstash();
|
||||
|
||||
if (loggingProperties.isUseJsonFormat()) {
|
||||
addJsonConsoleAppender(context, customFields);
|
||||
}
|
||||
if (logstashProperties.isEnabled()) {
|
||||
addLogstashTcpSocketAppender(context, customFields, logstashProperties);
|
||||
}
|
||||
if (loggingProperties.isUseJsonFormat() || logstashProperties.isEnabled()) {
|
||||
addContextListener(context, customFields, loggingProperties);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package it.sw.pa.comune.artegna.config;
|
||||
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
|
||||
import it.sw.pa.comune.artegna.security.*;
|
||||
import it.sw.pa.comune.artegna.web.filter.SpaWebFilter;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.util.function.Supplier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
|
||||
import org.springframework.security.web.authentication.RememberMeServices;
|
||||
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
||||
import org.springframework.security.web.csrf.*;
|
||||
import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.StringUtils;
|
||||
import tech.jhipster.config.JHipsterProperties;
|
||||
|
||||
@Configuration
|
||||
@EnableMethodSecurity(securedEnabled = true)
|
||||
public class SecurityConfiguration {
|
||||
|
||||
private final JHipsterProperties jHipsterProperties;
|
||||
|
||||
private final RememberMeServices rememberMeServices;
|
||||
|
||||
public SecurityConfiguration(RememberMeServices rememberMeServices, JHipsterProperties jHipsterProperties) {
|
||||
this.rememberMeServices = rememberMeServices;
|
||||
this.jHipsterProperties = jHipsterProperties;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.cors(withDefaults())
|
||||
.csrf(csrf ->
|
||||
csrf
|
||||
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
|
||||
.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())
|
||||
)
|
||||
.addFilterAfter(new SpaWebFilter(), BasicAuthenticationFilter.class)
|
||||
.headers(headers ->
|
||||
headers
|
||||
.contentSecurityPolicy(csp -> csp.policyDirectives(jHipsterProperties.getSecurity().getContentSecurityPolicy()))
|
||||
.frameOptions(FrameOptionsConfig::sameOrigin)
|
||||
.referrerPolicy(referrer -> referrer.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN))
|
||||
.permissionsPolicyHeader(permissions ->
|
||||
permissions.policy(
|
||||
"camera=(), fullscreen=(self), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), sync-xhr=()"
|
||||
)
|
||||
)
|
||||
)
|
||||
.authorizeHttpRequests(authz ->
|
||||
// prettier-ignore
|
||||
authz
|
||||
.requestMatchers("/index.html", "/*.js", "/*.txt", "/*.json", "/*.map", "/*.css").permitAll()
|
||||
.requestMatchers("/*.ico", "/*.png", "/*.svg", "/*.webapp").permitAll()
|
||||
.requestMatchers("/assets/**").permitAll()
|
||||
.requestMatchers("/swagger-ui/**").permitAll()
|
||||
.requestMatchers("/api/authenticate").permitAll()
|
||||
.requestMatchers("/api/register").permitAll()
|
||||
.requestMatchers("/api/activate").permitAll()
|
||||
.requestMatchers("/api/account/reset-password/init").permitAll()
|
||||
.requestMatchers("/api/account/reset-password/finish").permitAll()
|
||||
.requestMatchers("/api/admin/**").hasAuthority(AuthoritiesConstants.ADMIN)
|
||||
.requestMatchers("/api/**").authenticated()
|
||||
.requestMatchers("/v3/api-docs/**").hasAuthority(AuthoritiesConstants.ADMIN)
|
||||
.requestMatchers("/management/health").permitAll()
|
||||
.requestMatchers("/management/health/**").permitAll()
|
||||
.requestMatchers("/management/info").permitAll()
|
||||
.requestMatchers("/management/prometheus").permitAll()
|
||||
.requestMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
|
||||
)
|
||||
.rememberMe(rememberMe ->
|
||||
rememberMe
|
||||
.rememberMeServices(rememberMeServices)
|
||||
.rememberMeParameter("remember-me")
|
||||
.key(jHipsterProperties.getSecurity().getRememberMe().getKey())
|
||||
)
|
||||
.exceptionHandling(exceptionHanding -> {
|
||||
AntPathMatcher pathMatcher = new AntPathMatcher();
|
||||
RequestMatcher apiRequestMatcher = request -> pathMatcher.match("/api/**", request.getRequestURI());
|
||||
exceptionHanding.defaultAuthenticationEntryPointFor(
|
||||
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
|
||||
new OrRequestMatcher(apiRequestMatcher)
|
||||
);
|
||||
})
|
||||
.formLogin(formLogin ->
|
||||
formLogin
|
||||
.loginPage("/")
|
||||
.loginProcessingUrl("/api/authentication")
|
||||
.successHandler((request, response, authentication) -> response.setStatus(HttpStatus.OK.value()))
|
||||
.failureHandler((request, response, exception) -> response.setStatus(HttpStatus.UNAUTHORIZED.value()))
|
||||
.permitAll()
|
||||
)
|
||||
.logout(logout ->
|
||||
logout.logoutUrl("/api/logout").logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()).permitAll()
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom CSRF handler to provide BREACH protection for Single-Page Applications (SPA).
|
||||
*
|
||||
* @see <a href="https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa">Spring Security Documentation - Integrating with CSRF Protection</a>
|
||||
* @see <a href="https://github.com/jhipster/generator-jhipster/pull/25907">JHipster - use customized SpaCsrfTokenRequestHandler to handle CSRF token</a>
|
||||
* @see <a href="https://stackoverflow.com/q/74447118/65681">CSRF protection not working with Spring Security 6</a>
|
||||
*/
|
||||
static final class SpaCsrfTokenRequestHandler implements CsrfTokenRequestHandler {
|
||||
|
||||
private final CsrfTokenRequestHandler plain = new CsrfTokenRequestAttributeHandler();
|
||||
private final CsrfTokenRequestHandler xor = new XorCsrfTokenRequestAttributeHandler();
|
||||
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
|
||||
/*
|
||||
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
|
||||
* the CsrfToken when it is rendered in the response body.
|
||||
*/
|
||||
this.xor.handle(request, response, csrfToken);
|
||||
|
||||
// Render the token value to a cookie by causing the deferred token to be loaded.
|
||||
csrfToken.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
|
||||
/*
|
||||
* If the request contains a request header, use CsrfTokenRequestAttributeHandler
|
||||
* to resolve the CsrfToken. This applies when a single-page application includes
|
||||
* the header value automatically, which was obtained via a cookie containing the
|
||||
* raw CsrfToken.
|
||||
*/
|
||||
if (StringUtils.hasText(request.getHeader(csrfToken.getHeaderName()))) {
|
||||
return this.plain.resolveCsrfTokenValue(request, csrfToken);
|
||||
}
|
||||
/*
|
||||
* In all other cases (e.g. if the request contains a request parameter), use
|
||||
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
|
||||
* when a server-side rendered form includes the _csrf request parameter as a
|
||||
* hidden input.
|
||||
*/
|
||||
return this.xor.resolveCsrfTokenValue(request, csrfToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package it.sw.pa.comune.artegna.config;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistration;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import tech.jhipster.config.JHipsterConstants;
|
||||
import tech.jhipster.config.JHipsterProperties;
|
||||
|
||||
@Configuration
|
||||
@Profile({ JHipsterConstants.SPRING_PROFILE_PRODUCTION })
|
||||
public class StaticResourcesWebConfiguration implements WebMvcConfigurer {
|
||||
|
||||
protected static final String[] RESOURCE_LOCATIONS = { "classpath:/static/", "classpath:/static/content/", "classpath:/static/i18n/" };
|
||||
protected static final String[] RESOURCE_PATHS = { "/*.js", "/*.css", "/*.svg", "/*.png", "*.ico", "/content/**", "/i18n/*" };
|
||||
|
||||
private final JHipsterProperties jhipsterProperties;
|
||||
|
||||
public StaticResourcesWebConfiguration(JHipsterProperties jHipsterProperties) {
|
||||
this.jhipsterProperties = jHipsterProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
ResourceHandlerRegistration resourceHandlerRegistration = appendResourceHandler(registry);
|
||||
initializeResourceHandler(resourceHandlerRegistration);
|
||||
}
|
||||
|
||||
protected ResourceHandlerRegistration appendResourceHandler(ResourceHandlerRegistry registry) {
|
||||
return registry.addResourceHandler(RESOURCE_PATHS);
|
||||
}
|
||||
|
||||
protected void initializeResourceHandler(ResourceHandlerRegistration resourceHandlerRegistration) {
|
||||
resourceHandlerRegistration.addResourceLocations(RESOURCE_LOCATIONS).setCacheControl(getCacheControl());
|
||||
}
|
||||
|
||||
protected CacheControl getCacheControl() {
|
||||
return CacheControl.maxAge(getJHipsterHttpCacheProperty(), TimeUnit.DAYS).cachePublic();
|
||||
}
|
||||
|
||||
private int getJHipsterHttpCacheProperty() {
|
||||
return jhipsterProperties.getHttp().getCache().getTimeToLiveInDays();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package it.sw.pa.comune.artegna.config;
|
||||
|
||||
import static java.net.URLDecoder.decode;
|
||||
|
||||
import jakarta.servlet.*;
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.web.server.*;
|
||||
import org.springframework.boot.web.servlet.ServletContextInitializer;
|
||||
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
import tech.jhipster.config.JHipsterProperties;
|
||||
|
||||
/**
|
||||
* Configuration of web application with Servlet 3.0 APIs.
|
||||
*/
|
||||
@Configuration
|
||||
public class WebConfigurer implements ServletContextInitializer, WebServerFactoryCustomizer<WebServerFactory> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WebConfigurer.class);
|
||||
|
||||
private final Environment env;
|
||||
|
||||
private final JHipsterProperties jHipsterProperties;
|
||||
|
||||
public WebConfigurer(Environment env, JHipsterProperties jHipsterProperties) {
|
||||
this.env = env;
|
||||
this.jHipsterProperties = jHipsterProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartup(ServletContext servletContext) {
|
||||
if (env.getActiveProfiles().length != 0) {
|
||||
LOG.info("Web application configuration, using profiles: {}", (Object[]) env.getActiveProfiles());
|
||||
}
|
||||
|
||||
LOG.info("Web application fully configured");
|
||||
}
|
||||
|
||||
/**
|
||||
* Customize the Servlet engine: Mime types, the document root, the cache.
|
||||
*/
|
||||
@Override
|
||||
public void customize(WebServerFactory server) {
|
||||
// When running in an IDE or with ./gradlew bootRun, set location of the static web assets.
|
||||
setLocationForStaticAssets(server);
|
||||
}
|
||||
|
||||
private void setLocationForStaticAssets(WebServerFactory server) {
|
||||
if (server instanceof ConfigurableServletWebServerFactory servletWebServer) {
|
||||
File root;
|
||||
String prefixPath = resolvePathPrefix();
|
||||
root = Path.of(prefixPath + "build/resources/main/static/").toFile();
|
||||
if (root.exists() && root.isDirectory()) {
|
||||
servletWebServer.setDocumentRoot(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve path prefix to static resources.
|
||||
*/
|
||||
private String resolvePathPrefix() {
|
||||
String fullExecutablePath = decode(this.getClass().getResource("").getPath(), StandardCharsets.UTF_8);
|
||||
String rootPath = Path.of(".").toUri().normalize().getPath();
|
||||
String extractedPath = fullExecutablePath.replace(rootPath, "");
|
||||
int extractionEndIndex = extractedPath.indexOf("build/");
|
||||
if (extractionEndIndex <= 0) {
|
||||
return "";
|
||||
}
|
||||
return extractedPath.substring(0, extractionEndIndex);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsFilter corsFilter() {
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
CorsConfiguration config = jHipsterProperties.getCors();
|
||||
if (!CollectionUtils.isEmpty(config.getAllowedOrigins()) || !CollectionUtils.isEmpty(config.getAllowedOriginPatterns())) {
|
||||
LOG.debug("Registering CORS filter");
|
||||
source.registerCorsConfiguration("/api/**", config);
|
||||
source.registerCorsConfiguration("/management/**", config);
|
||||
source.registerCorsConfiguration("/v3/api-docs", config);
|
||||
source.registerCorsConfiguration("/swagger-ui/**", config);
|
||||
}
|
||||
return new CorsFilter(source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Application configuration.
|
||||
*/
|
||||
package it.sw.pa.comune.artegna.config;
|
||||
@@ -0,0 +1,77 @@
|
||||
package it.sw.pa.comune.artegna.domain;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import org.springframework.data.annotation.CreatedBy;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedBy;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
/**
|
||||
* Base abstract class for entities which will hold definitions for created, last modified, created by,
|
||||
* last modified by attributes.
|
||||
*/
|
||||
@MappedSuperclass
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
@JsonIgnoreProperties(value = { "createdBy", "createdDate", "lastModifiedBy", "lastModifiedDate" }, allowGetters = true)
|
||||
public abstract class AbstractAuditingEntity<T> implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public abstract T getId();
|
||||
|
||||
@CreatedBy
|
||||
@Column(name = "created_by", nullable = false, length = 50, updatable = false)
|
||||
private String createdBy;
|
||||
|
||||
@CreatedDate
|
||||
@Column(name = "created_date", updatable = false)
|
||||
private Instant createdDate = Instant.now();
|
||||
|
||||
@LastModifiedBy
|
||||
@Column(name = "last_modified_by", length = 50)
|
||||
private String lastModifiedBy;
|
||||
|
||||
@LastModifiedDate
|
||||
@Column(name = "last_modified_date")
|
||||
private Instant lastModifiedDate = Instant.now();
|
||||
|
||||
public String getCreatedBy() {
|
||||
return createdBy;
|
||||
}
|
||||
|
||||
public void setCreatedBy(String createdBy) {
|
||||
this.createdBy = createdBy;
|
||||
}
|
||||
|
||||
public Instant getCreatedDate() {
|
||||
return createdDate;
|
||||
}
|
||||
|
||||
public void setCreatedDate(Instant createdDate) {
|
||||
this.createdDate = createdDate;
|
||||
}
|
||||
|
||||
public String getLastModifiedBy() {
|
||||
return lastModifiedBy;
|
||||
}
|
||||
|
||||
public void setLastModifiedBy(String lastModifiedBy) {
|
||||
this.lastModifiedBy = lastModifiedBy;
|
||||
}
|
||||
|
||||
public Instant getLastModifiedDate() {
|
||||
return lastModifiedDate;
|
||||
}
|
||||
|
||||
public void setLastModifiedDate(Instant lastModifiedDate) {
|
||||
this.lastModifiedDate = lastModifiedDate;
|
||||
}
|
||||
}
|
||||
99
src/main/java/it/sw/pa/comune/artegna/domain/Authority.java
Normal file
99
src/main/java/it/sw/pa/comune/artegna/domain/Authority.java
Normal file
@@ -0,0 +1,99 @@
|
||||
package it.sw.pa.comune.artegna.domain;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
import org.hibernate.annotations.Cache;
|
||||
import org.hibernate.annotations.CacheConcurrencyStrategy;
|
||||
import org.springframework.data.domain.Persistable;
|
||||
|
||||
/**
|
||||
* A Authority.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "jhi_authority")
|
||||
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
|
||||
@JsonIgnoreProperties(value = { "new", "id" })
|
||||
@SuppressWarnings("common-java:DuplicatedBlocks")
|
||||
public class Authority implements Serializable, Persistable<String> {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@NotNull
|
||||
@Size(max = 50)
|
||||
@Id
|
||||
@Column(name = "name", length = 50, nullable = false)
|
||||
private String name;
|
||||
|
||||
@org.springframework.data.annotation.Transient
|
||||
@Transient
|
||||
private boolean isPersisted;
|
||||
|
||||
// jhipster-needle-entity-add-field - JHipster will add fields here
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public Authority name(String name) {
|
||||
this.setName(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@PostLoad
|
||||
@PostPersist
|
||||
public void updateEntityState() {
|
||||
this.setIsPersisted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@org.springframework.data.annotation.Transient
|
||||
@Transient
|
||||
@Override
|
||||
public boolean isNew() {
|
||||
return !this.isPersisted;
|
||||
}
|
||||
|
||||
public Authority setIsPersisted() {
|
||||
this.isPersisted = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
// jhipster-needle-entity-add-getters-setters - JHipster will add getters and setters here
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof Authority)) {
|
||||
return false;
|
||||
}
|
||||
return getName() != null && getName().equals(((Authority) o).getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(getName());
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Authority{" +
|
||||
"name=" + getName() +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package it.sw.pa.comune.artegna.domain;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Objects;
|
||||
import org.hibernate.annotations.Cache;
|
||||
import org.hibernate.annotations.CacheConcurrencyStrategy;
|
||||
|
||||
/**
|
||||
* Persistent tokens are used by Spring Security to automatically log in users.
|
||||
*
|
||||
* @see it.sw.pa.comune.artegna.security.PersistentTokenRememberMeServices
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "jhi_persistent_token")
|
||||
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
|
||||
public class PersistentToken implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private static final int MAX_USER_AGENT_LEN = 255;
|
||||
|
||||
@Id
|
||||
private String series;
|
||||
|
||||
@JsonIgnore
|
||||
@NotNull
|
||||
@Column(name = "token_value", nullable = false)
|
||||
private String tokenValue;
|
||||
|
||||
@Column(name = "token_date")
|
||||
private LocalDate tokenDate;
|
||||
|
||||
//an IPV6 address max length is 39 characters
|
||||
@Size(min = 0, max = 39)
|
||||
@Column(name = "ip_address", length = 39)
|
||||
private String ipAddress;
|
||||
|
||||
@Column(name = "user_agent")
|
||||
private String userAgent;
|
||||
|
||||
@JsonIgnore
|
||||
@ManyToOne
|
||||
private User user;
|
||||
|
||||
public String getSeries() {
|
||||
return series;
|
||||
}
|
||||
|
||||
public void setSeries(String series) {
|
||||
this.series = series;
|
||||
}
|
||||
|
||||
public String getTokenValue() {
|
||||
return tokenValue;
|
||||
}
|
||||
|
||||
public void setTokenValue(String tokenValue) {
|
||||
this.tokenValue = tokenValue;
|
||||
}
|
||||
|
||||
public LocalDate getTokenDate() {
|
||||
return tokenDate;
|
||||
}
|
||||
|
||||
public void setTokenDate(LocalDate tokenDate) {
|
||||
this.tokenDate = tokenDate;
|
||||
}
|
||||
|
||||
public String getIpAddress() {
|
||||
return ipAddress;
|
||||
}
|
||||
|
||||
public void setIpAddress(String ipAddress) {
|
||||
this.ipAddress = ipAddress;
|
||||
}
|
||||
|
||||
public String getUserAgent() {
|
||||
return userAgent;
|
||||
}
|
||||
|
||||
public void setUserAgent(String userAgent) {
|
||||
if (userAgent.length() >= MAX_USER_AGENT_LEN) {
|
||||
this.userAgent = userAgent.substring(0, MAX_USER_AGENT_LEN - 1);
|
||||
} else {
|
||||
this.userAgent = userAgent;
|
||||
}
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof PersistentToken)) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(series, ((PersistentToken) o).series);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(series);
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PersistentToken{" +
|
||||
"series='" + series + '\'' +
|
||||
", tokenValue='" + tokenValue + '\'' +
|
||||
", tokenDate=" + tokenDate +
|
||||
", ipAddress='" + ipAddress + '\'' +
|
||||
", userAgent='" + userAgent + '\'' +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
247
src/main/java/it/sw/pa/comune/artegna/domain/User.java
Normal file
247
src/main/java/it/sw/pa/comune/artegna/domain/User.java
Normal file
@@ -0,0 +1,247 @@
|
||||
package it.sw.pa.comune.artegna.domain;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import it.sw.pa.comune.artegna.config.Constants;
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hibernate.annotations.BatchSize;
|
||||
import org.hibernate.annotations.Cache;
|
||||
import org.hibernate.annotations.CacheConcurrencyStrategy;
|
||||
|
||||
/**
|
||||
* A user.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "jhi_user")
|
||||
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
|
||||
public class User extends AbstractAuditingEntity<Long> implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||
@SequenceGenerator(name = "sequenceGenerator")
|
||||
private Long id;
|
||||
|
||||
@NotNull
|
||||
@Pattern(regexp = Constants.LOGIN_REGEX)
|
||||
@Size(min = 1, max = 50)
|
||||
@Column(length = 50, unique = true, nullable = false)
|
||||
private String login;
|
||||
|
||||
@JsonIgnore
|
||||
@NotNull
|
||||
@Size(min = 60, max = 60)
|
||||
@Column(name = "password_hash", length = 60, nullable = false)
|
||||
private String password;
|
||||
|
||||
@Size(max = 50)
|
||||
@Column(name = "first_name", length = 50)
|
||||
private String firstName;
|
||||
|
||||
@Size(max = 50)
|
||||
@Column(name = "last_name", length = 50)
|
||||
private String lastName;
|
||||
|
||||
@Email
|
||||
@Size(min = 5, max = 254)
|
||||
@Column(length = 254, unique = true)
|
||||
private String email;
|
||||
|
||||
@NotNull
|
||||
@Column(nullable = false)
|
||||
private boolean activated = false;
|
||||
|
||||
@Size(min = 2, max = 10)
|
||||
@Column(name = "lang_key", length = 10)
|
||||
private String langKey;
|
||||
|
||||
@Size(max = 256)
|
||||
@Column(name = "image_url", length = 256)
|
||||
private String imageUrl;
|
||||
|
||||
@Size(max = 20)
|
||||
@Column(name = "activation_key", length = 20)
|
||||
@JsonIgnore
|
||||
private String activationKey;
|
||||
|
||||
@Size(max = 20)
|
||||
@Column(name = "reset_key", length = 20)
|
||||
@JsonIgnore
|
||||
private String resetKey;
|
||||
|
||||
@Column(name = "reset_date")
|
||||
private Instant resetDate = null;
|
||||
|
||||
@JsonIgnore
|
||||
@ManyToMany
|
||||
@JoinTable(
|
||||
name = "jhi_user_authority",
|
||||
joinColumns = { @JoinColumn(name = "user_id", referencedColumnName = "id") },
|
||||
inverseJoinColumns = { @JoinColumn(name = "authority_name", referencedColumnName = "name") }
|
||||
)
|
||||
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
|
||||
@BatchSize(size = 20)
|
||||
private Set<Authority> authorities = new HashSet<>();
|
||||
|
||||
@JsonIgnore
|
||||
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "user")
|
||||
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
|
||||
private Set<PersistentToken> persistentTokens = new HashSet<>();
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getLogin() {
|
||||
return login;
|
||||
}
|
||||
|
||||
// Lowercase the login before saving it in database
|
||||
public void setLogin(String login) {
|
||||
this.login = StringUtils.lowerCase(login, Locale.ENGLISH);
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getImageUrl() {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
public void setImageUrl(String imageUrl) {
|
||||
this.imageUrl = imageUrl;
|
||||
}
|
||||
|
||||
public boolean isActivated() {
|
||||
return activated;
|
||||
}
|
||||
|
||||
public void setActivated(boolean activated) {
|
||||
this.activated = activated;
|
||||
}
|
||||
|
||||
public String getActivationKey() {
|
||||
return activationKey;
|
||||
}
|
||||
|
||||
public void setActivationKey(String activationKey) {
|
||||
this.activationKey = activationKey;
|
||||
}
|
||||
|
||||
public String getResetKey() {
|
||||
return resetKey;
|
||||
}
|
||||
|
||||
public void setResetKey(String resetKey) {
|
||||
this.resetKey = resetKey;
|
||||
}
|
||||
|
||||
public Instant getResetDate() {
|
||||
return resetDate;
|
||||
}
|
||||
|
||||
public void setResetDate(Instant resetDate) {
|
||||
this.resetDate = resetDate;
|
||||
}
|
||||
|
||||
public String getLangKey() {
|
||||
return langKey;
|
||||
}
|
||||
|
||||
public void setLangKey(String langKey) {
|
||||
this.langKey = langKey;
|
||||
}
|
||||
|
||||
public Set<Authority> getAuthorities() {
|
||||
return authorities;
|
||||
}
|
||||
|
||||
public void setAuthorities(Set<Authority> authorities) {
|
||||
this.authorities = authorities;
|
||||
}
|
||||
|
||||
public Set<PersistentToken> getPersistentTokens() {
|
||||
return persistentTokens;
|
||||
}
|
||||
|
||||
public void setPersistentTokens(Set<PersistentToken> persistentTokens) {
|
||||
this.persistentTokens = persistentTokens;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof User)) {
|
||||
return false;
|
||||
}
|
||||
return id != null && id.equals(((User) o).id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
// see https://vladmihalcea.com/how-to-implement-equals-and-hashcode-using-the-jpa-entity-identifier/
|
||||
return getClass().hashCode();
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
@Override
|
||||
public String toString() {
|
||||
return "User{" +
|
||||
"login='" + login + '\'' +
|
||||
", firstName='" + firstName + '\'' +
|
||||
", lastName='" + lastName + '\'' +
|
||||
", email='" + email + '\'' +
|
||||
", imageUrl='" + imageUrl + '\'' +
|
||||
", activated='" + activated + '\'' +
|
||||
", langKey='" + langKey + '\'' +
|
||||
", activationKey='" + activationKey + '\'' +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Domain objects.
|
||||
*/
|
||||
package it.sw.pa.comune.artegna.domain;
|
||||
4
src/main/java/it/sw/pa/comune/artegna/package-info.java
Normal file
4
src/main/java/it/sw/pa/comune/artegna/package-info.java
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Application root.
|
||||
*/
|
||||
package it.sw.pa.comune.artegna;
|
||||
@@ -0,0 +1,12 @@
|
||||
package it.sw.pa.comune.artegna.repository;
|
||||
|
||||
import it.sw.pa.comune.artegna.domain.Authority;
|
||||
import org.springframework.data.jpa.repository.*;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* Spring Data JPA repository for the Authority entity.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@Repository
|
||||
public interface AuthorityRepository extends JpaRepository<Authority, String> {}
|
||||
@@ -0,0 +1,16 @@
|
||||
package it.sw.pa.comune.artegna.repository;
|
||||
|
||||
import it.sw.pa.comune.artegna.domain.PersistentToken;
|
||||
import it.sw.pa.comune.artegna.domain.User;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
/**
|
||||
* Spring Data JPA repository for the {@link PersistentToken} entity.
|
||||
*/
|
||||
public interface PersistentTokenRepository extends JpaRepository<PersistentToken, String> {
|
||||
List<PersistentToken> findByUser(User user);
|
||||
|
||||
List<PersistentToken> findByTokenDateBefore(LocalDate localDate);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package it.sw.pa.comune.artegna.repository;
|
||||
|
||||
import it.sw.pa.comune.artegna.domain.User;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.data.domain.*;
|
||||
import org.springframework.data.jpa.repository.EntityGraph;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* Spring Data JPA repository for the {@link User} entity.
|
||||
*/
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
String USERS_BY_LOGIN_CACHE = "usersByLogin";
|
||||
|
||||
String USERS_BY_EMAIL_CACHE = "usersByEmail";
|
||||
Optional<User> findOneByActivationKey(String activationKey);
|
||||
List<User> findAllByActivatedIsFalseAndActivationKeyIsNotNullAndCreatedDateBefore(Instant dateTime);
|
||||
Optional<User> findOneByResetKey(String resetKey);
|
||||
Optional<User> findOneByEmailIgnoreCase(String email);
|
||||
Optional<User> findOneByLogin(String login);
|
||||
|
||||
@EntityGraph(attributePaths = "authorities")
|
||||
@Cacheable(cacheNames = USERS_BY_LOGIN_CACHE, unless = "#result == null")
|
||||
Optional<User> findOneWithAuthoritiesByLogin(String login);
|
||||
|
||||
@EntityGraph(attributePaths = "authorities")
|
||||
@Cacheable(cacheNames = USERS_BY_EMAIL_CACHE, unless = "#result == null")
|
||||
Optional<User> findOneWithAuthoritiesByEmailIgnoreCase(String email);
|
||||
|
||||
Page<User> findAllByIdNotNullAndActivatedIsTrue(Pageable pageable);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Repository layer.
|
||||
*/
|
||||
package it.sw.pa.comune.artegna.repository;
|
||||
@@ -0,0 +1,15 @@
|
||||
package it.sw.pa.comune.artegna.security;
|
||||
|
||||
/**
|
||||
* Constants for Spring Security authorities.
|
||||
*/
|
||||
public final class AuthoritiesConstants {
|
||||
|
||||
public static final String ADMIN = "ROLE_ADMIN";
|
||||
|
||||
public static final String USER = "ROLE_USER";
|
||||
|
||||
public static final String ANONYMOUS = "ROLE_ANONYMOUS";
|
||||
|
||||
private AuthoritiesConstants() {}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package it.sw.pa.comune.artegna.security;
|
||||
|
||||
import it.sw.pa.comune.artegna.domain.Authority;
|
||||
import it.sw.pa.comune.artegna.domain.User;
|
||||
import it.sw.pa.comune.artegna.repository.UserRepository;
|
||||
import java.util.*;
|
||||
import org.hibernate.validator.internal.constraintvalidators.hv.EmailValidator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Authenticate a user from the database.
|
||||
*/
|
||||
@Component("userDetailsService")
|
||||
public class DomainUserDetailsService implements UserDetailsService {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DomainUserDetailsService.class);
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public DomainUserDetailsService(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public UserDetails loadUserByUsername(final String login) {
|
||||
LOG.debug("Authenticating {}", login);
|
||||
|
||||
if (new EmailValidator().isValid(login, null)) {
|
||||
return userRepository
|
||||
.findOneWithAuthoritiesByEmailIgnoreCase(login)
|
||||
.map(user -> createSpringSecurityUser(login, user))
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User with email " + login + " was not found in the database"));
|
||||
}
|
||||
|
||||
String lowercaseLogin = login.toLowerCase(Locale.ENGLISH);
|
||||
return userRepository
|
||||
.findOneWithAuthoritiesByLogin(lowercaseLogin)
|
||||
.map(user -> createSpringSecurityUser(lowercaseLogin, user))
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User " + lowercaseLogin + " was not found in the database"));
|
||||
}
|
||||
|
||||
private org.springframework.security.core.userdetails.User createSpringSecurityUser(String lowercaseLogin, User user) {
|
||||
if (!user.isActivated()) {
|
||||
throw new UserNotActivatedException("User " + lowercaseLogin + " was not activated");
|
||||
}
|
||||
return UserWithId.fromUser(user);
|
||||
}
|
||||
|
||||
public static class UserWithId extends org.springframework.security.core.userdetails.User {
|
||||
|
||||
private final Long id;
|
||||
|
||||
public UserWithId(String login, String password, Collection<? extends GrantedAuthority> authorities, Long id) {
|
||||
super(login, password, authorities);
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return super.equals(obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return super.hashCode();
|
||||
}
|
||||
|
||||
public static UserWithId fromUser(User user) {
|
||||
return new UserWithId(
|
||||
user.getLogin(),
|
||||
user.getPassword(),
|
||||
user.getAuthorities().stream().map(Authority::getName).map(SimpleGrantedAuthority::new).toList(),
|
||||
user.getId()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package it.sw.pa.comune.artegna.security;
|
||||
|
||||
import it.sw.pa.comune.artegna.domain.PersistentToken;
|
||||
import it.sw.pa.comune.artegna.repository.PersistentTokenRepository;
|
||||
import it.sw.pa.comune.artegna.repository.UserRepository;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.web.authentication.rememberme.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.jhipster.config.JHipsterProperties;
|
||||
import tech.jhipster.security.PersistentTokenCache;
|
||||
import tech.jhipster.security.RandomUtil;
|
||||
|
||||
/**
|
||||
* Custom implementation of Spring Security's RememberMeServices.
|
||||
* <p>
|
||||
* Persistent tokens are used by Spring Security to automatically log in users.
|
||||
* <p>
|
||||
* This is a specific implementation of Spring Security's remember-me authentication, but it is much
|
||||
* more powerful than the standard implementations:
|
||||
* <ul>
|
||||
* <li>It allows a user to see the list of his currently opened sessions, and invalidate them</li>
|
||||
* <li>It stores more information, such as the IP address and the user agent, for audit purposes</li>
|
||||
* <li>When a user logs out, only his current session is invalidated, and not all of his sessions</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Please note that it allows the use of the same token for 5 seconds, and this value stored in a specific
|
||||
* cache during that period. This is to allow concurrent requests from the same user: otherwise, two
|
||||
* requests being sent at the same time could invalidate each other's token.
|
||||
* <p>
|
||||
* This is inspired by:
|
||||
* <ul>
|
||||
* <li><a href="https://github.com/blog/1661-modeling-your-app-s-user-session">GitHub's "Modeling your App's User Session"</a></li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* The main algorithm comes from Spring Security's {@code PersistentTokenBasedRememberMeServices}, but this class
|
||||
* couldn't be cleanly extended.
|
||||
*/
|
||||
@Service
|
||||
public class PersistentTokenRememberMeServices extends AbstractRememberMeServices {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PersistentTokenRememberMeServices.class);
|
||||
|
||||
// Token is valid for one month
|
||||
private static final int TOKEN_VALIDITY_DAYS = 31;
|
||||
|
||||
private static final int TOKEN_VALIDITY_SECONDS = 60 * 60 * 24 * TOKEN_VALIDITY_DAYS;
|
||||
|
||||
private static final long UPGRADED_TOKEN_VALIDITY_MILLIS = 5000l;
|
||||
|
||||
private final PersistentTokenCache<UpgradedRememberMeToken> upgradedTokenCache;
|
||||
|
||||
private final PersistentTokenRepository persistentTokenRepository;
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public PersistentTokenRememberMeServices(
|
||||
JHipsterProperties jHipsterProperties,
|
||||
org.springframework.security.core.userdetails.UserDetailsService userDetailsService,
|
||||
PersistentTokenRepository persistentTokenRepository,
|
||||
UserRepository userRepository
|
||||
) {
|
||||
super(jHipsterProperties.getSecurity().getRememberMe().getKey(), userDetailsService);
|
||||
this.persistentTokenRepository = persistentTokenRepository;
|
||||
this.userRepository = userRepository;
|
||||
upgradedTokenCache = new PersistentTokenCache<>(UPGRADED_TOKEN_VALIDITY_MILLIS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
|
||||
synchronized (this) {
|
||||
// prevent 2 authentication requests from the same user in parallel
|
||||
String login = null;
|
||||
UpgradedRememberMeToken upgradedToken = upgradedTokenCache.get(cookieTokens[0]);
|
||||
if (upgradedToken != null) {
|
||||
login = upgradedToken.getUserLoginIfValid(cookieTokens);
|
||||
LOG.debug("Detected previously upgraded login token for user '{}'", login);
|
||||
}
|
||||
|
||||
if (login == null) {
|
||||
PersistentToken token = getPersistentToken(cookieTokens);
|
||||
login = token.getUser().getLogin();
|
||||
|
||||
// Token also matches, so login is valid. Update the token value, keeping the *same* series number.
|
||||
LOG.debug("Refreshing persistent login token for user '{}', series '{}'", login, token.getSeries());
|
||||
token.setTokenDate(LocalDate.now());
|
||||
token.setTokenValue(RandomUtil.generateRandomAlphanumericString());
|
||||
token.setIpAddress(request.getRemoteAddr());
|
||||
token.setUserAgent(request.getHeader("User-Agent"));
|
||||
try {
|
||||
persistentTokenRepository.saveAndFlush(token);
|
||||
} catch (DataAccessException e) {
|
||||
LOG.error("Failed to update token: ", e);
|
||||
throw new RememberMeAuthenticationException("Autologin failed due to data access problem", e);
|
||||
}
|
||||
addCookie(token, request, response);
|
||||
upgradedTokenCache.put(cookieTokens[0], new UpgradedRememberMeToken(cookieTokens, login));
|
||||
}
|
||||
return getUserDetailsService().loadUserByUsername(login);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
|
||||
String login = successfulAuthentication.getName();
|
||||
|
||||
LOG.debug("Creating new persistent login for user {}", login);
|
||||
PersistentToken token = userRepository
|
||||
.findOneByLogin(login)
|
||||
.map(u -> {
|
||||
PersistentToken t = new PersistentToken();
|
||||
t.setSeries(RandomUtil.generateRandomAlphanumericString());
|
||||
t.setUser(u);
|
||||
t.setTokenValue(RandomUtil.generateRandomAlphanumericString());
|
||||
t.setTokenDate(LocalDate.now());
|
||||
t.setIpAddress(request.getRemoteAddr());
|
||||
t.setUserAgent(request.getHeader("User-Agent"));
|
||||
return t;
|
||||
})
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User " + login + " was not found in the database"));
|
||||
try {
|
||||
persistentTokenRepository.saveAndFlush(token);
|
||||
addCookie(token, request, response);
|
||||
} catch (DataAccessException e) {
|
||||
LOG.error("Failed to save persistent token ", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When logout occurs, only invalidate the current token, and not all user sessions.
|
||||
* <p>
|
||||
* The standard Spring Security implementations are too basic: they invalidate all tokens for the
|
||||
* current user, so when he logs out from one browser, all his other sessions are destroyed.
|
||||
*
|
||||
* @param request the request.
|
||||
* @param response the response.
|
||||
* @param authentication the authentication.
|
||||
*/
|
||||
@Override
|
||||
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
|
||||
String rememberMeCookie = extractRememberMeCookie(request);
|
||||
if (rememberMeCookie != null && rememberMeCookie.length() != 0) {
|
||||
try {
|
||||
String[] cookieTokens = decodeCookie(rememberMeCookie);
|
||||
PersistentToken token = getPersistentToken(cookieTokens);
|
||||
persistentTokenRepository.deleteById(token.getSeries());
|
||||
} catch (InvalidCookieException ice) {
|
||||
LOG.info("Invalid cookie, no persistent token could be deleted", ice);
|
||||
} catch (RememberMeAuthenticationException rmae) {
|
||||
LOG.debug("No persistent token found, so no token could be deleted", rmae);
|
||||
}
|
||||
}
|
||||
super.logout(request, response, authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the token and return it.
|
||||
*/
|
||||
private PersistentToken getPersistentToken(String[] cookieTokens) {
|
||||
if (cookieTokens.length != 2) {
|
||||
throw new InvalidCookieException(
|
||||
"Cookie token did not contain " + 2 + " tokens, but contained '" + Arrays.asList(cookieTokens) + "'"
|
||||
);
|
||||
}
|
||||
String presentedSeries = cookieTokens[0];
|
||||
String presentedToken = cookieTokens[1];
|
||||
Optional<PersistentToken> optionalToken = persistentTokenRepository.findById(presentedSeries);
|
||||
if (!optionalToken.isPresent()) {
|
||||
// No series match, so we can't authenticate using this cookie
|
||||
throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
|
||||
}
|
||||
PersistentToken token = optionalToken.orElseThrow();
|
||||
// We have a match for this user/series combination
|
||||
LOG.info("presentedToken={} / tokenValue={}", presentedToken, token.getTokenValue());
|
||||
if (!presentedToken.equals(token.getTokenValue())) {
|
||||
// Token doesn't match series value. Delete this session and throw an exception.
|
||||
persistentTokenRepository.deleteById(token.getSeries());
|
||||
throw new CookieTheftException("Invalid remember-me token (Series/token) mismatch. Implies previous " + "cookie theft attack.");
|
||||
}
|
||||
if (token.getTokenDate().plusDays(TOKEN_VALIDITY_DAYS).isBefore(LocalDate.now())) {
|
||||
persistentTokenRepository.deleteById(token.getSeries());
|
||||
throw new RememberMeAuthenticationException("Remember-me login has expired");
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
private void addCookie(PersistentToken token, HttpServletRequest request, HttpServletResponse response) {
|
||||
setCookie(new String[] { token.getSeries(), token.getTokenValue() }, TOKEN_VALIDITY_SECONDS, request, response);
|
||||
}
|
||||
|
||||
private static class UpgradedRememberMeToken implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final String[] upgradedToken;
|
||||
|
||||
private final String userLogin;
|
||||
|
||||
UpgradedRememberMeToken(String[] upgradedToken, String userLogin) {
|
||||
this.upgradedToken = upgradedToken;
|
||||
this.userLogin = userLogin;
|
||||
}
|
||||
|
||||
String getUserLoginIfValid(String[] currentToken) {
|
||||
if (currentToken[0].equals(this.upgradedToken[0]) && currentToken[1].equals(this.upgradedToken[1])) {
|
||||
return this.userLogin;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package it.sw.pa.comune.artegna.security;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
/**
|
||||
* Utility class for Spring Security.
|
||||
*/
|
||||
public final class SecurityUtils {
|
||||
|
||||
private SecurityUtils() {}
|
||||
|
||||
/**
|
||||
* Get the login of the current user.
|
||||
*
|
||||
* @return the login of the current user.
|
||||
*/
|
||||
public static Optional<String> getCurrentUserLogin() {
|
||||
SecurityContext securityContext = SecurityContextHolder.getContext();
|
||||
return Optional.ofNullable(extractPrincipal(securityContext.getAuthentication()));
|
||||
}
|
||||
|
||||
private static String extractPrincipal(Authentication authentication) {
|
||||
if (authentication == null) {
|
||||
return null;
|
||||
} else if (authentication.getPrincipal() instanceof UserDetails springSecurityUser) {
|
||||
return springSecurityUser.getUsername();
|
||||
} else if (authentication.getPrincipal() instanceof String s) {
|
||||
return s;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user is authenticated.
|
||||
*
|
||||
* @return true if the user is authenticated, false otherwise.
|
||||
*/
|
||||
public static boolean isAuthenticated() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
return authentication != null && getAuthorities(authentication).noneMatch(AuthoritiesConstants.ANONYMOUS::equals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current user has any of the authorities.
|
||||
*
|
||||
* @param authorities the authorities to check.
|
||||
* @return true if the current user has any of the authorities, false otherwise.
|
||||
*/
|
||||
public static boolean hasCurrentUserAnyOfAuthorities(String... authorities) {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
return (
|
||||
authentication != null && getAuthorities(authentication).anyMatch(authority -> Arrays.asList(authorities).contains(authority))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current user has none of the authorities.
|
||||
*
|
||||
* @param authorities the authorities to check.
|
||||
* @return true if the current user has none of the authorities, false otherwise.
|
||||
*/
|
||||
public static boolean hasCurrentUserNoneOfAuthorities(String... authorities) {
|
||||
return !hasCurrentUserAnyOfAuthorities(authorities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current user has a specific authority.
|
||||
*
|
||||
* @param authority the authority to check.
|
||||
* @return true if the current user has the authority, false otherwise.
|
||||
*/
|
||||
public static boolean hasCurrentUserThisAuthority(String authority) {
|
||||
return hasCurrentUserAnyOfAuthorities(authority);
|
||||
}
|
||||
|
||||
private static Stream<String> getAuthorities(Authentication authentication) {
|
||||
return authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package it.sw.pa.comune.artegna.security;
|
||||
|
||||
import it.sw.pa.comune.artegna.config.Constants;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.domain.AuditorAware;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Implementation of {@link AuditorAware} based on Spring Security.
|
||||
*/
|
||||
@Component
|
||||
public class SpringSecurityAuditorAware implements AuditorAware<String> {
|
||||
|
||||
@Override
|
||||
public Optional<String> getCurrentAuditor() {
|
||||
return Optional.of(SecurityUtils.getCurrentUserLogin().orElse(Constants.SYSTEM));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package it.sw.pa.comune.artegna.security;
|
||||
|
||||
import java.io.Serial;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
/**
|
||||
* This exception is thrown in case of a not activated user trying to authenticate.
|
||||
*/
|
||||
public class UserNotActivatedException extends AuthenticationException {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public UserNotActivatedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public UserNotActivatedException(String message, Throwable t) {
|
||||
super(message, t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Application security utilities.
|
||||
*/
|
||||
package it.sw.pa.comune.artegna.security;
|
||||
@@ -0,0 +1,13 @@
|
||||
package it.sw.pa.comune.artegna.service;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
public class EmailAlreadyUsedException extends RuntimeException {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public EmailAlreadyUsedException() {
|
||||
super("Email is already in use!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package it.sw.pa.comune.artegna.service;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
public class InvalidPasswordException extends RuntimeException {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public InvalidPasswordException() {
|
||||
super("Incorrect password");
|
||||
}
|
||||
}
|
||||
120
src/main/java/it/sw/pa/comune/artegna/service/MailService.java
Normal file
120
src/main/java/it/sw/pa/comune/artegna/service/MailService.java
Normal file
@@ -0,0 +1,120 @@
|
||||
package it.sw.pa.comune.artegna.service;
|
||||
|
||||
import it.sw.pa.comune.artegna.domain.User;
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.mail.MailException;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.thymeleaf.context.Context;
|
||||
import org.thymeleaf.spring6.SpringTemplateEngine;
|
||||
import tech.jhipster.config.JHipsterProperties;
|
||||
|
||||
/**
|
||||
* Service for sending emails asynchronously.
|
||||
* <p>
|
||||
* We use the {@link Async} annotation to send emails asynchronously.
|
||||
*/
|
||||
@Service
|
||||
public class MailService {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MailService.class);
|
||||
|
||||
private static final String USER = "user";
|
||||
|
||||
private static final String BASE_URL = "baseUrl";
|
||||
|
||||
private final JHipsterProperties jHipsterProperties;
|
||||
|
||||
private final JavaMailSender javaMailSender;
|
||||
|
||||
private final MessageSource messageSource;
|
||||
|
||||
private final SpringTemplateEngine templateEngine;
|
||||
|
||||
public MailService(
|
||||
JHipsterProperties jHipsterProperties,
|
||||
JavaMailSender javaMailSender,
|
||||
MessageSource messageSource,
|
||||
SpringTemplateEngine templateEngine
|
||||
) {
|
||||
this.jHipsterProperties = jHipsterProperties;
|
||||
this.javaMailSender = javaMailSender;
|
||||
this.messageSource = messageSource;
|
||||
this.templateEngine = templateEngine;
|
||||
}
|
||||
|
||||
@Async
|
||||
public void sendEmail(String to, String subject, String content, boolean isMultipart, boolean isHtml) {
|
||||
sendEmailSync(to, subject, content, isMultipart, isHtml);
|
||||
}
|
||||
|
||||
private void sendEmailSync(String to, String subject, String content, boolean isMultipart, boolean isHtml) {
|
||||
LOG.debug(
|
||||
"Send email[multipart '{}' and html '{}'] to '{}' with subject '{}' and content={}",
|
||||
isMultipart,
|
||||
isHtml,
|
||||
to,
|
||||
subject,
|
||||
content
|
||||
);
|
||||
|
||||
// Prepare message using a Spring helper
|
||||
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
|
||||
try {
|
||||
MimeMessageHelper message = new MimeMessageHelper(mimeMessage, isMultipart, StandardCharsets.UTF_8.name());
|
||||
message.setTo(to);
|
||||
message.setFrom(jHipsterProperties.getMail().getFrom());
|
||||
message.setSubject(subject);
|
||||
message.setText(content, isHtml);
|
||||
javaMailSender.send(mimeMessage);
|
||||
LOG.debug("Sent email to User '{}'", to);
|
||||
} catch (MailException | MessagingException e) {
|
||||
LOG.warn("Email could not be sent to user '{}'", to, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Async
|
||||
public void sendEmailFromTemplate(User user, String templateName, String titleKey) {
|
||||
sendEmailFromTemplateSync(user, templateName, titleKey);
|
||||
}
|
||||
|
||||
private void sendEmailFromTemplateSync(User user, String templateName, String titleKey) {
|
||||
if (user.getEmail() == null) {
|
||||
LOG.debug("Email doesn't exist for user '{}'", user.getLogin());
|
||||
return;
|
||||
}
|
||||
Locale locale = Locale.forLanguageTag(user.getLangKey());
|
||||
Context context = new Context(locale);
|
||||
context.setVariable(USER, user);
|
||||
context.setVariable(BASE_URL, jHipsterProperties.getMail().getBaseUrl());
|
||||
String content = templateEngine.process(templateName, context);
|
||||
String subject = messageSource.getMessage(titleKey, null, locale);
|
||||
sendEmailSync(user.getEmail(), subject, content, false, true);
|
||||
}
|
||||
|
||||
@Async
|
||||
public void sendActivationEmail(User user) {
|
||||
LOG.debug("Sending activation email to '{}'", user.getEmail());
|
||||
sendEmailFromTemplateSync(user, "mail/activationEmail", "email.activation.title");
|
||||
}
|
||||
|
||||
@Async
|
||||
public void sendCreationEmail(User user) {
|
||||
LOG.debug("Sending creation email to '{}'", user.getEmail());
|
||||
sendEmailFromTemplateSync(user, "mail/creationEmail", "email.activation.title");
|
||||
}
|
||||
|
||||
@Async
|
||||
public void sendPasswordResetMail(User user) {
|
||||
LOG.debug("Sending password reset email to '{}'", user.getEmail());
|
||||
sendEmailFromTemplateSync(user, "mail/passwordResetEmail", "email.reset.title");
|
||||
}
|
||||
}
|
||||
349
src/main/java/it/sw/pa/comune/artegna/service/UserService.java
Normal file
349
src/main/java/it/sw/pa/comune/artegna/service/UserService.java
Normal file
@@ -0,0 +1,349 @@
|
||||
package it.sw.pa.comune.artegna.service;
|
||||
|
||||
import it.sw.pa.comune.artegna.config.Constants;
|
||||
import it.sw.pa.comune.artegna.domain.Authority;
|
||||
import it.sw.pa.comune.artegna.domain.User;
|
||||
import it.sw.pa.comune.artegna.repository.AuthorityRepository;
|
||||
import it.sw.pa.comune.artegna.repository.PersistentTokenRepository;
|
||||
import it.sw.pa.comune.artegna.repository.UserRepository;
|
||||
import it.sw.pa.comune.artegna.security.AuthoritiesConstants;
|
||||
import it.sw.pa.comune.artegna.security.SecurityUtils;
|
||||
import it.sw.pa.comune.artegna.service.dto.AdminUserDTO;
|
||||
import it.sw.pa.comune.artegna.service.dto.UserDTO;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import tech.jhipster.security.RandomUtil;
|
||||
|
||||
/**
|
||||
* Service class for managing users.
|
||||
*/
|
||||
@Service
|
||||
@Transactional
|
||||
public class UserService {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UserService.class);
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
private final PersistentTokenRepository persistentTokenRepository;
|
||||
|
||||
private final AuthorityRepository authorityRepository;
|
||||
|
||||
private final CacheManager cacheManager;
|
||||
|
||||
public UserService(
|
||||
UserRepository userRepository,
|
||||
PasswordEncoder passwordEncoder,
|
||||
PersistentTokenRepository persistentTokenRepository,
|
||||
AuthorityRepository authorityRepository,
|
||||
CacheManager cacheManager
|
||||
) {
|
||||
this.userRepository = userRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.persistentTokenRepository = persistentTokenRepository;
|
||||
this.authorityRepository = authorityRepository;
|
||||
this.cacheManager = cacheManager;
|
||||
}
|
||||
|
||||
public Optional<User> activateRegistration(String key) {
|
||||
LOG.debug("Activating user for activation key {}", key);
|
||||
return userRepository
|
||||
.findOneByActivationKey(key)
|
||||
.map(user -> {
|
||||
// activate given user for the registration key.
|
||||
user.setActivated(true);
|
||||
user.setActivationKey(null);
|
||||
this.clearUserCaches(user);
|
||||
LOG.debug("Activated user: {}", user);
|
||||
return user;
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<User> completePasswordReset(String newPassword, String key) {
|
||||
LOG.debug("Reset user password for reset key {}", key);
|
||||
return userRepository
|
||||
.findOneByResetKey(key)
|
||||
.filter(user -> user.getResetDate().isAfter(Instant.now().minus(1, ChronoUnit.DAYS)))
|
||||
.map(user -> {
|
||||
user.setPassword(passwordEncoder.encode(newPassword));
|
||||
user.setResetKey(null);
|
||||
user.setResetDate(null);
|
||||
this.clearUserCaches(user);
|
||||
return user;
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<User> requestPasswordReset(String mail) {
|
||||
return userRepository
|
||||
.findOneByEmailIgnoreCase(mail)
|
||||
.filter(User::isActivated)
|
||||
.map(user -> {
|
||||
user.setResetKey(RandomUtil.generateResetKey());
|
||||
user.setResetDate(Instant.now());
|
||||
this.clearUserCaches(user);
|
||||
return user;
|
||||
});
|
||||
}
|
||||
|
||||
public User registerUser(AdminUserDTO userDTO, String password) {
|
||||
userRepository
|
||||
.findOneByLogin(userDTO.getLogin().toLowerCase())
|
||||
.ifPresent(existingUser -> {
|
||||
boolean removed = removeNonActivatedUser(existingUser);
|
||||
if (!removed) {
|
||||
throw new UsernameAlreadyUsedException();
|
||||
}
|
||||
});
|
||||
userRepository
|
||||
.findOneByEmailIgnoreCase(userDTO.getEmail())
|
||||
.ifPresent(existingUser -> {
|
||||
boolean removed = removeNonActivatedUser(existingUser);
|
||||
if (!removed) {
|
||||
throw new EmailAlreadyUsedException();
|
||||
}
|
||||
});
|
||||
User newUser = new User();
|
||||
String encryptedPassword = passwordEncoder.encode(password);
|
||||
newUser.setLogin(userDTO.getLogin().toLowerCase());
|
||||
// new user gets initially a generated password
|
||||
newUser.setPassword(encryptedPassword);
|
||||
newUser.setFirstName(userDTO.getFirstName());
|
||||
newUser.setLastName(userDTO.getLastName());
|
||||
if (userDTO.getEmail() != null) {
|
||||
newUser.setEmail(userDTO.getEmail().toLowerCase());
|
||||
}
|
||||
newUser.setImageUrl(userDTO.getImageUrl());
|
||||
newUser.setLangKey(userDTO.getLangKey());
|
||||
// new user is not active
|
||||
newUser.setActivated(false);
|
||||
// new user gets registration key
|
||||
newUser.setActivationKey(RandomUtil.generateActivationKey());
|
||||
Set<Authority> authorities = new HashSet<>();
|
||||
authorityRepository.findById(AuthoritiesConstants.USER).ifPresent(authorities::add);
|
||||
newUser.setAuthorities(authorities);
|
||||
userRepository.save(newUser);
|
||||
this.clearUserCaches(newUser);
|
||||
LOG.debug("Created Information for User: {}", newUser);
|
||||
return newUser;
|
||||
}
|
||||
|
||||
private boolean removeNonActivatedUser(User existingUser) {
|
||||
if (existingUser.isActivated()) {
|
||||
return false;
|
||||
}
|
||||
userRepository.delete(existingUser);
|
||||
userRepository.flush();
|
||||
this.clearUserCaches(existingUser);
|
||||
return true;
|
||||
}
|
||||
|
||||
public User createUser(AdminUserDTO userDTO) {
|
||||
User user = new User();
|
||||
user.setLogin(userDTO.getLogin().toLowerCase());
|
||||
user.setFirstName(userDTO.getFirstName());
|
||||
user.setLastName(userDTO.getLastName());
|
||||
if (userDTO.getEmail() != null) {
|
||||
user.setEmail(userDTO.getEmail().toLowerCase());
|
||||
}
|
||||
user.setImageUrl(userDTO.getImageUrl());
|
||||
if (userDTO.getLangKey() == null) {
|
||||
user.setLangKey(Constants.DEFAULT_LANGUAGE); // default language
|
||||
} else {
|
||||
user.setLangKey(userDTO.getLangKey());
|
||||
}
|
||||
String encryptedPassword = passwordEncoder.encode(RandomUtil.generatePassword());
|
||||
user.setPassword(encryptedPassword);
|
||||
user.setResetKey(RandomUtil.generateResetKey());
|
||||
user.setResetDate(Instant.now());
|
||||
user.setActivated(true);
|
||||
if (userDTO.getAuthorities() != null) {
|
||||
Set<Authority> authorities = userDTO
|
||||
.getAuthorities()
|
||||
.stream()
|
||||
.map(authorityRepository::findById)
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.collect(Collectors.toSet());
|
||||
user.setAuthorities(authorities);
|
||||
}
|
||||
userRepository.save(user);
|
||||
this.clearUserCaches(user);
|
||||
LOG.debug("Created Information for User: {}", user);
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all information for a specific user, and return the modified user.
|
||||
*
|
||||
* @param userDTO user to update.
|
||||
* @return updated user.
|
||||
*/
|
||||
public Optional<AdminUserDTO> updateUser(AdminUserDTO userDTO) {
|
||||
return Optional.of(userRepository.findById(userDTO.getId()))
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.map(user -> {
|
||||
this.clearUserCaches(user);
|
||||
user.setLogin(userDTO.getLogin().toLowerCase());
|
||||
user.setFirstName(userDTO.getFirstName());
|
||||
user.setLastName(userDTO.getLastName());
|
||||
if (userDTO.getEmail() != null) {
|
||||
user.setEmail(userDTO.getEmail().toLowerCase());
|
||||
}
|
||||
user.setImageUrl(userDTO.getImageUrl());
|
||||
user.setActivated(userDTO.isActivated());
|
||||
user.setLangKey(userDTO.getLangKey());
|
||||
Set<Authority> managedAuthorities = user.getAuthorities();
|
||||
managedAuthorities.clear();
|
||||
userDTO
|
||||
.getAuthorities()
|
||||
.stream()
|
||||
.map(authorityRepository::findById)
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.forEach(managedAuthorities::add);
|
||||
userRepository.save(user);
|
||||
this.clearUserCaches(user);
|
||||
LOG.debug("Changed Information for User: {}", user);
|
||||
return user;
|
||||
})
|
||||
.map(AdminUserDTO::new);
|
||||
}
|
||||
|
||||
public void deleteUser(String login) {
|
||||
userRepository
|
||||
.findOneByLogin(login)
|
||||
.ifPresent(user -> {
|
||||
userRepository.delete(user);
|
||||
this.clearUserCaches(user);
|
||||
LOG.debug("Deleted User: {}", user);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update basic information (first name, last name, email, language) for the current user.
|
||||
*
|
||||
* @param firstName first name of user.
|
||||
* @param lastName last name of user.
|
||||
* @param email email id of user.
|
||||
* @param langKey language key.
|
||||
* @param imageUrl image URL of user.
|
||||
*/
|
||||
public void updateUser(String firstName, String lastName, String email, String langKey, String imageUrl) {
|
||||
SecurityUtils.getCurrentUserLogin()
|
||||
.flatMap(userRepository::findOneByLogin)
|
||||
.ifPresent(user -> {
|
||||
user.setFirstName(firstName);
|
||||
user.setLastName(lastName);
|
||||
if (email != null) {
|
||||
user.setEmail(email.toLowerCase());
|
||||
}
|
||||
user.setLangKey(langKey);
|
||||
user.setImageUrl(imageUrl);
|
||||
userRepository.save(user);
|
||||
this.clearUserCaches(user);
|
||||
LOG.debug("Changed Information for User: {}", user);
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void changePassword(String currentClearTextPassword, String newPassword) {
|
||||
SecurityUtils.getCurrentUserLogin()
|
||||
.flatMap(userRepository::findOneByLogin)
|
||||
.ifPresent(user -> {
|
||||
String currentEncryptedPassword = user.getPassword();
|
||||
if (!passwordEncoder.matches(currentClearTextPassword, currentEncryptedPassword)) {
|
||||
throw new InvalidPasswordException();
|
||||
}
|
||||
String encryptedPassword = passwordEncoder.encode(newPassword);
|
||||
user.setPassword(encryptedPassword);
|
||||
this.clearUserCaches(user);
|
||||
LOG.debug("Changed password for User: {}", user);
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<AdminUserDTO> getAllManagedUsers(Pageable pageable) {
|
||||
return userRepository.findAll(pageable).map(AdminUserDTO::new);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<UserDTO> getAllPublicUsers(Pageable pageable) {
|
||||
return userRepository.findAllByIdNotNullAndActivatedIsTrue(pageable).map(UserDTO::new);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<User> getUserWithAuthoritiesByLogin(String login) {
|
||||
return userRepository.findOneWithAuthoritiesByLogin(login);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<User> getUserWithAuthorities() {
|
||||
return SecurityUtils.getCurrentUserLogin().flatMap(userRepository::findOneWithAuthoritiesByLogin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persistent Token are used for providing automatic authentication, they should be automatically deleted after
|
||||
* 30 days.
|
||||
* <p>
|
||||
* This is scheduled to get fired every day, at midnight.
|
||||
*/
|
||||
@Scheduled(cron = "0 0 0 * * ?")
|
||||
public void removeOldPersistentTokens() {
|
||||
LocalDate now = LocalDate.now();
|
||||
persistentTokenRepository
|
||||
.findByTokenDateBefore(now.minusMonths(1))
|
||||
.forEach(token -> {
|
||||
LOG.debug("Deleting token {}", token.getSeries());
|
||||
User user = token.getUser();
|
||||
user.getPersistentTokens().remove(token);
|
||||
persistentTokenRepository.delete(token);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Not activated users should be automatically deleted after 3 days.
|
||||
* <p>
|
||||
* This is scheduled to get fired every day, at 01:00 (am).
|
||||
*/
|
||||
@Scheduled(cron = "0 0 1 * * ?")
|
||||
public void removeNotActivatedUsers() {
|
||||
userRepository
|
||||
.findAllByActivatedIsFalseAndActivationKeyIsNotNullAndCreatedDateBefore(Instant.now().minus(3, ChronoUnit.DAYS))
|
||||
.forEach(user -> {
|
||||
LOG.debug("Deleting not activated user {}", user.getLogin());
|
||||
userRepository.delete(user);
|
||||
this.clearUserCaches(user);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all the authorities.
|
||||
* @return a list of all the authorities.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<String> getAuthorities() {
|
||||
return authorityRepository.findAll().stream().map(Authority::getName).toList();
|
||||
}
|
||||
|
||||
private void clearUserCaches(User user) {
|
||||
Objects.requireNonNull(cacheManager.getCache(UserRepository.USERS_BY_LOGIN_CACHE)).evictIfPresent(user.getLogin());
|
||||
if (user.getEmail() != null) {
|
||||
Objects.requireNonNull(cacheManager.getCache(UserRepository.USERS_BY_EMAIL_CACHE)).evictIfPresent(user.getEmail());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package it.sw.pa.comune.artegna.service;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
public class UsernameAlreadyUsedException extends RuntimeException {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public UsernameAlreadyUsedException() {
|
||||
super("Login name already used!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package it.sw.pa.comune.artegna.service.dto;
|
||||
|
||||
import it.sw.pa.comune.artegna.config.Constants;
|
||||
import it.sw.pa.comune.artegna.domain.Authority;
|
||||
import it.sw.pa.comune.artegna.domain.User;
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A DTO representing a user, with his authorities.
|
||||
*/
|
||||
public class AdminUserDTO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long id;
|
||||
|
||||
@NotBlank
|
||||
@Pattern(regexp = Constants.LOGIN_REGEX)
|
||||
@Size(min = 1, max = 50)
|
||||
private String login;
|
||||
|
||||
@Size(max = 50)
|
||||
private String firstName;
|
||||
|
||||
@Size(max = 50)
|
||||
private String lastName;
|
||||
|
||||
@Email
|
||||
@Size(min = 5, max = 254)
|
||||
private String email;
|
||||
|
||||
@Size(max = 256)
|
||||
private String imageUrl;
|
||||
|
||||
private boolean activated = false;
|
||||
|
||||
@Size(min = 2, max = 10)
|
||||
private String langKey;
|
||||
|
||||
private String createdBy;
|
||||
|
||||
private Instant createdDate;
|
||||
|
||||
private String lastModifiedBy;
|
||||
|
||||
private Instant lastModifiedDate;
|
||||
|
||||
private Set<String> authorities;
|
||||
|
||||
public AdminUserDTO() {
|
||||
// Empty constructor needed for Jackson.
|
||||
}
|
||||
|
||||
public AdminUserDTO(User user) {
|
||||
this.id = user.getId();
|
||||
this.login = user.getLogin();
|
||||
this.firstName = user.getFirstName();
|
||||
this.lastName = user.getLastName();
|
||||
this.email = user.getEmail();
|
||||
this.activated = user.isActivated();
|
||||
this.imageUrl = user.getImageUrl();
|
||||
this.langKey = user.getLangKey();
|
||||
this.createdBy = user.getCreatedBy();
|
||||
this.createdDate = user.getCreatedDate();
|
||||
this.lastModifiedBy = user.getLastModifiedBy();
|
||||
this.lastModifiedDate = user.getLastModifiedDate();
|
||||
this.authorities = user.getAuthorities().stream().map(Authority::getName).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getLogin() {
|
||||
return login;
|
||||
}
|
||||
|
||||
public void setLogin(String login) {
|
||||
this.login = login;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getImageUrl() {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
public void setImageUrl(String imageUrl) {
|
||||
this.imageUrl = imageUrl;
|
||||
}
|
||||
|
||||
public boolean isActivated() {
|
||||
return activated;
|
||||
}
|
||||
|
||||
public void setActivated(boolean activated) {
|
||||
this.activated = activated;
|
||||
}
|
||||
|
||||
public String getLangKey() {
|
||||
return langKey;
|
||||
}
|
||||
|
||||
public void setLangKey(String langKey) {
|
||||
this.langKey = langKey;
|
||||
}
|
||||
|
||||
public String getCreatedBy() {
|
||||
return createdBy;
|
||||
}
|
||||
|
||||
public void setCreatedBy(String createdBy) {
|
||||
this.createdBy = createdBy;
|
||||
}
|
||||
|
||||
public Instant getCreatedDate() {
|
||||
return createdDate;
|
||||
}
|
||||
|
||||
public void setCreatedDate(Instant createdDate) {
|
||||
this.createdDate = createdDate;
|
||||
}
|
||||
|
||||
public String getLastModifiedBy() {
|
||||
return lastModifiedBy;
|
||||
}
|
||||
|
||||
public void setLastModifiedBy(String lastModifiedBy) {
|
||||
this.lastModifiedBy = lastModifiedBy;
|
||||
}
|
||||
|
||||
public Instant getLastModifiedDate() {
|
||||
return lastModifiedDate;
|
||||
}
|
||||
|
||||
public void setLastModifiedDate(Instant lastModifiedDate) {
|
||||
this.lastModifiedDate = lastModifiedDate;
|
||||
}
|
||||
|
||||
public Set<String> getAuthorities() {
|
||||
return authorities;
|
||||
}
|
||||
|
||||
public void setAuthorities(Set<String> authorities) {
|
||||
this.authorities = authorities;
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AdminUserDTO{" +
|
||||
"login='" + login + '\'' +
|
||||
", firstName='" + firstName + '\'' +
|
||||
", lastName='" + lastName + '\'' +
|
||||
", email='" + email + '\'' +
|
||||
", imageUrl='" + imageUrl + '\'' +
|
||||
", activated=" + activated +
|
||||
", langKey='" + langKey + '\'' +
|
||||
", createdBy=" + createdBy +
|
||||
", createdDate=" + createdDate +
|
||||
", lastModifiedBy='" + lastModifiedBy + '\'' +
|
||||
", lastModifiedDate=" + lastModifiedDate +
|
||||
", authorities=" + authorities +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package it.sw.pa.comune.artegna.service.dto;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* A DTO representing a password change required data - current and new password.
|
||||
*/
|
||||
public class PasswordChangeDTO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String currentPassword;
|
||||
private String newPassword;
|
||||
|
||||
public PasswordChangeDTO() {
|
||||
// Empty constructor needed for Jackson.
|
||||
}
|
||||
|
||||
public PasswordChangeDTO(String currentPassword, String newPassword) {
|
||||
this.currentPassword = currentPassword;
|
||||
this.newPassword = newPassword;
|
||||
}
|
||||
|
||||
public String getCurrentPassword() {
|
||||
return currentPassword;
|
||||
}
|
||||
|
||||
public void setCurrentPassword(String currentPassword) {
|
||||
this.currentPassword = currentPassword;
|
||||
}
|
||||
|
||||
public String getNewPassword() {
|
||||
return newPassword;
|
||||
}
|
||||
|
||||
public void setNewPassword(String newPassword) {
|
||||
this.newPassword = newPassword;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package it.sw.pa.comune.artegna.service.dto;
|
||||
|
||||
import it.sw.pa.comune.artegna.domain.User;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A DTO representing a user, with only the public attributes.
|
||||
*/
|
||||
public class UserDTO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long id;
|
||||
|
||||
private String login;
|
||||
|
||||
public UserDTO() {
|
||||
// Empty constructor needed for Jackson.
|
||||
}
|
||||
|
||||
public UserDTO(User user) {
|
||||
this.id = user.getId();
|
||||
// Customize it here if you need, or not, firstName/lastName/etc
|
||||
this.login = user.getLogin();
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getLogin() {
|
||||
return login;
|
||||
}
|
||||
|
||||
public void setLogin(String login) {
|
||||
this.login = login;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UserDTO userDTO = (UserDTO) o;
|
||||
if (userDTO.getId() == null || getId() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Objects.equals(getId(), userDTO.getId()) && Objects.equals(getLogin(), userDTO.getLogin());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(getId(), getLogin());
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UserDTO{" +
|
||||
"id='" + id + '\'' +
|
||||
", login='" + login + '\'' +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Data transfer objects for rest mapping.
|
||||
*/
|
||||
package it.sw.pa.comune.artegna.service.dto;
|
||||
@@ -0,0 +1,150 @@
|
||||
package it.sw.pa.comune.artegna.service.mapper;
|
||||
|
||||
import it.sw.pa.comune.artegna.domain.Authority;
|
||||
import it.sw.pa.comune.artegna.domain.User;
|
||||
import it.sw.pa.comune.artegna.service.dto.AdminUserDTO;
|
||||
import it.sw.pa.comune.artegna.service.dto.UserDTO;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import org.mapstruct.BeanMapping;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.Named;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Mapper for the entity {@link User} and its DTO called {@link UserDTO}.
|
||||
*
|
||||
* Normal mappers are generated using MapStruct, this one is hand-coded as MapStruct
|
||||
* support is still in beta, and requires a manual step with an IDE.
|
||||
*/
|
||||
@Service
|
||||
public class UserMapper {
|
||||
|
||||
public List<UserDTO> usersToUserDTOs(List<User> users) {
|
||||
return users.stream().filter(Objects::nonNull).map(this::userToUserDTO).toList();
|
||||
}
|
||||
|
||||
public UserDTO userToUserDTO(User user) {
|
||||
return new UserDTO(user);
|
||||
}
|
||||
|
||||
public List<AdminUserDTO> usersToAdminUserDTOs(List<User> users) {
|
||||
return users.stream().filter(Objects::nonNull).map(this::userToAdminUserDTO).toList();
|
||||
}
|
||||
|
||||
public AdminUserDTO userToAdminUserDTO(User user) {
|
||||
return new AdminUserDTO(user);
|
||||
}
|
||||
|
||||
public List<User> userDTOsToUsers(List<AdminUserDTO> userDTOs) {
|
||||
return userDTOs.stream().filter(Objects::nonNull).map(this::userDTOToUser).toList();
|
||||
}
|
||||
|
||||
public User userDTOToUser(AdminUserDTO userDTO) {
|
||||
if (userDTO == null) {
|
||||
return null;
|
||||
} else {
|
||||
User user = new User();
|
||||
user.setId(userDTO.getId());
|
||||
user.setLogin(userDTO.getLogin());
|
||||
user.setFirstName(userDTO.getFirstName());
|
||||
user.setLastName(userDTO.getLastName());
|
||||
user.setEmail(userDTO.getEmail());
|
||||
user.setImageUrl(userDTO.getImageUrl());
|
||||
user.setCreatedBy(userDTO.getCreatedBy());
|
||||
user.setCreatedDate(userDTO.getCreatedDate());
|
||||
user.setLastModifiedBy(userDTO.getLastModifiedBy());
|
||||
user.setLastModifiedDate(userDTO.getLastModifiedDate());
|
||||
user.setActivated(userDTO.isActivated());
|
||||
user.setLangKey(userDTO.getLangKey());
|
||||
Set<Authority> authorities = this.authoritiesFromStrings(userDTO.getAuthorities());
|
||||
user.setAuthorities(authorities);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
private Set<Authority> authoritiesFromStrings(Set<String> authoritiesAsString) {
|
||||
Set<Authority> authorities = new HashSet<>();
|
||||
|
||||
if (authoritiesAsString != null) {
|
||||
authorities = authoritiesAsString
|
||||
.stream()
|
||||
.map(string -> {
|
||||
Authority auth = new Authority();
|
||||
auth.setName(string);
|
||||
return auth;
|
||||
})
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
return authorities;
|
||||
}
|
||||
|
||||
public User userFromId(Long id) {
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
User user = new User();
|
||||
user.setId(id);
|
||||
return user;
|
||||
}
|
||||
|
||||
@Named("id")
|
||||
@BeanMapping(ignoreByDefault = true)
|
||||
@Mapping(target = "id", source = "id")
|
||||
public UserDTO toDtoId(User user) {
|
||||
if (user == null) {
|
||||
return null;
|
||||
}
|
||||
UserDTO userDto = new UserDTO();
|
||||
userDto.setId(user.getId());
|
||||
return userDto;
|
||||
}
|
||||
|
||||
@Named("idSet")
|
||||
@BeanMapping(ignoreByDefault = true)
|
||||
@Mapping(target = "id", source = "id")
|
||||
public Set<UserDTO> toDtoIdSet(Set<User> users) {
|
||||
if (users == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
Set<UserDTO> userSet = new HashSet<>();
|
||||
for (User userEntity : users) {
|
||||
userSet.add(this.toDtoId(userEntity));
|
||||
}
|
||||
|
||||
return userSet;
|
||||
}
|
||||
|
||||
@Named("login")
|
||||
@BeanMapping(ignoreByDefault = true)
|
||||
@Mapping(target = "id", source = "id")
|
||||
@Mapping(target = "login", source = "login")
|
||||
public UserDTO toDtoLogin(User user) {
|
||||
if (user == null) {
|
||||
return null;
|
||||
}
|
||||
UserDTO userDto = new UserDTO();
|
||||
userDto.setId(user.getId());
|
||||
userDto.setLogin(user.getLogin());
|
||||
return userDto;
|
||||
}
|
||||
|
||||
@Named("loginSet")
|
||||
@BeanMapping(ignoreByDefault = true)
|
||||
@Mapping(target = "id", source = "id")
|
||||
@Mapping(target = "login", source = "login")
|
||||
public Set<UserDTO> toDtoLoginSet(Set<User> users) {
|
||||
if (users == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
Set<UserDTO> userSet = new HashSet<>();
|
||||
for (User userEntity : users) {
|
||||
userSet.add(this.toDtoLogin(userEntity));
|
||||
}
|
||||
|
||||
return userSet;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Data transfer objects mappers.
|
||||
*/
|
||||
package it.sw.pa.comune.artegna.service.mapper;
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Service layer.
|
||||
*/
|
||||
package it.sw.pa.comune.artegna.service;
|
||||
@@ -0,0 +1,33 @@
|
||||
package it.sw.pa.comune.artegna.web.filter;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
public class SpaWebFilter extends OncePerRequestFilter {
|
||||
|
||||
/**
|
||||
* Forwards any unmapped paths (except those containing a period) to the client {@code index.html}.
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
// Request URI includes the contextPath if any, removed it.
|
||||
String path = request.getRequestURI().substring(request.getContextPath().length());
|
||||
if (
|
||||
!path.startsWith("/api") &&
|
||||
!path.startsWith("/management") &&
|
||||
!path.startsWith("/v3/api-docs") &&
|
||||
!path.contains(".") &&
|
||||
path.matches("/(.*)")
|
||||
) {
|
||||
request.getRequestDispatcher("/index.html").forward(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Request chain filters.
|
||||
*/
|
||||
package it.sw.pa.comune.artegna.web.filter;
|
||||
@@ -0,0 +1,255 @@
|
||||
package it.sw.pa.comune.artegna.web.rest;
|
||||
|
||||
import it.sw.pa.comune.artegna.domain.PersistentToken;
|
||||
import it.sw.pa.comune.artegna.domain.User;
|
||||
import it.sw.pa.comune.artegna.repository.PersistentTokenRepository;
|
||||
import it.sw.pa.comune.artegna.repository.UserRepository;
|
||||
import it.sw.pa.comune.artegna.security.SecurityUtils;
|
||||
import it.sw.pa.comune.artegna.service.MailService;
|
||||
import it.sw.pa.comune.artegna.service.UserService;
|
||||
import it.sw.pa.comune.artegna.service.dto.AdminUserDTO;
|
||||
import it.sw.pa.comune.artegna.service.dto.PasswordChangeDTO;
|
||||
import it.sw.pa.comune.artegna.web.rest.errors.*;
|
||||
import it.sw.pa.comune.artegna.web.rest.vm.KeyAndPasswordVM;
|
||||
import it.sw.pa.comune.artegna.web.rest.vm.ManagedUserVM;
|
||||
import jakarta.validation.Valid;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Principal;
|
||||
import java.util.*;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* REST controller for managing the current user's account.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class AccountResource {
|
||||
|
||||
private static class AccountResourceException extends RuntimeException {
|
||||
|
||||
private AccountResourceException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AccountResource.class);
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
private final MailService mailService;
|
||||
|
||||
private final PersistentTokenRepository persistentTokenRepository;
|
||||
|
||||
public AccountResource(
|
||||
UserRepository userRepository,
|
||||
UserService userService,
|
||||
MailService mailService,
|
||||
PersistentTokenRepository persistentTokenRepository
|
||||
) {
|
||||
this.userRepository = userRepository;
|
||||
this.userService = userService;
|
||||
this.mailService = mailService;
|
||||
this.persistentTokenRepository = persistentTokenRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code POST /register} : register the user.
|
||||
*
|
||||
* @param managedUserVM the managed user View Model.
|
||||
* @throws InvalidPasswordException {@code 400 (Bad Request)} if the password is incorrect.
|
||||
* @throws EmailAlreadyUsedException {@code 400 (Bad Request)} if the email is already used.
|
||||
* @throws LoginAlreadyUsedException {@code 400 (Bad Request)} if the login is already used.
|
||||
*/
|
||||
@PostMapping("/register")
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
public void registerAccount(@Valid @RequestBody ManagedUserVM managedUserVM) {
|
||||
if (isPasswordLengthInvalid(managedUserVM.getPassword())) {
|
||||
throw new InvalidPasswordException();
|
||||
}
|
||||
User user = userService.registerUser(managedUserVM, managedUserVM.getPassword());
|
||||
mailService.sendActivationEmail(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code GET /activate} : activate the registered user.
|
||||
*
|
||||
* @param key the activation key.
|
||||
* @throws RuntimeException {@code 500 (Internal Server Error)} if the user couldn't be activated.
|
||||
*/
|
||||
@GetMapping("/activate")
|
||||
public void activateAccount(@RequestParam(value = "key") String key) {
|
||||
Optional<User> user = userService.activateRegistration(key);
|
||||
if (!user.isPresent()) {
|
||||
throw new AccountResourceException("No user was found for this activation key");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code GET /authenticate} : check if the user is authenticated.
|
||||
*
|
||||
* @return the {@link ResponseEntity} with status {@code 204 (No Content)},
|
||||
* or with status {@code 401 (Unauthorized)} if not authenticated.
|
||||
*/
|
||||
@GetMapping("/authenticate")
|
||||
public ResponseEntity<Void> isAuthenticated(Principal principal) {
|
||||
LOG.debug("REST request to check if the current user is authenticated");
|
||||
return ResponseEntity.status(principal == null ? HttpStatus.UNAUTHORIZED : HttpStatus.NO_CONTENT).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code GET /account} : get the current user.
|
||||
*
|
||||
* @return the current user.
|
||||
* @throws RuntimeException {@code 500 (Internal Server Error)} if the user couldn't be returned.
|
||||
*/
|
||||
@GetMapping("/account")
|
||||
public AdminUserDTO getAccount() {
|
||||
return userService
|
||||
.getUserWithAuthorities()
|
||||
.map(AdminUserDTO::new)
|
||||
.orElseThrow(() -> new AccountResourceException("User could not be found"));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code POST /account} : update the current user information.
|
||||
*
|
||||
* @param userDTO the current user information.
|
||||
* @throws EmailAlreadyUsedException {@code 400 (Bad Request)} if the email is already used.
|
||||
* @throws RuntimeException {@code 500 (Internal Server Error)} if the user login wasn't found.
|
||||
*/
|
||||
@PostMapping("/account")
|
||||
public void saveAccount(@Valid @RequestBody AdminUserDTO userDTO) {
|
||||
String userLogin = SecurityUtils.getCurrentUserLogin().orElseThrow(() ->
|
||||
new AccountResourceException("Current user login not found")
|
||||
);
|
||||
Optional<User> existingUser = userRepository.findOneByEmailIgnoreCase(userDTO.getEmail());
|
||||
if (existingUser.isPresent() && (!existingUser.orElseThrow().getLogin().equalsIgnoreCase(userLogin))) {
|
||||
throw new EmailAlreadyUsedException();
|
||||
}
|
||||
Optional<User> user = userRepository.findOneByLogin(userLogin);
|
||||
if (!user.isPresent()) {
|
||||
throw new AccountResourceException("User could not be found");
|
||||
}
|
||||
userService.updateUser(
|
||||
userDTO.getFirstName(),
|
||||
userDTO.getLastName(),
|
||||
userDTO.getEmail(),
|
||||
userDTO.getLangKey(),
|
||||
userDTO.getImageUrl()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code POST /account/change-password} : changes the current user's password.
|
||||
*
|
||||
* @param passwordChangeDto current and new password.
|
||||
* @throws InvalidPasswordException {@code 400 (Bad Request)} if the new password is incorrect.
|
||||
*/
|
||||
@PostMapping(path = "/account/change-password")
|
||||
public void changePassword(@RequestBody PasswordChangeDTO passwordChangeDto) {
|
||||
if (isPasswordLengthInvalid(passwordChangeDto.getNewPassword())) {
|
||||
throw new InvalidPasswordException();
|
||||
}
|
||||
userService.changePassword(passwordChangeDto.getCurrentPassword(), passwordChangeDto.getNewPassword());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code GET /account/sessions} : get the current open sessions.
|
||||
*
|
||||
* @return the current open sessions.
|
||||
* @throws RuntimeException {@code 500 (Internal Server Error)} if the current open sessions couldn't be retrieved.
|
||||
*/
|
||||
@GetMapping("/account/sessions")
|
||||
public List<PersistentToken> getCurrentSessions() {
|
||||
return persistentTokenRepository.findByUser(
|
||||
userRepository
|
||||
.findOneByLogin(
|
||||
SecurityUtils.getCurrentUserLogin().orElseThrow(() -> new AccountResourceException("Current user login not found"))
|
||||
)
|
||||
.orElseThrow(() -> new AccountResourceException("User could not be found"))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code DELETE /account/sessions?series={series}} : invalidate an existing session.
|
||||
*
|
||||
* - You can only delete your own sessions, not any other user's session
|
||||
* - If you delete one of your existing sessions, and that you are currently logged in on that session, you will
|
||||
* still be able to use that session, until you quit your browser: it does not work in real time (there is
|
||||
* no API for that), it only removes the "remember me" cookie
|
||||
* - This is also true if you invalidate your current session: you will still be able to use it until you close
|
||||
* your browser or that the session times out. But automatic login (the "remember me" cookie) will not work
|
||||
* anymore.
|
||||
* There is an API to invalidate the current session, but there is no API to check which session uses which
|
||||
* cookie.
|
||||
*
|
||||
* @param series the series of an existing session.
|
||||
* @throws IllegalArgumentException if the series couldn't be URL decoded.
|
||||
*/
|
||||
@DeleteMapping("/account/sessions/{series}")
|
||||
public void invalidateSession(@PathVariable("series") String series) {
|
||||
String decodedSeries = URLDecoder.decode(series, StandardCharsets.UTF_8);
|
||||
SecurityUtils.getCurrentUserLogin()
|
||||
.flatMap(userRepository::findOneByLogin)
|
||||
.ifPresent(u ->
|
||||
persistentTokenRepository
|
||||
.findByUser(u)
|
||||
.stream()
|
||||
.filter(persistentToken -> StringUtils.equals(persistentToken.getSeries(), decodedSeries))
|
||||
.findAny()
|
||||
.ifPresent(t -> persistentTokenRepository.deleteById(decodedSeries))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code POST /account/reset-password/init} : Send an email to reset the password of the user.
|
||||
*
|
||||
* @param mail the mail of the user.
|
||||
*/
|
||||
@PostMapping(path = "/account/reset-password/init")
|
||||
public void requestPasswordReset(@RequestBody String mail) {
|
||||
Optional<User> user = userService.requestPasswordReset(mail);
|
||||
if (user.isPresent()) {
|
||||
mailService.sendPasswordResetMail(user.orElseThrow());
|
||||
} else {
|
||||
// Pretend the request has been successful to prevent checking which emails really exist
|
||||
// but log that an invalid attempt has been made
|
||||
LOG.warn("Password reset requested for non existing mail");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code POST /account/reset-password/finish} : Finish to reset the password of the user.
|
||||
*
|
||||
* @param keyAndPassword the generated key and the new password.
|
||||
* @throws InvalidPasswordException {@code 400 (Bad Request)} if the password is incorrect.
|
||||
* @throws RuntimeException {@code 500 (Internal Server Error)} if the password could not be reset.
|
||||
*/
|
||||
@PostMapping(path = "/account/reset-password/finish")
|
||||
public void finishPasswordReset(@RequestBody KeyAndPasswordVM keyAndPassword) {
|
||||
if (isPasswordLengthInvalid(keyAndPassword.getNewPassword())) {
|
||||
throw new InvalidPasswordException();
|
||||
}
|
||||
Optional<User> user = userService.completePasswordReset(keyAndPassword.getNewPassword(), keyAndPassword.getKey());
|
||||
|
||||
if (!user.isPresent()) {
|
||||
throw new AccountResourceException("No user was found for this reset key");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isPasswordLengthInvalid(String password) {
|
||||
return (
|
||||
StringUtils.isEmpty(password) ||
|
||||
password.length() < ManagedUserVM.PASSWORD_MIN_LENGTH ||
|
||||
password.length() > ManagedUserVM.PASSWORD_MAX_LENGTH
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package it.sw.pa.comune.artegna.web.rest;
|
||||
|
||||
import it.sw.pa.comune.artegna.domain.Authority;
|
||||
import it.sw.pa.comune.artegna.repository.AuthorityRepository;
|
||||
import it.sw.pa.comune.artegna.web.rest.errors.BadRequestAlertException;
|
||||
import jakarta.validation.Valid;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import tech.jhipster.web.util.HeaderUtil;
|
||||
import tech.jhipster.web.util.ResponseUtil;
|
||||
|
||||
/**
|
||||
* REST controller for managing {@link it.sw.pa.comune.artegna.domain.Authority}.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/authorities")
|
||||
@Transactional
|
||||
public class AuthorityResource {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AuthorityResource.class);
|
||||
|
||||
private static final String ENTITY_NAME = "adminAuthority";
|
||||
|
||||
@Value("${jhipster.clientApp.name:smartbooking}")
|
||||
private String applicationName;
|
||||
|
||||
private final AuthorityRepository authorityRepository;
|
||||
|
||||
public AuthorityResource(AuthorityRepository authorityRepository) {
|
||||
this.authorityRepository = authorityRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code POST /authorities} : Create a new authority.
|
||||
*
|
||||
* @param authority the authority to create.
|
||||
* @return the {@link ResponseEntity} with status {@code 201 (Created)} and with body the new authority, or with status {@code 400 (Bad Request)} if the authority has already an ID.
|
||||
* @throws URISyntaxException if the Location URI syntax is incorrect.
|
||||
*/
|
||||
@PostMapping("")
|
||||
@PreAuthorize("hasAnyAuthority('ROLE_ADMIN')")
|
||||
public ResponseEntity<Authority> createAuthority(@Valid @RequestBody Authority authority) throws URISyntaxException {
|
||||
LOG.debug("REST request to save Authority : {}", authority);
|
||||
if (authorityRepository.existsById(authority.getName())) {
|
||||
throw new BadRequestAlertException("authority already exists", ENTITY_NAME, "idexists");
|
||||
}
|
||||
authority = authorityRepository.save(authority);
|
||||
return ResponseEntity.created(new URI("/api/authorities/" + authority.getName()))
|
||||
.headers(HeaderUtil.createEntityCreationAlert(applicationName, true, ENTITY_NAME, authority.getName()))
|
||||
.body(authority);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code GET /authorities} : get all the authorities.
|
||||
*
|
||||
* @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of authorities in body.
|
||||
*/
|
||||
@GetMapping("")
|
||||
@PreAuthorize("hasAnyAuthority('ROLE_ADMIN')")
|
||||
public List<Authority> getAllAuthorities() {
|
||||
LOG.debug("REST request to get all Authorities");
|
||||
return authorityRepository.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code GET /authorities/:id} : get the "id" authority.
|
||||
*
|
||||
* @param id the id of the authority to retrieve.
|
||||
* @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the authority, or with status {@code 404 (Not Found)}.
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAnyAuthority('ROLE_ADMIN')")
|
||||
public ResponseEntity<Authority> getAuthority(@PathVariable("id") String id) {
|
||||
LOG.debug("REST request to get Authority : {}", id);
|
||||
Optional<Authority> authority = authorityRepository.findById(id);
|
||||
return ResponseUtil.wrapOrNotFound(authority);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code DELETE /authorities/:id} : delete the "id" authority.
|
||||
*
|
||||
* @param id the id of the authority to delete.
|
||||
* @return the {@link ResponseEntity} with status {@code 204 (NO_CONTENT)}.
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAnyAuthority('ROLE_ADMIN')")
|
||||
public ResponseEntity<Void> deleteAuthority(@PathVariable("id") String id) {
|
||||
LOG.debug("REST request to delete Authority : {}", id);
|
||||
authorityRepository.deleteById(id);
|
||||
return ResponseEntity.noContent().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, id)).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package it.sw.pa.comune.artegna.web.rest;
|
||||
|
||||
import it.sw.pa.comune.artegna.service.UserService;
|
||||
import it.sw.pa.comune.artegna.service.dto.UserDTO;
|
||||
import java.util.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
import tech.jhipster.web.util.PaginationUtil;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class PublicUserResource {
|
||||
|
||||
private static final List<String> ALLOWED_ORDERED_PROPERTIES = Collections.unmodifiableList(
|
||||
Arrays.asList("id", "login", "firstName", "lastName", "email", "activated", "langKey")
|
||||
);
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PublicUserResource.class);
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
public PublicUserResource(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code GET /users} : get all users with only public information - calling this method is allowed for anyone.
|
||||
*
|
||||
* @param pageable the pagination information.
|
||||
* @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body all users.
|
||||
*/
|
||||
@GetMapping("/users")
|
||||
public ResponseEntity<List<UserDTO>> getAllPublicUsers(@org.springdoc.core.annotations.ParameterObject Pageable pageable) {
|
||||
LOG.debug("REST request to get all public User names");
|
||||
if (!onlyContainsAllowedProperties(pageable)) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
final Page<UserDTO> page = userService.getAllPublicUsers(pageable);
|
||||
HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page);
|
||||
return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK);
|
||||
}
|
||||
|
||||
private boolean onlyContainsAllowedProperties(Pageable pageable) {
|
||||
return pageable.getSort().stream().map(Sort.Order::getProperty).allMatch(ALLOWED_ORDERED_PROPERTIES::contains);
|
||||
}
|
||||
}
|
||||
208
src/main/java/it/sw/pa/comune/artegna/web/rest/UserResource.java
Normal file
208
src/main/java/it/sw/pa/comune/artegna/web/rest/UserResource.java
Normal file
@@ -0,0 +1,208 @@
|
||||
package it.sw.pa.comune.artegna.web.rest;
|
||||
|
||||
import it.sw.pa.comune.artegna.config.Constants;
|
||||
import it.sw.pa.comune.artegna.domain.User;
|
||||
import it.sw.pa.comune.artegna.repository.UserRepository;
|
||||
import it.sw.pa.comune.artegna.security.AuthoritiesConstants;
|
||||
import it.sw.pa.comune.artegna.service.MailService;
|
||||
import it.sw.pa.comune.artegna.service.UserService;
|
||||
import it.sw.pa.comune.artegna.service.dto.AdminUserDTO;
|
||||
import it.sw.pa.comune.artegna.web.rest.errors.BadRequestAlertException;
|
||||
import it.sw.pa.comune.artegna.web.rest.errors.EmailAlreadyUsedException;
|
||||
import it.sw.pa.comune.artegna.web.rest.errors.LoginAlreadyUsedException;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
import tech.jhipster.web.util.HeaderUtil;
|
||||
import tech.jhipster.web.util.PaginationUtil;
|
||||
import tech.jhipster.web.util.ResponseUtil;
|
||||
|
||||
/**
|
||||
* REST controller for managing users.
|
||||
* <p>
|
||||
* This class accesses the {@link it.sw.pa.comune.artegna.domain.User} entity, and needs to fetch its collection of authorities.
|
||||
* <p>
|
||||
* For a normal use-case, it would be better to have an eager relationship between User and Authority,
|
||||
* and send everything to the client side: there would be no View Model and DTO, a lot less code, and an outer-join
|
||||
* which would be good for performance.
|
||||
* <p>
|
||||
* We use a View Model and a DTO for 3 reasons:
|
||||
* <ul>
|
||||
* <li>We want to keep a lazy association between the user and the authorities, because people will
|
||||
* quite often do relationships with the user, and we don't want them to get the authorities all
|
||||
* the time for nothing (for performance reasons). This is the #1 goal: we should not impact our users'
|
||||
* application because of this use-case.</li>
|
||||
* <li> Not having an outer join causes n+1 requests to the database. This is not a real issue as
|
||||
* we have by default a second-level cache. This means on the first HTTP call we do the n+1 requests,
|
||||
* but then all authorities come from the cache, so in fact it's much better than doing an outer join
|
||||
* (which will get lots of data from the database, for each HTTP call).</li>
|
||||
* <li> As this manages users, for security reasons, we'd rather have a DTO layer.</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Another option would be to have a specific JPA entity graph to handle this case.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/admin")
|
||||
public class UserResource {
|
||||
|
||||
private static final List<String> ALLOWED_ORDERED_PROPERTIES = Collections.unmodifiableList(
|
||||
Arrays.asList(
|
||||
"id",
|
||||
"login",
|
||||
"firstName",
|
||||
"lastName",
|
||||
"email",
|
||||
"activated",
|
||||
"langKey",
|
||||
"createdBy",
|
||||
"createdDate",
|
||||
"lastModifiedBy",
|
||||
"lastModifiedDate"
|
||||
)
|
||||
);
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UserResource.class);
|
||||
|
||||
@Value("${jhipster.clientApp.name:smartbooking}")
|
||||
private String applicationName;
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
private final MailService mailService;
|
||||
|
||||
public UserResource(UserService userService, UserRepository userRepository, MailService mailService) {
|
||||
this.userService = userService;
|
||||
this.userRepository = userRepository;
|
||||
this.mailService = mailService;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code POST /admin/users} : Creates a new user.
|
||||
* <p>
|
||||
* Creates a new user if the login and email are not already used, and sends a
|
||||
* mail with an activation link.
|
||||
* The user needs to be activated on creation.
|
||||
*
|
||||
* @param userDTO the user to create.
|
||||
* @return the {@link ResponseEntity} with status {@code 201 (Created)} and with body the new user, or with status {@code 400 (Bad Request)} if the login or email is already in use.
|
||||
* @throws URISyntaxException if the Location URI syntax is incorrect.
|
||||
* @throws BadRequestAlertException {@code 400 (Bad Request)} if the login or email is already in use.
|
||||
*/
|
||||
@PostMapping("/users")
|
||||
@PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")")
|
||||
public ResponseEntity<User> createUser(@Valid @RequestBody AdminUserDTO userDTO) throws URISyntaxException {
|
||||
LOG.debug("REST request to save User : {}", userDTO);
|
||||
|
||||
if (userDTO.getId() != null) {
|
||||
throw new BadRequestAlertException("A new user cannot already have an ID", "userManagement", "idexists");
|
||||
// Lowercase the user login before comparing with database
|
||||
} else if (userRepository.findOneByLogin(userDTO.getLogin().toLowerCase()).isPresent()) {
|
||||
throw new LoginAlreadyUsedException();
|
||||
} else if (userRepository.findOneByEmailIgnoreCase(userDTO.getEmail()).isPresent()) {
|
||||
throw new EmailAlreadyUsedException();
|
||||
} else {
|
||||
User newUser = userService.createUser(userDTO);
|
||||
mailService.sendCreationEmail(newUser);
|
||||
return ResponseEntity.created(new URI("/api/admin/users/" + newUser.getLogin()))
|
||||
.headers(HeaderUtil.createAlert(applicationName, "userManagement.created", newUser.getLogin()))
|
||||
.body(newUser);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code PUT /admin/users} : Updates an existing User.
|
||||
*
|
||||
* @param userDTO the user to update.
|
||||
* @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated user.
|
||||
* @throws EmailAlreadyUsedException {@code 400 (Bad Request)} if the email is already in use.
|
||||
* @throws LoginAlreadyUsedException {@code 400 (Bad Request)} if the login is already in use.
|
||||
*/
|
||||
@PutMapping({ "/users", "/users/{login}" })
|
||||
@PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")")
|
||||
public ResponseEntity<AdminUserDTO> updateUser(
|
||||
@PathVariable(name = "login", required = false) @Pattern(regexp = Constants.LOGIN_REGEX) String login,
|
||||
@Valid @RequestBody AdminUserDTO userDTO
|
||||
) {
|
||||
LOG.debug("REST request to update User : {}", userDTO);
|
||||
Optional<User> existingUser = userRepository.findOneByEmailIgnoreCase(userDTO.getEmail());
|
||||
if (existingUser.isPresent() && (!existingUser.orElseThrow().getId().equals(userDTO.getId()))) {
|
||||
throw new EmailAlreadyUsedException();
|
||||
}
|
||||
existingUser = userRepository.findOneByLogin(userDTO.getLogin().toLowerCase());
|
||||
if (existingUser.isPresent() && (!existingUser.orElseThrow().getId().equals(userDTO.getId()))) {
|
||||
throw new LoginAlreadyUsedException();
|
||||
}
|
||||
Optional<AdminUserDTO> updatedUser = userService.updateUser(userDTO);
|
||||
|
||||
return ResponseUtil.wrapOrNotFound(
|
||||
updatedUser,
|
||||
HeaderUtil.createAlert(applicationName, "userManagement.updated", userDTO.getLogin())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code GET /admin/users} : get all users with all the details - calling this are only allowed for the administrators.
|
||||
*
|
||||
* @param pageable the pagination information.
|
||||
* @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body all users.
|
||||
*/
|
||||
@GetMapping("/users")
|
||||
@PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")")
|
||||
public ResponseEntity<List<AdminUserDTO>> getAllUsers(@org.springdoc.core.annotations.ParameterObject Pageable pageable) {
|
||||
LOG.debug("REST request to get all User for an admin");
|
||||
if (!onlyContainsAllowedProperties(pageable)) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
final Page<AdminUserDTO> page = userService.getAllManagedUsers(pageable);
|
||||
HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page);
|
||||
return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK);
|
||||
}
|
||||
|
||||
private boolean onlyContainsAllowedProperties(Pageable pageable) {
|
||||
return pageable.getSort().stream().map(Sort.Order::getProperty).allMatch(ALLOWED_ORDERED_PROPERTIES::contains);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code GET /admin/users/:login} : get the "login" user.
|
||||
*
|
||||
* @param login the login of the user to find.
|
||||
* @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the "login" user, or with status {@code 404 (Not Found)}.
|
||||
*/
|
||||
@GetMapping("/users/{login}")
|
||||
@PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")")
|
||||
public ResponseEntity<AdminUserDTO> getUser(@PathVariable("login") @Pattern(regexp = Constants.LOGIN_REGEX) String login) {
|
||||
LOG.debug("REST request to get User : {}", login);
|
||||
return ResponseUtil.wrapOrNotFound(userService.getUserWithAuthoritiesByLogin(login).map(AdminUserDTO::new));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code DELETE /admin/users/:login} : delete the "login" User.
|
||||
*
|
||||
* @param login the login of the user to delete.
|
||||
* @return the {@link ResponseEntity} with status {@code 204 (NO_CONTENT)}.
|
||||
*/
|
||||
@DeleteMapping("/users/{login}")
|
||||
@PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")")
|
||||
public ResponseEntity<Void> deleteUser(@PathVariable("login") @Pattern(regexp = Constants.LOGIN_REGEX) String login) {
|
||||
LOG.debug("REST request to delete User: {}", login);
|
||||
userService.deleteUser(login);
|
||||
return ResponseEntity.noContent().headers(HeaderUtil.createAlert(applicationName, "userManagement.deleted", login)).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package it.sw.pa.comune.artegna.web.rest.errors;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.net.URI;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.ErrorResponseException;
|
||||
import tech.jhipster.web.rest.errors.ProblemDetailWithCause;
|
||||
import tech.jhipster.web.rest.errors.ProblemDetailWithCause.ProblemDetailWithCauseBuilder;
|
||||
|
||||
@SuppressWarnings("java:S110") // Inheritance tree of classes should not be too deep
|
||||
public class BadRequestAlertException extends ErrorResponseException {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final String entityName;
|
||||
|
||||
private final String errorKey;
|
||||
|
||||
public BadRequestAlertException(String defaultMessage, String entityName, String errorKey) {
|
||||
this(ErrorConstants.DEFAULT_TYPE, defaultMessage, entityName, errorKey);
|
||||
}
|
||||
|
||||
public BadRequestAlertException(URI type, String defaultMessage, String entityName, String errorKey) {
|
||||
super(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
ProblemDetailWithCauseBuilder.instance()
|
||||
.withStatus(HttpStatus.BAD_REQUEST.value())
|
||||
.withType(type)
|
||||
.withTitle(defaultMessage)
|
||||
.withProperty("message", "error." + errorKey)
|
||||
.withProperty("params", entityName)
|
||||
.build(),
|
||||
null
|
||||
);
|
||||
this.entityName = entityName;
|
||||
this.errorKey = errorKey;
|
||||
}
|
||||
|
||||
public String getEntityName() {
|
||||
return entityName;
|
||||
}
|
||||
|
||||
public String getErrorKey() {
|
||||
return errorKey;
|
||||
}
|
||||
|
||||
public ProblemDetailWithCause getProblemDetailWithCause() {
|
||||
return (ProblemDetailWithCause) this.getBody();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package it.sw.pa.comune.artegna.web.rest.errors;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
@SuppressWarnings("java:S110") // Inheritance tree of classes should not be too deep
|
||||
public class EmailAlreadyUsedException extends BadRequestAlertException {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public EmailAlreadyUsedException() {
|
||||
super(ErrorConstants.EMAIL_ALREADY_USED_TYPE, "Email is already in use!", "userManagement", "emailexists");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package it.sw.pa.comune.artegna.web.rest.errors;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
public final class ErrorConstants {
|
||||
|
||||
public static final String ERR_CONCURRENCY_FAILURE = "error.concurrencyFailure";
|
||||
public static final String ERR_VALIDATION = "error.validation";
|
||||
public static final String PROBLEM_BASE_URL = "https://www.jhipster.tech/problem";
|
||||
public static final URI DEFAULT_TYPE = URI.create(PROBLEM_BASE_URL + "/problem-with-message");
|
||||
public static final URI CONSTRAINT_VIOLATION_TYPE = URI.create(PROBLEM_BASE_URL + "/constraint-violation");
|
||||
public static final URI INVALID_PASSWORD_TYPE = URI.create(PROBLEM_BASE_URL + "/invalid-password");
|
||||
public static final URI EMAIL_ALREADY_USED_TYPE = URI.create(PROBLEM_BASE_URL + "/email-already-used");
|
||||
public static final URI LOGIN_ALREADY_USED_TYPE = URI.create(PROBLEM_BASE_URL + "/login-already-used");
|
||||
|
||||
private ErrorConstants() {}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package it.sw.pa.comune.artegna.web.rest.errors;
|
||||
|
||||
import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.dao.ConcurrencyFailureException;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageConversionException;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.web.ErrorResponse;
|
||||
import org.springframework.web.ErrorResponseException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.context.request.WebRequest;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
|
||||
import tech.jhipster.config.JHipsterConstants;
|
||||
import tech.jhipster.web.rest.errors.ProblemDetailWithCause;
|
||||
import tech.jhipster.web.rest.errors.ProblemDetailWithCause.ProblemDetailWithCauseBuilder;
|
||||
import tech.jhipster.web.util.HeaderUtil;
|
||||
|
||||
/**
|
||||
* Controller advice to translate the server side exceptions to client-friendly json structures.
|
||||
* The error response follows RFC7807 - Problem Details for HTTP APIs (https://tools.ietf.org/html/rfc7807).
|
||||
*/
|
||||
@ControllerAdvice
|
||||
public class ExceptionTranslator extends ResponseEntityExceptionHandler {
|
||||
|
||||
private static final String FIELD_ERRORS_KEY = "fieldErrors";
|
||||
private static final String MESSAGE_KEY = "message";
|
||||
private static final String PATH_KEY = "path";
|
||||
private static final boolean CASUAL_CHAIN_ENABLED = false;
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ExceptionTranslator.class);
|
||||
|
||||
@Value("${jhipster.clientApp.name:smartbooking}")
|
||||
private String applicationName;
|
||||
|
||||
private final Environment env;
|
||||
|
||||
public ExceptionTranslator(Environment env) {
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
@ExceptionHandler
|
||||
public ResponseEntity<Object> handleAnyException(Throwable ex, NativeWebRequest request) {
|
||||
LOG.debug("Converting Exception to Problem Details:", ex);
|
||||
ProblemDetailWithCause pdCause = wrapAndCustomizeProblem(ex, request);
|
||||
return handleExceptionInternal((Exception) ex, pdCause, buildHeaders(ex), HttpStatusCode.valueOf(pdCause.getStatus()), request);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected ResponseEntity<Object> handleExceptionInternal(
|
||||
Exception ex,
|
||||
@Nullable Object body,
|
||||
HttpHeaders headers,
|
||||
HttpStatusCode statusCode,
|
||||
WebRequest request
|
||||
) {
|
||||
body = body == null ? wrapAndCustomizeProblem((Throwable) ex, (NativeWebRequest) request) : body;
|
||||
return super.handleExceptionInternal(ex, body, headers, statusCode, request);
|
||||
}
|
||||
|
||||
protected ProblemDetailWithCause wrapAndCustomizeProblem(Throwable ex, NativeWebRequest request) {
|
||||
return customizeProblem(getProblemDetailWithCause(ex), ex, request);
|
||||
}
|
||||
|
||||
private ProblemDetailWithCause getProblemDetailWithCause(Throwable ex) {
|
||||
if (
|
||||
ex instanceof it.sw.pa.comune.artegna.service.UsernameAlreadyUsedException
|
||||
) return (ProblemDetailWithCause) new LoginAlreadyUsedException().getBody();
|
||||
if (
|
||||
ex instanceof it.sw.pa.comune.artegna.service.EmailAlreadyUsedException
|
||||
) return (ProblemDetailWithCause) new EmailAlreadyUsedException().getBody();
|
||||
if (
|
||||
ex instanceof it.sw.pa.comune.artegna.service.InvalidPasswordException
|
||||
) return (ProblemDetailWithCause) new InvalidPasswordException().getBody();
|
||||
|
||||
if (
|
||||
ex instanceof ErrorResponseException exp && exp.getBody() instanceof ProblemDetailWithCause problemDetailWithCause
|
||||
) return problemDetailWithCause;
|
||||
return ProblemDetailWithCauseBuilder.instance().withStatus(toStatus(ex).value()).build();
|
||||
}
|
||||
|
||||
protected ProblemDetailWithCause customizeProblem(ProblemDetailWithCause problem, Throwable err, NativeWebRequest request) {
|
||||
if (problem.getStatus() <= 0) problem.setStatus(toStatus(err));
|
||||
|
||||
if (problem.getType() == null || problem.getType().equals(URI.create("about:blank"))) problem.setType(getMappedType(err));
|
||||
|
||||
// higher precedence to Custom/ResponseStatus types
|
||||
String title = extractTitle(err, problem.getStatus());
|
||||
String problemTitle = problem.getTitle();
|
||||
if (problemTitle == null || !problemTitle.equals(title)) {
|
||||
problem.setTitle(title);
|
||||
}
|
||||
|
||||
if (problem.getDetail() == null) {
|
||||
// higher precedence to cause
|
||||
problem.setDetail(getCustomizedErrorDetails(err));
|
||||
}
|
||||
|
||||
Map<String, Object> problemProperties = problem.getProperties();
|
||||
if (problemProperties == null || !problemProperties.containsKey(MESSAGE_KEY)) problem.setProperty(
|
||||
MESSAGE_KEY,
|
||||
getMappedMessageKey(err) != null ? getMappedMessageKey(err) : "error.http." + problem.getStatus()
|
||||
);
|
||||
|
||||
if (problemProperties == null || !problemProperties.containsKey(PATH_KEY)) problem.setProperty(PATH_KEY, getPathValue(request));
|
||||
|
||||
if (
|
||||
(err instanceof MethodArgumentNotValidException fieldException) &&
|
||||
(problemProperties == null || !problemProperties.containsKey(FIELD_ERRORS_KEY))
|
||||
) problem.setProperty(FIELD_ERRORS_KEY, getFieldErrors(fieldException));
|
||||
|
||||
problem.setCause(buildCause(err.getCause(), request).orElse(null));
|
||||
|
||||
return problem;
|
||||
}
|
||||
|
||||
private String extractTitle(Throwable err, int statusCode) {
|
||||
return getCustomizedTitle(err) != null ? getCustomizedTitle(err) : extractTitleForResponseStatus(err, statusCode);
|
||||
}
|
||||
|
||||
private List<FieldErrorVM> getFieldErrors(MethodArgumentNotValidException ex) {
|
||||
return ex
|
||||
.getBindingResult()
|
||||
.getFieldErrors()
|
||||
.stream()
|
||||
.map(f ->
|
||||
new FieldErrorVM(
|
||||
f.getObjectName().replaceFirst("DTO$", ""),
|
||||
f.getField(),
|
||||
StringUtils.isNotBlank(f.getDefaultMessage()) ? f.getDefaultMessage() : f.getCode()
|
||||
)
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private String extractTitleForResponseStatus(Throwable err, int statusCode) {
|
||||
var specialStatus = extractResponseStatus(err);
|
||||
return specialStatus == null ? HttpStatus.valueOf(statusCode).getReasonPhrase() : specialStatus.reason();
|
||||
}
|
||||
|
||||
private String extractURI(NativeWebRequest request) {
|
||||
HttpServletRequest nativeRequest = request.getNativeRequest(HttpServletRequest.class);
|
||||
return nativeRequest != null ? nativeRequest.getRequestURI() : StringUtils.EMPTY;
|
||||
}
|
||||
|
||||
private HttpStatus toStatus(final Throwable throwable) {
|
||||
// Let the ErrorResponse take this responsibility
|
||||
if (throwable instanceof ErrorResponse err) return HttpStatus.valueOf(err.getBody().getStatus());
|
||||
|
||||
return Optional.ofNullable(getMappedStatus(throwable)).orElse(
|
||||
Optional.ofNullable(resolveResponseStatus(throwable)).map(ResponseStatus::value).orElse(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
);
|
||||
}
|
||||
|
||||
private ResponseStatus extractResponseStatus(final Throwable throwable) {
|
||||
return Optional.ofNullable(resolveResponseStatus(throwable)).orElse(null);
|
||||
}
|
||||
|
||||
private ResponseStatus resolveResponseStatus(final Throwable type) {
|
||||
final ResponseStatus candidate = findMergedAnnotation(type.getClass(), ResponseStatus.class);
|
||||
return candidate == null && type.getCause() != null ? resolveResponseStatus(type.getCause()) : candidate;
|
||||
}
|
||||
|
||||
private URI getMappedType(Throwable err) {
|
||||
if (err instanceof MethodArgumentNotValidException) return ErrorConstants.CONSTRAINT_VIOLATION_TYPE;
|
||||
return ErrorConstants.DEFAULT_TYPE;
|
||||
}
|
||||
|
||||
private String getMappedMessageKey(Throwable err) {
|
||||
if (err instanceof MethodArgumentNotValidException) {
|
||||
return ErrorConstants.ERR_VALIDATION;
|
||||
} else if (err instanceof ConcurrencyFailureException || err.getCause() instanceof ConcurrencyFailureException) {
|
||||
return ErrorConstants.ERR_CONCURRENCY_FAILURE;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getCustomizedTitle(Throwable err) {
|
||||
if (err instanceof MethodArgumentNotValidException) return "Method argument not valid";
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getCustomizedErrorDetails(Throwable err) {
|
||||
Collection<String> activeProfiles = Arrays.asList(env.getActiveProfiles());
|
||||
if (activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_PRODUCTION)) {
|
||||
if (err instanceof HttpMessageConversionException) return "Unable to convert http message";
|
||||
if (err instanceof DataAccessException) return "Failure during data access";
|
||||
if (containsPackageName(err.getMessage())) return "Unexpected runtime exception";
|
||||
}
|
||||
return err.getCause() != null ? err.getCause().getMessage() : err.getMessage();
|
||||
}
|
||||
|
||||
private HttpStatus getMappedStatus(Throwable err) {
|
||||
// Where we disagree with Spring defaults
|
||||
if (err instanceof AccessDeniedException) return HttpStatus.FORBIDDEN;
|
||||
if (err instanceof ConcurrencyFailureException) return HttpStatus.CONFLICT;
|
||||
if (err instanceof BadCredentialsException) return HttpStatus.UNAUTHORIZED;
|
||||
return null;
|
||||
}
|
||||
|
||||
private URI getPathValue(NativeWebRequest request) {
|
||||
if (request == null) return URI.create("about:blank");
|
||||
return URI.create(extractURI(request));
|
||||
}
|
||||
|
||||
private HttpHeaders buildHeaders(Throwable err) {
|
||||
return err instanceof BadRequestAlertException badRequestAlertException
|
||||
? HeaderUtil.createFailureAlert(
|
||||
applicationName,
|
||||
true,
|
||||
badRequestAlertException.getEntityName(),
|
||||
badRequestAlertException.getErrorKey(),
|
||||
badRequestAlertException.getMessage()
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
public Optional<ProblemDetailWithCause> buildCause(final Throwable throwable, NativeWebRequest request) {
|
||||
if (throwable != null && isCasualChainEnabled()) {
|
||||
return Optional.of(customizeProblem(getProblemDetailWithCause(throwable), throwable, request));
|
||||
}
|
||||
return Optional.ofNullable(null);
|
||||
}
|
||||
|
||||
private boolean isCasualChainEnabled() {
|
||||
// Customize as per the needs
|
||||
return CASUAL_CHAIN_ENABLED;
|
||||
}
|
||||
|
||||
private boolean containsPackageName(String message) {
|
||||
// This list is for sure not complete
|
||||
return StringUtils.containsAny(
|
||||
message,
|
||||
"org.",
|
||||
"java.",
|
||||
"net.",
|
||||
"jakarta.",
|
||||
"javax.",
|
||||
"com.",
|
||||
"io.",
|
||||
"de.",
|
||||
"it.sw.pa.comune.artegna"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package it.sw.pa.comune.artegna.web.rest.errors;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
public class FieldErrorVM implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final String objectName;
|
||||
|
||||
private final String field;
|
||||
|
||||
private final String message;
|
||||
|
||||
public FieldErrorVM(String dto, String field, String message) {
|
||||
this.objectName = dto;
|
||||
this.field = field;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getObjectName() {
|
||||
return objectName;
|
||||
}
|
||||
|
||||
public String getField() {
|
||||
return field;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package it.sw.pa.comune.artegna.web.rest.errors;
|
||||
|
||||
import java.io.Serial;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.ErrorResponseException;
|
||||
import tech.jhipster.web.rest.errors.ProblemDetailWithCause.ProblemDetailWithCauseBuilder;
|
||||
|
||||
@SuppressWarnings("java:S110") // Inheritance tree of classes should not be too deep
|
||||
public class InvalidPasswordException extends ErrorResponseException {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public InvalidPasswordException() {
|
||||
super(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
ProblemDetailWithCauseBuilder.instance()
|
||||
.withStatus(HttpStatus.BAD_REQUEST.value())
|
||||
.withType(ErrorConstants.INVALID_PASSWORD_TYPE)
|
||||
.withTitle("Incorrect password")
|
||||
.build(),
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package it.sw.pa.comune.artegna.web.rest.errors;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
@SuppressWarnings("java:S110") // Inheritance tree of classes should not be too deep
|
||||
public class LoginAlreadyUsedException extends BadRequestAlertException {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public LoginAlreadyUsedException() {
|
||||
super(ErrorConstants.LOGIN_ALREADY_USED_TYPE, "Login name already used!", "userManagement", "userexists");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Rest layer error handling.
|
||||
*/
|
||||
package it.sw.pa.comune.artegna.web.rest.errors;
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Rest layer.
|
||||
*/
|
||||
package it.sw.pa.comune.artegna.web.rest;
|
||||
@@ -0,0 +1,27 @@
|
||||
package it.sw.pa.comune.artegna.web.rest.vm;
|
||||
|
||||
/**
|
||||
* View Model object for storing the user's key and password.
|
||||
*/
|
||||
public class KeyAndPasswordVM {
|
||||
|
||||
private String key;
|
||||
|
||||
private String newPassword;
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String getNewPassword() {
|
||||
return newPassword;
|
||||
}
|
||||
|
||||
public void setNewPassword(String newPassword) {
|
||||
this.newPassword = newPassword;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package it.sw.pa.comune.artegna.web.rest.vm;
|
||||
|
||||
import it.sw.pa.comune.artegna.service.dto.AdminUserDTO;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* View Model extending the AdminUserDTO, which is meant to be used in the user management UI.
|
||||
*/
|
||||
public class ManagedUserVM extends AdminUserDTO {
|
||||
|
||||
public static final int PASSWORD_MIN_LENGTH = 4;
|
||||
|
||||
public static final int PASSWORD_MAX_LENGTH = 100;
|
||||
|
||||
@Size(min = PASSWORD_MIN_LENGTH, max = PASSWORD_MAX_LENGTH)
|
||||
private String password;
|
||||
|
||||
public ManagedUserVM() {
|
||||
// Empty constructor needed for Jackson.
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ManagedUserVM{" + super.toString() + "} ";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Rest layer visual models.
|
||||
*/
|
||||
package it.sw.pa.comune.artegna.web.rest.vm;
|
||||
10
src/main/resources/banner.txt
Normal file
10
src/main/resources/banner.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
${AnsiColor.GREEN} ██╗${AnsiColor.BLUE} ██╗ ██╗ ████████╗ ███████╗ ██████╗ ████████╗ ████████╗ ████${AnsiColor.GREEN}███╗
|
||||
${AnsiColor.GREEN} ██║ ██║ ${AnsiColor.BLUE} ██║ ╚══██╔══╝ ██╔═══██╗ ██╔════╝ ╚══██╔══╝ ██╔══${AnsiColor.GREEN}═══╝ ██╔═══██╗
|
||||
${AnsiColor.GREEN} ██║ ████████║${AnsiColor.BLUE} ██║ ███████╔╝ ╚█████╗ ██║ ${AnsiColor.GREEN} ██████╗ ███████╔╝
|
||||
${AnsiColor.GREEN}██╗ ██║ ██╔═══██║ ██║ ${AnsiColor.BLUE} ██╔════╝ ╚═══██╗ ${AnsiColor.GREEN}██║ ██╔═══╝ ██╔══██║
|
||||
${AnsiColor.GREEN}╚██████╔╝ ██║ ██║ ████████╗ ██║ ${AnsiColor.BLUE} ██████${AnsiColor.GREEN}╔╝ ██║ ████████╗ ██║ ╚██╗
|
||||
${AnsiColor.GREEN} ╚═════╝ ╚═╝ ╚═╝ ╚═══════╝ ╚═╝ ${AnsiColor.BLUE} ╚═${AnsiColor.GREEN}════╝ ╚═╝ ╚═══════╝ ╚═╝ ╚═╝
|
||||
|
||||
${AnsiColor.BRIGHT_BLUE}:: JHipster 🤓 :: Running Spring Boot ${spring-boot.version} :: Startup profile(s) ${spring.profiles.active} ::
|
||||
:: https://www.jhipster.tech ::${AnsiColor.DEFAULT}
|
||||
103
src/main/resources/config/application-dev.yml
Normal file
103
src/main/resources/config/application-dev.yml
Normal file
@@ -0,0 +1,103 @@
|
||||
# ===================================================================
|
||||
# Spring Boot configuration for the "dev" profile.
|
||||
#
|
||||
# This configuration overrides the application.yml file.
|
||||
#
|
||||
# More information on profiles: https://www.jhipster.tech/profiles/
|
||||
# More information on configuration properties: https://www.jhipster.tech/common-application-properties/
|
||||
# ===================================================================
|
||||
|
||||
# ===================================================================
|
||||
# Standard Spring Boot properties.
|
||||
# Full reference is available at:
|
||||
# https://docs.spring.io/spring-boot/appendix/application-properties/index.html
|
||||
# ===================================================================
|
||||
|
||||
logging:
|
||||
level:
|
||||
ROOT: DEBUG
|
||||
tech.jhipster: DEBUG
|
||||
org.hibernate.SQL: DEBUG
|
||||
it.sw.pa.comune.artegna: DEBUG
|
||||
|
||||
spring:
|
||||
devtools:
|
||||
restart:
|
||||
enabled: true
|
||||
additional-exclude: static/**
|
||||
livereload:
|
||||
enabled: false # we use Webpack dev server + BrowserSync for livereload
|
||||
jackson:
|
||||
serialization:
|
||||
indent-output: true
|
||||
datasource:
|
||||
type: com.zaxxer.hikari.HikariDataSource
|
||||
url: jdbc:postgresql://localhost:5432/smartbooking
|
||||
username: smartbooking
|
||||
password:
|
||||
hikari:
|
||||
poolName: Hikari
|
||||
auto-commit: false
|
||||
liquibase:
|
||||
# Remove 'faker' if you do not want the sample data to be loaded automatically
|
||||
contexts: dev, faker
|
||||
mail:
|
||||
host: localhost
|
||||
port: 25
|
||||
username:
|
||||
password:
|
||||
messages:
|
||||
cache-duration: PT1S # 1 second, see the ISO 8601 standard
|
||||
thymeleaf:
|
||||
cache: false
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
# make sure requests the proxy uri instead of the server one
|
||||
forward-headers-strategy: native
|
||||
|
||||
# ===================================================================
|
||||
# JHipster specific properties
|
||||
#
|
||||
# Full reference is available at: https://www.jhipster.tech/common-application-properties/
|
||||
# ===================================================================
|
||||
|
||||
jhipster:
|
||||
cache: # Cache configuration
|
||||
ehcache: # Ehcache configuration
|
||||
time-to-live-seconds: 3600 # By default objects stay 1 hour in the cache
|
||||
max-entries: 100 # Number of objects in each cache entry
|
||||
# CORS is only enabled by default with the "dev" profile
|
||||
cors:
|
||||
# Allow Ionic for JHipster by default (* no longer allowed in Spring Boot 2.4+)
|
||||
allowed-origins: 'http://localhost:8100,https://localhost:8100,http://localhost:9000,https://localhost:9000'
|
||||
# Enable CORS when running in GitHub Codespaces
|
||||
allowed-origin-patterns: 'https://*.githubpreview.dev'
|
||||
allowed-methods: '*'
|
||||
allowed-headers: '*'
|
||||
exposed-headers: 'Link,X-Total-Count,X-${jhipster.clientApp.name}-alert,X-${jhipster.clientApp.name}-error,X-${jhipster.clientApp.name}-params'
|
||||
allow-credentials: true
|
||||
max-age: 1800
|
||||
security:
|
||||
remember-me:
|
||||
# security key (this key should be unique for your application, and kept secret)
|
||||
key: c3201ccf39b5d82ef696ad12b95009f33d980fc0468a86e957ad91487d1fe80d9440f72a1d09721585bc96b11048fda07240
|
||||
mail: # specific JHipster mail property, for standard properties see MailProperties
|
||||
base-url: http://127.0.0.1:8080
|
||||
logging:
|
||||
use-json-format: false # By default, logs are not in Json format
|
||||
logstash: # Forward logs to logstash over a socket, used by LoggingConfiguration
|
||||
enabled: false
|
||||
host: localhost
|
||||
port: 5000
|
||||
ring-buffer-size: 512
|
||||
# ===================================================================
|
||||
# Application specific properties
|
||||
# Add your own application properties here, see the ApplicationProperties class
|
||||
# to have type-safe configuration, like in the JHipsterProperties above
|
||||
#
|
||||
# More documentation is available at:
|
||||
# https://www.jhipster.tech/common-application-properties/
|
||||
# ===================================================================
|
||||
|
||||
# application:
|
||||
116
src/main/resources/config/application-prod.yml
Normal file
116
src/main/resources/config/application-prod.yml
Normal file
@@ -0,0 +1,116 @@
|
||||
# ===================================================================
|
||||
# Spring Boot configuration for the "prod" profile.
|
||||
#
|
||||
# This configuration overrides the application.yml file.
|
||||
#
|
||||
# More information on profiles: https://www.jhipster.tech/profiles/
|
||||
# More information on configuration properties: https://www.jhipster.tech/common-application-properties/
|
||||
# ===================================================================
|
||||
|
||||
# ===================================================================
|
||||
# Standard Spring Boot properties.
|
||||
# Full reference is available at:
|
||||
# https://docs.spring.io/spring-boot/appendix/application-properties/index.html
|
||||
# ===================================================================
|
||||
|
||||
logging:
|
||||
level:
|
||||
ROOT: INFO
|
||||
tech.jhipster: INFO
|
||||
it.sw.pa.comune.artegna: INFO
|
||||
|
||||
management:
|
||||
prometheus:
|
||||
metrics:
|
||||
export:
|
||||
enabled: false
|
||||
|
||||
spring:
|
||||
devtools:
|
||||
restart:
|
||||
enabled: false
|
||||
livereload:
|
||||
enabled: false
|
||||
datasource:
|
||||
type: com.zaxxer.hikari.HikariDataSource
|
||||
url: jdbc:postgresql://localhost:5432/smartbooking
|
||||
username: smartbooking
|
||||
password:
|
||||
hikari:
|
||||
poolName: Hikari
|
||||
auto-commit: false
|
||||
# Replace by 'prod, faker' to add the faker context and have sample data loaded in production
|
||||
liquibase:
|
||||
contexts: prod
|
||||
mail:
|
||||
host: localhost
|
||||
port: 25
|
||||
username:
|
||||
password:
|
||||
thymeleaf:
|
||||
cache: true
|
||||
|
||||
# ===================================================================
|
||||
# To enable TLS in production, generate a certificate using:
|
||||
# keytool -genkey -alias smartbooking -storetype PKCS12 -keyalg RSA -keysize 2048 -keystore keystore.p12 -validity 3650
|
||||
#
|
||||
# You can also use Let's Encrypt:
|
||||
# See details in topic "Create a Java Keystore (.JKS) from Let's Encrypt Certificates" on https://maximilian-boehm.com/en-gb/blog
|
||||
#
|
||||
# Then, modify the server.ssl properties so your "server" configuration looks like:
|
||||
#
|
||||
# server:
|
||||
# port: 443
|
||||
# ssl:
|
||||
# key-store: classpath:config/tls/keystore.p12
|
||||
# key-store-password: password
|
||||
# key-store-type: PKCS12
|
||||
# key-alias: selfsigned
|
||||
# # The ciphers suite enforce the security by deactivating some old and deprecated SSL cipher, this list was tested against SSL Labs (https://www.ssllabs.com/ssltest/)
|
||||
# ciphers: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 ,TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 ,TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 ,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,TLS_DHE_RSA_WITH_AES_128_CBC_SHA256,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_CBC_SHA256,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_RSA_WITH_AES_128_GCM_SHA256,TLS_RSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_128_CBC_SHA256,TLS_RSA_WITH_AES_256_CBC_SHA256,TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA,TLS_RSA_WITH_CAMELLIA_256_CBC_SHA,TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA,TLS_RSA_WITH_CAMELLIA_128_CBC_SHA
|
||||
# ===================================================================
|
||||
server:
|
||||
port: 8080
|
||||
shutdown: graceful # see https://docs.spring.io/spring-boot/reference/web/graceful-shutdown.html#web.graceful-shutdown
|
||||
compression:
|
||||
enabled: true
|
||||
mime-types: text/html,text/xml,text/plain,text/css,application/javascript,application/json,image/svg+xml
|
||||
min-response-size: 1024
|
||||
|
||||
# ===================================================================
|
||||
# JHipster specific properties
|
||||
#
|
||||
# Full reference is available at: https://www.jhipster.tech/common-application-properties/
|
||||
# ===================================================================
|
||||
|
||||
jhipster:
|
||||
http:
|
||||
cache: # Used by the CachingHttpHeadersFilter
|
||||
timeToLiveInDays: 1461
|
||||
cache: # Cache configuration
|
||||
ehcache: # Ehcache configuration
|
||||
time-to-live-seconds: 3600 # By default objects stay 1 hour in the cache
|
||||
max-entries: 1000 # Number of objects in each cache entry
|
||||
security:
|
||||
remember-me:
|
||||
# security key (this key should be unique for your application, and kept secret)
|
||||
key: c3201ccf39b5d82ef696ad12b95009f33d980fc0468a86e957ad91487d1fe80d9440f72a1d09721585bc96b11048fda07240
|
||||
mail: # specific JHipster mail property, for standard properties see MailProperties
|
||||
base-url: http://my-server-url-to-change # Modify according to your server's URL
|
||||
logging:
|
||||
use-json-format: false # By default, logs are not in Json format
|
||||
logstash: # Forward logs to logstash over a socket, used by LoggingConfiguration
|
||||
enabled: false
|
||||
host: localhost
|
||||
port: 5000
|
||||
ring-buffer-size: 512
|
||||
# ===================================================================
|
||||
# Application specific properties
|
||||
# Add your own application properties here, see the ApplicationProperties class
|
||||
# to have type-safe configuration, like in the JHipsterProperties above
|
||||
#
|
||||
# More documentation is available at:
|
||||
# https://www.jhipster.tech/common-application-properties/
|
||||
# ===================================================================
|
||||
|
||||
# application:
|
||||
27
src/main/resources/config/application-tls.yml
Normal file
27
src/main/resources/config/application-tls.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
# ===================================================================
|
||||
# Activate this profile to enable TLS and HTTP/2.
|
||||
#
|
||||
# JHipster has generated a self-signed certificate, which will be used to encrypt traffic.
|
||||
# As your browser will not understand this certificate, you will need to import it.
|
||||
#
|
||||
# Another (easiest) solution with Chrome is to enable the "allow-insecure-localhost" flag
|
||||
# at chrome://flags/#allow-insecure-localhost
|
||||
# ===================================================================
|
||||
server:
|
||||
ssl:
|
||||
key-store: classpath:config/tls/keystore.p12
|
||||
key-store-password: password
|
||||
key-store-type: PKCS12
|
||||
key-alias: selfsigned
|
||||
ciphers:
|
||||
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
|
||||
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
|
||||
- TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
|
||||
- TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
|
||||
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
|
||||
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
|
||||
- TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
|
||||
- TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
|
||||
enabled-protocols: TLSv1.2
|
||||
http2:
|
||||
enabled: true
|
||||
224
src/main/resources/config/application.yml
Normal file
224
src/main/resources/config/application.yml
Normal file
@@ -0,0 +1,224 @@
|
||||
# ===================================================================
|
||||
# Spring Boot configuration.
|
||||
#
|
||||
# This configuration will be overridden by the Spring profile you use,
|
||||
# for example application-dev.yml if you use the "dev" profile.
|
||||
#
|
||||
# More information on profiles: https://www.jhipster.tech/profiles/
|
||||
# More information on configuration properties: https://www.jhipster.tech/common-application-properties/
|
||||
# ===================================================================
|
||||
|
||||
# ===================================================================
|
||||
# Standard Spring Boot properties.
|
||||
# Full reference is available at:
|
||||
# https://docs.spring.io/spring-boot/appendix/application-properties/index.html
|
||||
# ===================================================================
|
||||
|
||||
---
|
||||
# Conditionally disable springdoc on missing api-docs profile
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: '!api-docs'
|
||||
springdoc:
|
||||
api-docs:
|
||||
enabled: false
|
||||
---
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
base-path: /management
|
||||
exposure:
|
||||
include:
|
||||
- configprops
|
||||
- env
|
||||
- health
|
||||
- info
|
||||
- jhimetrics
|
||||
- jhiopenapigroups
|
||||
- logfile
|
||||
- loggers
|
||||
- prometheus
|
||||
- threaddump
|
||||
- caches
|
||||
- liquibase
|
||||
endpoint:
|
||||
health:
|
||||
show-details: when_authorized
|
||||
roles: 'ROLE_ADMIN'
|
||||
probes:
|
||||
enabled: true
|
||||
group:
|
||||
liveness:
|
||||
include: livenessState
|
||||
readiness:
|
||||
include: readinessState,db
|
||||
jhimetrics:
|
||||
access: read-only
|
||||
info:
|
||||
git:
|
||||
mode: full
|
||||
env:
|
||||
enabled: true
|
||||
health:
|
||||
mail:
|
||||
enabled: false # When using the MailService, configure an SMTP server and set this to true
|
||||
prometheus:
|
||||
metrics:
|
||||
export:
|
||||
enabled: true
|
||||
step: 60
|
||||
observations:
|
||||
key-values:
|
||||
application: ${spring.application.name}
|
||||
metrics:
|
||||
enable:
|
||||
http: true
|
||||
jvm: true
|
||||
logback: true
|
||||
process: true
|
||||
system: true
|
||||
distribution:
|
||||
percentiles-histogram:
|
||||
all: true
|
||||
percentiles:
|
||||
all: 0, 0.5, 0.75, 0.95, 0.99, 1.0
|
||||
data:
|
||||
repository:
|
||||
autotime:
|
||||
enabled: true
|
||||
tags:
|
||||
application: ${spring.application.name}
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: smartbooking
|
||||
docker:
|
||||
compose:
|
||||
enabled: true
|
||||
lifecycle-management: start-only
|
||||
file: src/main/docker/services.yml
|
||||
profiles:
|
||||
# The commented value for `active` can be replaced with valid Spring profiles to load.
|
||||
# Otherwise, it will be filled in by gradle when building the JAR file
|
||||
# Either way, it can be overridden by `--spring.profiles.active` value passed in the commandline or `-Dspring.profiles.active` set in `JAVA_OPTS`
|
||||
active: '@spring.profiles.active@'
|
||||
group:
|
||||
dev:
|
||||
- dev
|
||||
- api-docs
|
||||
# Uncomment to activate TLS for the dev profile
|
||||
#- tls
|
||||
jmx:
|
||||
enabled: false
|
||||
data:
|
||||
jpa:
|
||||
repositories:
|
||||
bootstrap-mode: deferred
|
||||
jpa:
|
||||
open-in-view: false
|
||||
properties:
|
||||
hibernate.jdbc.time_zone: UTC
|
||||
hibernate.timezone.default_storage: NORMALIZE
|
||||
hibernate.type.preferred_instant_jdbc_type: TIMESTAMP
|
||||
hibernate.id.new_generator_mappings: true
|
||||
hibernate.connection.provider_disables_autocommit: true
|
||||
hibernate.cache.use_second_level_cache: true
|
||||
hibernate.cache.use_query_cache: false
|
||||
hibernate.generate_statistics: false
|
||||
# modify batch size as necessary
|
||||
hibernate.jdbc.batch_size: 25
|
||||
hibernate.order_inserts: true
|
||||
hibernate.order_updates: true
|
||||
hibernate.query.fail_on_pagination_over_collection_fetch: true
|
||||
hibernate.query.in_clause_parameter_padding: true
|
||||
hibernate:
|
||||
ddl-auto: none
|
||||
naming:
|
||||
physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
|
||||
implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
|
||||
messages:
|
||||
basename: i18n/messages
|
||||
main:
|
||||
allow-bean-definition-overriding: true
|
||||
mvc:
|
||||
problemdetails:
|
||||
enabled: true
|
||||
task:
|
||||
execution:
|
||||
thread-name-prefix: smartbooking-task-
|
||||
pool:
|
||||
core-size: 2
|
||||
max-size: 50
|
||||
queue-capacity: 10000
|
||||
scheduling:
|
||||
thread-name-prefix: smartbooking-scheduling-
|
||||
pool:
|
||||
size: 2
|
||||
thymeleaf:
|
||||
mode: HTML
|
||||
output:
|
||||
ansi:
|
||||
console-available: true
|
||||
|
||||
server:
|
||||
servlet:
|
||||
session:
|
||||
cookie:
|
||||
http-only: true
|
||||
|
||||
springdoc:
|
||||
show-actuator: true
|
||||
|
||||
# Properties to be exposed on the /info management endpoint
|
||||
info:
|
||||
# Comma separated list of profiles that will trigger the ribbon to show
|
||||
display-ribbon-on-profiles: 'dev'
|
||||
|
||||
# ===================================================================
|
||||
# JHipster specific properties
|
||||
#
|
||||
# Full reference is available at: https://www.jhipster.tech/common-application-properties/
|
||||
# ===================================================================
|
||||
|
||||
jhipster:
|
||||
clientApp:
|
||||
name: 'smartbookingApp'
|
||||
# By default CORS is disabled. Uncomment to enable.
|
||||
# cors:
|
||||
# allowed-origins: "http://localhost:8100,http://localhost:9000"
|
||||
# allowed-methods: "*"
|
||||
# allowed-headers: "*"
|
||||
# exposed-headers: "Link,X-Total-Count,X-${jhipster.clientApp.name}-alert,X-${jhipster.clientApp.name}-error,X-${jhipster.clientApp.name}-params"
|
||||
# allow-credentials: true
|
||||
# max-age: 1800
|
||||
mail:
|
||||
from: smartbooking@localhost
|
||||
api-docs:
|
||||
default-include-pattern: /api/**
|
||||
management-include-pattern: /management/**
|
||||
title: Smartbooking API
|
||||
description: Smartbooking API documentation
|
||||
version: 0.0.1
|
||||
terms-of-service-url:
|
||||
contact-name:
|
||||
contact-url:
|
||||
contact-email:
|
||||
license: unlicensed
|
||||
license-url:
|
||||
security:
|
||||
content-security-policy: "default-src 'self'; frame-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://storage.googleapis.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:"
|
||||
|
||||
|
||||
# jhipster-needle-add-application-yaml-document
|
||||
---
|
||||
# ===================================================================
|
||||
# Application specific properties
|
||||
# Add your own application properties here, see the ApplicationProperties class
|
||||
# to have type-safe configuration, like in the JHipsterProperties above
|
||||
#
|
||||
# More documentation is available at:
|
||||
# https://www.jhipster.tech/common-application-properties/
|
||||
# ===================================================================
|
||||
|
||||
# application:
|
||||
@@ -0,0 +1,140 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd
|
||||
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
|
||||
|
||||
<changeSet id="00000000000000" author="jhipster">
|
||||
<createSequence sequenceName="sequence_generator" startValue="1050" incrementBy="50"/>
|
||||
</changeSet>
|
||||
|
||||
<!--
|
||||
JHipster core tables.
|
||||
The initial schema has the '00000000000001' id, so that it is over-written if we re-generate it.
|
||||
-->
|
||||
<changeSet id="00000000000001" author="jhipster">
|
||||
<createTable tableName="jhi_user">
|
||||
<column name="id" type="bigint">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
<column name="login" type="varchar(50)">
|
||||
<constraints unique="true" nullable="false" uniqueConstraintName="ux_user_login"/>
|
||||
</column>
|
||||
<column name="password_hash" type="varchar(60)"/>
|
||||
<column name="first_name" type="varchar(50)"/>
|
||||
<column name="last_name" type="varchar(50)"/>
|
||||
<column name="email" type="varchar(191)">
|
||||
<constraints unique="true" nullable="true" uniqueConstraintName="ux_user_email"/>
|
||||
</column>
|
||||
<column name="image_url" type="varchar(256)"/>
|
||||
<column name="activated" type="boolean" valueBoolean="false">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="lang_key" type="varchar(10)"/>
|
||||
<column name="activation_key" type="varchar(20)"/>
|
||||
<column name="reset_key" type="varchar(20)"/>
|
||||
<column name="created_by" type="varchar(50)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_date" type="timestamp"/>
|
||||
<column name="reset_date" type="timestamp">
|
||||
<constraints nullable="true"/>
|
||||
</column>
|
||||
<column name="last_modified_by" type="varchar(50)"/>
|
||||
<column name="last_modified_date" type="timestamp"/>
|
||||
</createTable>
|
||||
|
||||
<createTable tableName="jhi_authority">
|
||||
<column name="name" type="varchar(50)">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
|
||||
<createTable tableName="jhi_user_authority">
|
||||
<column name="user_id" type="bigint">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="authority_name" type="varchar(50)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
|
||||
<addPrimaryKey columnNames="user_id, authority_name" tableName="jhi_user_authority"/>
|
||||
|
||||
<createTable tableName="jhi_persistent_token">
|
||||
<column name="series" type="varchar(20)">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
<column name="user_id" type="bigint"/>
|
||||
<column name="token_value" type="varchar(20)">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="token_date" type="date"/>
|
||||
<column name="ip_address" type="varchar(39)"/>
|
||||
<column name="user_agent" type="varchar(255)"/>
|
||||
</createTable>
|
||||
|
||||
<addForeignKeyConstraint baseColumnNames="authority_name"
|
||||
baseTableName="jhi_user_authority"
|
||||
constraintName="fk_authority_name"
|
||||
referencedColumnNames="name"
|
||||
referencedTableName="jhi_authority"/>
|
||||
|
||||
<addForeignKeyConstraint baseColumnNames="user_id"
|
||||
baseTableName="jhi_user_authority"
|
||||
constraintName="fk_user_id"
|
||||
referencedColumnNames="id"
|
||||
referencedTableName="jhi_user"/>
|
||||
|
||||
<addNotNullConstraint columnName="password_hash"
|
||||
columnDataType="varchar(60)"
|
||||
tableName="jhi_user"/>
|
||||
|
||||
<addForeignKeyConstraint baseColumnNames="user_id"
|
||||
baseTableName="jhi_persistent_token"
|
||||
constraintName="fk_user_persistent_token"
|
||||
referencedColumnNames="id"
|
||||
referencedTableName="jhi_user"/>
|
||||
<loadData
|
||||
file="config/liquibase/data/authority.csv"
|
||||
separator=";"
|
||||
tableName="jhi_authority"
|
||||
usePreparedStatements="true">
|
||||
<column name="name" type="string"/>
|
||||
</loadData>
|
||||
<loadData
|
||||
file="config/liquibase/data/user.csv"
|
||||
separator=";"
|
||||
tableName="jhi_user"
|
||||
usePreparedStatements="true">
|
||||
<column name="id" type="numeric"/>
|
||||
<column name="activated" type="boolean"/>
|
||||
<column name="created_date" type="timestamp"/>
|
||||
</loadData>
|
||||
<loadData
|
||||
file="config/liquibase/data/user_authority.csv"
|
||||
separator=";"
|
||||
tableName="jhi_user_authority"
|
||||
usePreparedStatements="true">
|
||||
<column name="user_id" type="numeric"/>
|
||||
</loadData>
|
||||
<dropDefaultValue tableName="jhi_user" columnName="created_date" columnDataType="${datetimeType}"/>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="jhipster" id="00000000000002" context="test">
|
||||
<createTable tableName="jhi_date_time_wrapper">
|
||||
<column name="id" type="BIGINT">
|
||||
<constraints primaryKey="true" primaryKeyName="jhi_date_time_wrapperPK"/>
|
||||
</column>
|
||||
<column name="instant" type="timestamp"/>
|
||||
<column name="local_date_time" type="timestamp"/>
|
||||
<column name="offset_date_time" type="timestamp"/>
|
||||
<column name="zoned_date_time" type="timestamp"/>
|
||||
<column name="local_time" type="time"/>
|
||||
<column name="offset_time" type="time"/>
|
||||
<column name="local_date" type="date"/>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
3
src/main/resources/config/liquibase/data/authority.csv
Normal file
3
src/main/resources/config/liquibase/data/authority.csv
Normal file
@@ -0,0 +1,3 @@
|
||||
name
|
||||
ROLE_ADMIN
|
||||
ROLE_USER
|
||||
|
3
src/main/resources/config/liquibase/data/user.csv
Normal file
3
src/main/resources/config/liquibase/data/user.csv
Normal file
@@ -0,0 +1,3 @@
|
||||
id;login;password_hash;first_name;last_name;email;image_url;activated;lang_key;created_by;last_modified_by
|
||||
1;admin;$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC;Administrator;Administrator;admin@localhost;;true;it;system;system
|
||||
2;user;$2a$10$VEjxo0jq2YG9Rbk2HmX9S.k1uZBGYUHdUcid3g/vfiEl7lwWgOH/K;User;User;user@localhost;;true;it;system;system
|
||||
|
@@ -0,0 +1,4 @@
|
||||
user_id;authority_name
|
||||
1;ROLE_ADMIN
|
||||
1;ROLE_USER
|
||||
2;ROLE_USER
|
||||
|
18
src/main/resources/config/liquibase/master.xml
Normal file
18
src/main/resources/config/liquibase/master.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
||||
<property name="now" value="current_timestamp" dbms="postgresql"/>
|
||||
<property name="floatType" value="float4" dbms="postgresql"/>
|
||||
<property name="clobType" value="clob" dbms="postgresql"/>
|
||||
<property name="blobType" value="blob" dbms="postgresql"/>
|
||||
<property name="uuidType" value="uuid" dbms="postgresql"/>
|
||||
<property name="datetimeType" value="datetime" dbms="postgresql"/>
|
||||
<property name="timeType" value="time(6)" dbms="postgresql"/>
|
||||
|
||||
<include file="config/liquibase/changelog/00000000000000_initial_schema.xml" relativeToChangelogFile="false"/>
|
||||
<!-- jhipster-needle-liquibase-add-changelog - JHipster will add liquibase changelogs here -->
|
||||
<!-- jhipster-needle-liquibase-add-constraints-changelog - JHipster will add liquibase constraints changelogs here -->
|
||||
<!-- jhipster-needle-liquibase-add-incremental-changelog - JHipster will add incremental liquibase changelogs here -->
|
||||
</databaseChangeLog>
|
||||
BIN
src/main/resources/config/tls/keystore.p12
Normal file
BIN
src/main/resources/config/tls/keystore.p12
Normal file
Binary file not shown.
21
src/main/resources/i18n/messages.properties
Normal file
21
src/main/resources/i18n/messages.properties
Normal file
@@ -0,0 +1,21 @@
|
||||
# Error page
|
||||
error.title=Your request cannot be processed
|
||||
error.subtitle=Sorry, an error has occurred.
|
||||
error.status=Status:
|
||||
error.message=Message:
|
||||
|
||||
# Activation email
|
||||
email.activation.title=smartbooking account activation
|
||||
email.activation.greeting=Dear {0}
|
||||
email.activation.text1=Your smartbooking account has been created, please click on the URL below to activate it:
|
||||
email.activation.text2=Regards,
|
||||
email.signature=smartbooking Team.
|
||||
|
||||
# Creation email
|
||||
email.creation.text1=Your smartbooking account has been created, please click on the URL below to access it:
|
||||
|
||||
# Reset email
|
||||
email.reset.title=smartbooking password reset
|
||||
email.reset.greeting=Dear {0}
|
||||
email.reset.text1=For your smartbooking account a password reset was requested, please click on the URL below to reset it:
|
||||
email.reset.text2=Regards,
|
||||
21
src/main/resources/i18n/messages_it.properties
Normal file
21
src/main/resources/i18n/messages_it.properties
Normal file
@@ -0,0 +1,21 @@
|
||||
# Error page
|
||||
error.title=La tua richiesta non può essere processata
|
||||
error.subtitle= E' stato riscontrato un errore.
|
||||
error.status=Stato:
|
||||
error.message=Messaggio:
|
||||
|
||||
# Activation email
|
||||
email.activation.title=smartbooking attivazione
|
||||
email.activation.greeting=Caro {0}
|
||||
email.activation.text1=Il tuo account smartbooking è stato creato, clicca sul link qui sotto per attivare:
|
||||
email.activation.text2=Saluti,
|
||||
email.signature=smartbooking.
|
||||
|
||||
# Creation email
|
||||
email.creation.text1=L'account smartbooking è stato attivato, clicca sul link qui sotto per accedere:
|
||||
|
||||
# Reset email
|
||||
email.reset.title=smartbooking reimpostazione della password
|
||||
email.reset.greeting=Caro {0}
|
||||
email.reset.text1=E' stata richiesta una reimpostazione della password per il tuo account smartbooking, clicca sul seguente URL per ripristinarlo:
|
||||
email.reset.text2=Saluti,
|
||||
80
src/main/resources/logback-spring.xml
Normal file
80
src/main/resources/logback-spring.xml
Normal file
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE configuration>
|
||||
|
||||
<configuration scan="true">
|
||||
<!-- Patterns based on https://github.com/spring-projects/spring-boot/blob/v3.0.0/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/logback/defaults.xml -->
|
||||
<conversionRule conversionWord="crlf" converterClass="it.sw.pa.comune.artegna.config.CRLFLogConverter" />
|
||||
<property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd'T'HH:mm:ss.SSSXXX}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %crlf(%m){red} %n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
|
||||
<!-- The FILE and ASYNC appenders are here as examples for a production configuration -->
|
||||
<!--
|
||||
<property name="FILE_LOG_PATTERN" value="${FILE_LOG_PATTERN:-%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd'T'HH:mm:ss.SSSXXX}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-40.40logger{39} : %crlf(%m) %n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
|
||||
-->
|
||||
|
||||
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
|
||||
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
|
||||
|
||||
<!-- The FILE and ASYNC appenders are here as examples for a production configuration -->
|
||||
<!--
|
||||
<include resource="org/springframework/boot/logging/logback/file-appender.xml" />
|
||||
|
||||
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
|
||||
<queueSize>512</queueSize>
|
||||
<appender-ref ref="FILE"/>
|
||||
</appender>
|
||||
|
||||
<root level="${logging.level.root}">
|
||||
<appender-ref ref="ASYNC"/>
|
||||
</root>
|
||||
-->
|
||||
|
||||
<logger name="it.sw.pa.comune.artegna" level="INFO"/>
|
||||
|
||||
<logger name="angus.activation" level="WARN"/>
|
||||
<logger name="jakarta.activation" level="WARN"/>
|
||||
<logger name="jakarta.mail" level="WARN"/>
|
||||
<logger name="jakarta.management.remote" level="WARN"/>
|
||||
<logger name="jakarta.xml.bind" level="WARN"/>
|
||||
<logger name="jdk.event.security" level="INFO"/>
|
||||
<logger name="com.ryantenney" level="WARN"/>
|
||||
<logger name="com.sun" level="WARN"/>
|
||||
<logger name="com.zaxxer" level="WARN"/>
|
||||
<logger name="org.ehcache" level="WARN"/>
|
||||
<logger name="org.apache" level="WARN"/>
|
||||
<logger name="org.apache.catalina.startup.DigesterFactory" level="OFF"/>
|
||||
<logger name="org.bson" level="WARN"/>
|
||||
<logger name="org.hibernate.validator" level="WARN"/>
|
||||
<logger name="org.hibernate" level="WARN"/>
|
||||
<logger name="org.hibernate.ejb.HibernatePersistence" level="OFF"/>
|
||||
<logger name="org.postgresql" level="WARN"/>
|
||||
<logger name="org.springframework" level="WARN"/>
|
||||
<logger name="org.springframework.web" level="WARN"/>
|
||||
<logger name="org.springframework.security" level="WARN"/>
|
||||
<logger name="org.springframework.boot.autoconfigure.logging" level="INFO"/>
|
||||
<logger name="org.springframework.cache" level="WARN"/>
|
||||
<logger name="org.thymeleaf" level="WARN"/>
|
||||
<logger name="org.xnio" level="WARN"/>
|
||||
<logger name="io.swagger.v3" level="INFO"/>
|
||||
<logger name="sun.rmi" level="WARN"/>
|
||||
<logger name="sun.rmi.transport" level="WARN"/>
|
||||
<!-- See https://github.com/jhipster/generator-jhipster/issues/13835 -->
|
||||
<logger name="Validator" level="INFO"/>
|
||||
<!-- See https://github.com/jhipster/generator-jhipster/issues/14444 -->
|
||||
<logger name="_org.springframework.web.servlet.HandlerMapping.Mappings" level="INFO"/>
|
||||
<logger name="org.springframework.boot.docker" level="WARN"/>
|
||||
<logger name="liquibase" level="WARN"/>
|
||||
<logger name="LiquibaseSchemaResolver" level="INFO"/>
|
||||
<!-- jhipster-needle-logback-add-log - JHipster will add a new log with level -->
|
||||
|
||||
<springProperty name="log.level" source="logging.level.root" defaultValue="INFO" />
|
||||
<root level="${log.level}">
|
||||
<appender-ref ref="CONSOLE" />
|
||||
</root>
|
||||
|
||||
<!-- Prevent logback from outputting its own status -->
|
||||
<statusListener class="ch.qos.logback.core.status.NopStatusListener"/>
|
||||
<!-- Reset the JUL logging level to avoid conflicts with Logback -->
|
||||
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
|
||||
<resetJUL>true</resetJUL>
|
||||
</contextListener>
|
||||
|
||||
</configuration>
|
||||
94
src/main/resources/templates/error.html
Normal file
94
src/main/resources/templates/error.html
Normal file
@@ -0,0 +1,94 @@
|
||||
<!doctype html>
|
||||
<html
|
||||
xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.thymeleaf.org"
|
||||
th:lang="${#locale.language}"
|
||||
lang="en"
|
||||
>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<link rel="icon" href="${baseUrl}/favicon.ico" />
|
||||
<title>Your request cannot be processed</title>
|
||||
<style>
|
||||
::-moz-selection {
|
||||
background: #b3d4fc;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: #b3d4fc;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
html {
|
||||
padding: 30px 10px;
|
||||
font-size: 20px;
|
||||
line-height: 1.4;
|
||||
background: #3e8acc;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
html,
|
||||
input {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
max-width: 1000px;
|
||||
_width: 500px;
|
||||
padding: 30px 20px 50px;
|
||||
border: 1px solid #b3b3b3;
|
||||
border-radius: 4px;
|
||||
margin: 0 auto;
|
||||
box-shadow:
|
||||
0 1px 10px #a7a7a7,
|
||||
inset 0 1px 0 #fff;
|
||||
background: #fcfcfc;
|
||||
color: #3d3d3d;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 10px;
|
||||
font-size: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 span {
|
||||
color: #575757;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 1.5em 0 0.5em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0 0 0 40px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
_width: 380px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 th:text="#{error.title}">Your request cannot be processed <span>:(</span></h1>
|
||||
|
||||
<p th:text="#{error.subtitle}">Sorry, an error has occurred.</p>
|
||||
|
||||
<span th:text="#{error.status}">Status:</span> <span th:text="${error}"></span> (<span th:text="${error}"></span>)<br />
|
||||
<span th:if="${!#strings.isEmpty(message)}">
|
||||
<span th:text="#{error.message}">Message:</span> <span th:text="${message}"></span><br />
|
||||
</span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
20
src/main/resources/templates/mail/activationEmail.html
Normal file
20
src/main/resources/templates/mail/activationEmail.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<!doctype html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" th:lang="${#locale.language}" lang="en">
|
||||
<head>
|
||||
<title th:text="#{email.activation.title}">JHipster activation</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<link rel="icon" th:href="@{|${baseUrl}/favicon.ico|}" />
|
||||
</head>
|
||||
<body>
|
||||
<p th:text="#{email.activation.greeting(${user.login})}">Dear</p>
|
||||
<p th:text="#{email.activation.text1}">Your JHipster account has been created, please click on the URL below to activate it:</p>
|
||||
<p>
|
||||
<a th:with="url=(@{|${baseUrl}/account/activate?key=${user.activationKey}|})" th:href="${url}" th:text="${url}">Activation link</a>
|
||||
</p>
|
||||
<p>
|
||||
<span th:text="#{email.activation.text2}">Regards, </span>
|
||||
<br />
|
||||
<em th:text="#{email.signature}">JHipster.</em>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
20
src/main/resources/templates/mail/creationEmail.html
Normal file
20
src/main/resources/templates/mail/creationEmail.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<!doctype html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" th:lang="${#locale.language}" lang="en">
|
||||
<head>
|
||||
<title th:text="#{email.activation.title}">JHipster creation</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<link rel="icon" th:href="@{|${baseUrl}/favicon.ico|}" />
|
||||
</head>
|
||||
<body>
|
||||
<p th:text="#{email.activation.greeting(${user.login})}">Dear</p>
|
||||
<p th:text="#{email.creation.text1}">Your JHipster account has been created, please click on the URL below to access it:</p>
|
||||
<p>
|
||||
<a th:with="url=(@{|${baseUrl}/account/reset/finish?key=${user.resetKey}|})" th:href="${url}" th:text="${url}">Login link</a>
|
||||
</p>
|
||||
<p>
|
||||
<span th:text="#{email.activation.text2}">Regards, </span>
|
||||
<br />
|
||||
<em th:text="#{email.signature}">JHipster.</em>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
22
src/main/resources/templates/mail/passwordResetEmail.html
Normal file
22
src/main/resources/templates/mail/passwordResetEmail.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" th:lang="${#locale.language}" lang="en">
|
||||
<head>
|
||||
<title th:text="#{email.reset.title}">JHipster password reset</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<link rel="icon" th:href="@{|${baseUrl}/favicon.ico|}" />
|
||||
</head>
|
||||
<body>
|
||||
<p th:text="#{email.reset.greeting(${user.login})}">Dear</p>
|
||||
<p th:text="#{email.reset.text1}">
|
||||
For your JHipster account a password reset was requested, please click on the URL below to reset it:
|
||||
</p>
|
||||
<p>
|
||||
<a th:with="url=(@{|${baseUrl}/account/reset/finish?key=${user.resetKey}|})" th:href="${url}" th:text="${url}">Login link</a>
|
||||
</p>
|
||||
<p>
|
||||
<span th:text="#{email.reset.text2}">Regards, </span>
|
||||
<br />
|
||||
<em th:text="#{email.signature}">JHipster.</em>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
58
src/main/webapp/404.html
Normal file
58
src/main/webapp/404.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Page Not Found</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="favicon.ico" />
|
||||
<style>
|
||||
* {
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
color: #888;
|
||||
display: table;
|
||||
font-family: sans-serif;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
margin: 2em auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #555;
|
||||
font-size: 2em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 auto;
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 280px) {
|
||||
body,
|
||||
p {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
margin: 0 0 0.3em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Page Not Found</h1>
|
||||
<p>Sorry, but the page you were trying to view does not exist.</p>
|
||||
</body>
|
||||
</html>
|
||||
<!-- IE needs 512+ bytes: http://blogs.msdn.com/b/ieinternals/archive/2010/08/19/http-error-pages-in-internet-explorer.aspx -->
|
||||
13
src/main/webapp/WEB-INF/web.xml
Normal file
13
src/main/webapp/WEB-INF/web.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<web-app
|
||||
xmlns="http://java.sun.com/xml/ns/javaee"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
|
||||
version="3.0">
|
||||
|
||||
<mime-mapping>
|
||||
<extension>html</extension>
|
||||
<mime-type>text/html;charset=utf-8</mime-type>
|
||||
</mime-mapping>
|
||||
|
||||
</web-app>
|
||||
110
src/main/webapp/app/account/account.service.spec.ts
Normal file
110
src/main/webapp/app/account/account.service.spec.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import axios from 'axios';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import { type AccountStore, useStore } from '@/store';
|
||||
|
||||
import AccountService from './account.service';
|
||||
|
||||
const resetStore = (store: AccountStore) => {
|
||||
store.$reset();
|
||||
};
|
||||
|
||||
const axiosStub = {
|
||||
get: sinon.stub(axios, 'get'),
|
||||
post: sinon.stub(axios, 'post'),
|
||||
};
|
||||
|
||||
createTestingPinia({ stubActions: false });
|
||||
const store = useStore();
|
||||
|
||||
describe('Account Service test suite', () => {
|
||||
let accountService: AccountService;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
|
||||
axiosStub.get.reset();
|
||||
resetStore(store);
|
||||
});
|
||||
|
||||
it('should init service and do not retrieve account', async () => {
|
||||
axiosStub.get.resolves({});
|
||||
axiosStub.get
|
||||
.withArgs('management/info')
|
||||
.resolves({ status: 200, data: { 'display-ribbon-on-profiles': 'dev', activeProfiles: ['dev', 'test'] } });
|
||||
|
||||
accountService = new AccountService(store);
|
||||
await accountService.update();
|
||||
|
||||
expect(store.logon).toBe(null);
|
||||
expect(accountService.authenticated).toBe(false);
|
||||
expect(store.account).toBe(null);
|
||||
expect(axiosStub.get.calledWith('management/info')).toBeTruthy();
|
||||
expect(store.activeProfiles[0]).toBe('dev');
|
||||
expect(store.activeProfiles[1]).toBe('test');
|
||||
expect(store.ribbonOnProfiles).toBe('dev');
|
||||
});
|
||||
|
||||
it('should init service and retrieve profiles if already logged in before but no account found', async () => {
|
||||
axiosStub.get.resolves({});
|
||||
accountService = new AccountService(store);
|
||||
await accountService.update();
|
||||
|
||||
expect(store.logon).toBe(null);
|
||||
expect(accountService.authenticated).toBe(false);
|
||||
expect(store.account).toBe(null);
|
||||
expect(axiosStub.get.calledWith('management/info')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should init service and retrieve profiles if already logged in before but exception occurred and should be logged out', async () => {
|
||||
axiosStub.get.resolves({});
|
||||
axiosStub.get.withArgs('api/account').rejects();
|
||||
accountService = new AccountService(store);
|
||||
await accountService.update();
|
||||
|
||||
expect(accountService.authenticated).toBe(false);
|
||||
expect(store.account).toBe(null);
|
||||
expect(axiosStub.get.calledWith('management/info')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should init service and check for authority after retrieving account but getAccount failed', async () => {
|
||||
axiosStub.get.rejects();
|
||||
accountService = new AccountService(store);
|
||||
await accountService.update();
|
||||
|
||||
return accountService.hasAnyAuthorityAndCheckAuth('USER').then((value: boolean) => {
|
||||
expect(value).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should init service and check for authority after retrieving account', async () => {
|
||||
axiosStub.get.resolves({ status: 200, data: { authorities: ['USER'], langKey: 'en', login: 'ADMIN' } });
|
||||
accountService = new AccountService(store);
|
||||
await accountService.update();
|
||||
|
||||
return accountService.hasAnyAuthorityAndCheckAuth('USER').then((value: boolean) => {
|
||||
expect(value).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should init service as not authenticated and not return any authorities admin and not retrieve account', async () => {
|
||||
axiosStub.get.rejects();
|
||||
accountService = new AccountService(store);
|
||||
await accountService.update();
|
||||
|
||||
return accountService.hasAnyAuthorityAndCheckAuth('ADMIN').then((value: boolean) => {
|
||||
expect(value).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should init service as not authenticated and return authority user', async () => {
|
||||
axiosStub.get.rejects();
|
||||
accountService = new AccountService(store);
|
||||
await accountService.update();
|
||||
|
||||
return accountService.hasAnyAuthorityAndCheckAuth('USER').then((value: boolean) => {
|
||||
expect(value).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
85
src/main/webapp/app/account/account.service.ts
Normal file
85
src/main/webapp/app/account/account.service.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import { type AccountStore } from '@/store';
|
||||
|
||||
export default class AccountService {
|
||||
constructor(private store: AccountStore) {}
|
||||
|
||||
async update(): Promise<void> {
|
||||
if (!this.store.profilesLoaded) {
|
||||
await this.retrieveProfiles();
|
||||
this.store.setProfilesLoaded();
|
||||
}
|
||||
await this.loadAccount();
|
||||
}
|
||||
|
||||
async retrieveProfiles(): Promise<boolean> {
|
||||
try {
|
||||
const res = await axios.get<any>('management/info');
|
||||
if (res.data?.activeProfiles) {
|
||||
this.store.setRibbonOnProfiles(res.data['display-ribbon-on-profiles']);
|
||||
this.store.setActiveProfiles(res.data.activeProfiles);
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async retrieveAccount(): Promise<boolean> {
|
||||
try {
|
||||
const response = await axios.get<any>('api/account');
|
||||
if (response.status === 200 && response.data?.login) {
|
||||
const account = response.data;
|
||||
this.store.setAuthentication(account);
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// Ignore error
|
||||
}
|
||||
|
||||
this.store.logout();
|
||||
return false;
|
||||
}
|
||||
|
||||
async loadAccount() {
|
||||
if (this.store.logon) {
|
||||
return this.store.logon;
|
||||
}
|
||||
if (this.authenticated && this.userAuthorities) {
|
||||
return;
|
||||
}
|
||||
|
||||
const promise = this.retrieveAccount();
|
||||
this.store.authenticate(promise);
|
||||
promise.then(() => this.store.authenticate(null));
|
||||
await promise;
|
||||
}
|
||||
|
||||
async hasAnyAuthorityAndCheckAuth(authorities: any): Promise<boolean> {
|
||||
if (typeof authorities === 'string') {
|
||||
authorities = [authorities];
|
||||
}
|
||||
|
||||
return this.checkAuthorities(authorities);
|
||||
}
|
||||
|
||||
get authenticated(): boolean {
|
||||
return this.store.authenticated;
|
||||
}
|
||||
|
||||
get userAuthorities(): string[] {
|
||||
return this.store.account?.authorities;
|
||||
}
|
||||
|
||||
private checkAuthorities(authorities: string[]): boolean {
|
||||
if (this.userAuthorities) {
|
||||
for (const authority of authorities) {
|
||||
if (this.userAuthorities.includes(authority)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user