This commit is contained in:
2026-06-25 09:36:05 +07:00
parent d3fef38796
commit 5c644545b8
364 changed files with 78377 additions and 0 deletions
@@ -0,0 +1,19 @@
package com.sisvietnamvn.web;
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(SisvietnamvnApp.class);
}
}
@@ -0,0 +1,12 @@
package com.sisvietnamvn.web;
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.1.0")
@Retention(RetentionPolicy.SOURCE)
@Target({ ElementType.TYPE })
public @interface GeneratedByJHipster {}
@@ -0,0 +1,111 @@
package com.sisvietnamvn.web;
import com.sisvietnamvn.web.config.ApplicationProperties;
import com.sisvietnamvn.web.config.CRLFLogConverter;
import jakarta.annotation.PostConstruct;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.List;
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.context.properties.EnableConfigurationProperties;
import org.springframework.boot.h2console.autoconfigure.H2ConsoleAutoConfiguration;
import org.springframework.boot.liquibase.autoconfigure.LiquibaseProperties;
import org.springframework.core.env.Environment;
import tech.jhipster.config.DefaultProfileUtil;
import tech.jhipster.config.JHipsterConstants;
@SpringBootApplication(exclude = { H2ConsoleAutoConfiguration.class })
@EnableConfigurationProperties({ LiquibaseProperties.class, ApplicationProperties.class })
public class SisvietnamvnApp {
private static final Logger LOG = LoggerFactory.getLogger(SisvietnamvnApp.class);
private final Environment env;
public SisvietnamvnApp(Environment env) {
this.env = env;
}
/**
* Initializes sisvietnamvn.
* <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 = List.of(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(SisvietnamvnApp.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,117 @@
package com.sisvietnamvn.web.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(com.sisvietnamvn.web.repository..*)
|| within(com.sisvietnamvn.web.service..*)
|| within(com.sisvietnamvn.web.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 com.sisvietnamvn.web.aop.logging;
@@ -0,0 +1,38 @@
package com.sisvietnamvn.web.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Properties specific to Sisvietnamvn.
* <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 com.sisvietnamvn.web.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 com.sisvietnamvn.web.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.getFirst().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,59 @@
package com.sisvietnamvn.web.config;
import java.time.Duration;
import org.ehcache.config.builders.*;
import org.ehcache.jsr107.Eh107Configuration;
import org.hibernate.cache.jcache.ConfigSettings;
import org.springframework.boot.cache.autoconfigure.JCacheManagerCustomizer;
import org.springframework.boot.hibernate.autoconfigure.HibernatePropertiesCustomizer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import tech.jhipster.config.JHipsterProperties;
@Configuration
@EnableCaching
public class CacheConfiguration {
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, com.sisvietnamvn.web.repository.UserRepository.USERS_BY_LOGIN_CACHE);
createCache(cm, com.sisvietnamvn.web.repository.UserRepository.USERS_BY_EMAIL_CACHE);
createCache(cm, com.sisvietnamvn.web.domain.User.class.getName());
createCache(cm, com.sisvietnamvn.web.domain.Authority.class.getName());
createCache(cm, com.sisvietnamvn.web.domain.User.class.getName() + ".authorities");
// 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);
}
}
}
@@ -0,0 +1,31 @@
package com.sisvietnamvn.web.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.info.BuildProperties;
import org.springframework.boot.info.GitProperties;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import tech.jhipster.config.cache.PrefixedKeyGenerator;
@Configuration
public class CacheKeyGeneratorConfiguration {
private GitProperties gitProperties;
private BuildProperties buildProperties;
@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);
}
}
@@ -0,0 +1,15 @@
package com.sisvietnamvn.web.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 = "en";
private Constants() {}
}
@@ -0,0 +1,62 @@
package com.sisvietnamvn.web.config;
import java.sql.SQLException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.h2console.autoconfigure.H2ConsoleProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import tech.jhipster.config.JHipsterConstants;
import tech.jhipster.config.h2.H2ConfigurationHelper;
@Configuration
@EnableJpaRepositories({ "com.sisvietnamvn.web.repository" })
@EnableJpaAuditing(auditorAwareRef = "springSecurityAuditorAware")
@EnableTransactionManagement
@EnableConfigurationProperties(H2ConsoleProperties.class)
public class DatabaseConfiguration {
private static final Logger LOG = LoggerFactory.getLogger(DatabaseConfiguration.class);
private final Environment env;
public DatabaseConfiguration(Environment env) {
this.env = env;
}
/**
* Open the TCP port for the H2 database, so it is available remotely.
*
* @return the H2 database TCP server.
* @throws SQLException if the server failed to start.
*/
@Bean(initMethod = "start", destroyMethod = "stop")
@Profile(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT)
@ConditionalOnProperty(prefix = "spring.h2.console", name = "enabled", havingValue = "true")
public Object h2TCPServer() throws SQLException {
String port = getValidPortForH2();
LOG.debug("H2 database is available on port {}", port);
return H2ConfigurationHelper.createServer(port);
}
private String getValidPortForH2() {
var port = Integer.parseInt(env.getProperty("server.port"));
if (port < 10000) {
port = 10000 + port;
} else {
if (port < 63536) {
port = port + 2000;
} else {
port = port - 2000;
}
}
return String.valueOf(port);
}
}
@@ -0,0 +1,20 @@
package com.sisvietnamvn.web.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,24 @@
package com.sisvietnamvn.web.config;
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();
}
}
@@ -0,0 +1,18 @@
package com.sisvietnamvn.web.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import tools.jackson.datatype.hibernate7.Hibernate7Module;
import tools.jackson.datatype.hibernate7.Hibernate7Module.Feature;
@Configuration
public class JacksonHibernateConfiguration {
/*
* Support for Hibernate types in Jackson.
*/
@Bean
public Hibernate7Module hibernate7Module() {
return new Hibernate7Module().configure(Feature.SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS, true);
}
}
@@ -0,0 +1,83 @@
package com.sisvietnamvn.web.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.jdbc.autoconfigure.DataSourceProperties;
import org.springframework.boot.liquibase.autoconfigure.LiquibaseDataSource;
import org.springframework.boot.liquibase.autoconfigure.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 com.sisvietnamvn.web.config;
import com.sisvietnamvn.web.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 com.sisvietnamvn.web.config;
import static tech.jhipster.config.logging.LoggingUtils.*;
import ch.qos.logback.classic.LoggerContext;
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;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.ObjectMapper;
/*
* 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 JacksonException {
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,76 @@
package com.sisvietnamvn.web.config;
import static org.springframework.security.config.Customizer.withDefaults;
import com.sisvietnamvn.web.security.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
import org.springframework.security.web.SecurityFilterChain;
import tech.jhipster.config.JHipsterConstants;
import tech.jhipster.config.JHipsterProperties;
@Configuration
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfiguration {
private final Environment env;
private final JHipsterProperties jHipsterProperties;
public SecurityConfiguration(Environment env, JHipsterProperties jHipsterProperties) {
this.env = env;
this.jHipsterProperties = jHipsterProperties;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http.cors(withDefaults())
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz ->
// prettier-ignore
authz
.requestMatchers(HttpMethod.GET, "/", "/about", "/flex-finish", "/tin-tuc", "/tin-tuc/**", "/lien-he",
"/manage/**", "/css/**", "/images/**", "/js/**", "/UMass*/**", "/Undergraduate*/**",
"/favicon.ico", "/favicons/**", "/flex-finish/**", "/contact-us/**")
.permitAll()
.requestMatchers(HttpMethod.GET, "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/authenticate").permitAll()
.requestMatchers(HttpMethod.GET, "/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("/management/health").permitAll()
.requestMatchers("/management/health/**").permitAll()
.requestMatchers("/management/info").permitAll()
.requestMatchers("/management/prometheus").permitAll()
.requestMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
.accessDeniedHandler(new BearerTokenAccessDeniedHandler()))
.oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults()));
if (env.acceptsProfiles(Profiles.of(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT))) {
http.authorizeHttpRequests(authz -> authz.requestMatchers("/h2-console/**").permitAll())
.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.sameOrigin()));
}
return http.build();
}
}
@@ -0,0 +1,62 @@
package com.sisvietnamvn.web.config;
import static com.sisvietnamvn.web.security.SecurityUtils.JWT_ALGORITHM;
import com.nimbusds.jose.jwk.source.ImmutableSecret;
import com.nimbusds.jose.util.Base64;
import com.sisvietnamvn.web.management.SecurityMetersService;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
@Configuration
public class SecurityJwtConfiguration {
private static final Logger LOG = LoggerFactory.getLogger(SecurityJwtConfiguration.class);
@Value("${jhipster.security.authentication.jwt.base64-secret}")
private String jwtKey;
@Bean
public JwtDecoder jwtDecoder(SecurityMetersService metersService) {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withSecretKey(getSecretKey()).macAlgorithm(JWT_ALGORITHM).build();
return token -> {
try {
return jwtDecoder.decode(token);
} catch (Exception e) {
if (e.getMessage().contains("Invalid signature")) {
metersService.trackTokenInvalidSignature();
} else if (e.getMessage().contains("Jwt expired at")) {
metersService.trackTokenExpired();
} else if (
e.getMessage().contains("Invalid JWT serialization") ||
e.getMessage().contains("Malformed token") ||
e.getMessage().contains("Invalid unsecured/JWS/JWE")
) {
metersService.trackTokenMalformed();
} else {
LOG.error("Unknown JWT error {}", e.getMessage());
}
throw e;
}
};
}
@Bean
public JwtEncoder jwtEncoder() {
return new NimbusJwtEncoder(new ImmutableSecret<>(getSecretKey()));
}
private SecretKey getSecretKey() {
byte[] keyBytes = Base64.from(jwtKey).decode();
return new SecretKeySpec(keyBytes, 0, keyBytes.length, JWT_ALGORITHM.getName());
}
}
@@ -0,0 +1,77 @@
package com.sisvietnamvn.web.config;
import jakarta.servlet.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.server.*;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
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.JHipsterConstants;
import tech.jhipster.config.JHipsterProperties;
import tech.jhipster.config.h2.H2ConfigurationHelper;
/**
* Configuration of web application with Servlet 3.0 APIs.
*/
@Configuration
public class WebConfigurer implements ServletContextInitializer {
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());
}
if (h2ConsoleIsEnabled(env)) {
initH2Console(servletContext);
}
LOG.info("Web application fully configured");
}
@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);
}
private boolean h2ConsoleIsEnabled(Environment env) {
return (
env.acceptsProfiles(Profiles.of(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT)) &&
"true".equals(env.getProperty("spring.h2.console.enabled"))
);
}
/**
* Initializes H2 console.
*/
private void initH2Console(ServletContext servletContext) {
LOG.info("Initialize H2 console");
H2ConfigurationHelper.initH2Console(servletContext);
}
}
@@ -0,0 +1,4 @@
/**
* Application configuration.
*/
package com.sisvietnamvn.web.config;
@@ -0,0 +1,36 @@
package com.sisvietnamvn.web.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "index";
}
// Trang Flex Finish (MỚI THÊM)
@GetMapping("/flex-finish")
public String flexFinish() {
return "flex-finish"; // Trả về tên file flex-finish.html
}
// Trang Giới thiệu Bệnh viện
@GetMapping("/about")
public String about() {
return "about";
}
// Trang Tin tức
@GetMapping("/tin-tuc")
public String tintuc() {
return "tin-tuc";
}
@GetMapping("/lien-he")
public String lienhe() {
return "lien-he";
}
}
@@ -0,0 +1,16 @@
package com.sisvietnamvn.web.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/manage")
public class ManageController {
@GetMapping({"", "/"})
public String index() {
return "manage/index";
}
}
@@ -0,0 +1,77 @@
package com.sisvietnamvn.web.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;
}
}
@@ -0,0 +1,99 @@
package com.sisvietnamvn.web.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,235 @@
package com.sisvietnamvn.web.domain;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.sisvietnamvn.web.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")
@Column(name = "id")
private Long id;
@NotNull
@Pattern(regexp = Constants.LOGIN_REGEX)
@Size(min = 1, max = 50)
@Column(name = "login", 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(name = "email", length = 254, unique = true)
private String email;
@NotNull
@Column(name = "activated", 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<>();
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;
}
@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 com.sisvietnamvn.web.domain;
@@ -0,0 +1,50 @@
package com.sisvietnamvn.web.management;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Service;
@Service
public class SecurityMetersService {
public static final String INVALID_TOKENS_METER_NAME = "security.authentication.invalid-tokens";
public static final String INVALID_TOKENS_METER_DESCRIPTION =
"Indicates validation error count of the tokens presented by the clients.";
public static final String INVALID_TOKENS_METER_BASE_UNIT = "errors";
public static final String INVALID_TOKENS_METER_CAUSE_DIMENSION = "cause";
private final Counter tokenInvalidSignatureCounter;
private final Counter tokenExpiredCounter;
private final Counter tokenUnsupportedCounter;
private final Counter tokenMalformedCounter;
public SecurityMetersService(MeterRegistry registry) {
this.tokenInvalidSignatureCounter = invalidTokensCounterForCauseBuilder("invalid-signature").register(registry);
this.tokenExpiredCounter = invalidTokensCounterForCauseBuilder("expired").register(registry);
this.tokenUnsupportedCounter = invalidTokensCounterForCauseBuilder("unsupported").register(registry);
this.tokenMalformedCounter = invalidTokensCounterForCauseBuilder("malformed").register(registry);
}
private Counter.Builder invalidTokensCounterForCauseBuilder(String cause) {
return Counter.builder(INVALID_TOKENS_METER_NAME)
.baseUnit(INVALID_TOKENS_METER_BASE_UNIT)
.description(INVALID_TOKENS_METER_DESCRIPTION)
.tag(INVALID_TOKENS_METER_CAUSE_DIMENSION, cause);
}
public void trackTokenInvalidSignature() {
this.tokenInvalidSignatureCounter.increment();
}
public void trackTokenExpired() {
this.tokenExpiredCounter.increment();
}
public void trackTokenUnsupported() {
this.tokenUnsupportedCounter.increment();
}
public void trackTokenMalformed() {
this.tokenMalformedCounter.increment();
}
}
@@ -0,0 +1,4 @@
/**
* Application management.
*/
package com.sisvietnamvn.web.management;
@@ -0,0 +1,4 @@
/**
* Application root.
*/
package com.sisvietnamvn.web;
@@ -0,0 +1,12 @@
package com.sisvietnamvn.web.repository;
import com.sisvietnamvn.web.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,36 @@
package com.sisvietnamvn.web.repository;
import com.sisvietnamvn.web.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 com.sisvietnamvn.web.repository;
@@ -0,0 +1,15 @@
package com.sisvietnamvn.web.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 com.sisvietnamvn.web.security;
import com.sisvietnamvn.web.domain.Authority;
import com.sisvietnamvn.web.domain.User;
import com.sisvietnamvn.web.repository.UserRepository;
import java.util.*;
import org.hibernate.validator.internal.constraintvalidators.bv.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,127 @@
package com.sisvietnamvn.web.security;
import java.util.List;
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;
import org.springframework.security.oauth2.core.ClaimAccessor;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
/**
* Utility class for Spring Security.
*/
public final class SecurityUtils {
public static final MacAlgorithm JWT_ALGORITHM = MacAlgorithm.HS512;
public static final String AUTHORITIES_CLAIM = "auth";
public static final String USER_ID_CLAIM = "userId";
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 Jwt jwt) {
return jwt.getSubject();
} else if (authentication.getPrincipal() instanceof String s) {
return s;
}
return null;
}
/**
* Get the JWT of the current user.
*
* @return the JWT of the current user.
*/
public static Optional<String> getCurrentUserJWT() {
SecurityContext securityContext = SecurityContextHolder.getContext();
return Optional.ofNullable(securityContext.getAuthentication()).map(SecurityUtils::extractCredentials);
}
private static String extractCredentials(Authentication authentication) {
if (authentication.getCredentials() instanceof String token) {
return token;
} else if (authentication.getCredentials() instanceof Jwt jwt) {
return jwt.getTokenValue();
}
return null;
}
/**
* Get the Id of the current user.
*
* @return the Id of the current user.
*/
public static Optional<Long> getCurrentUserId() {
SecurityContext securityContext = SecurityContextHolder.getContext();
return Optional.ofNullable(securityContext.getAuthentication())
.filter(authentication -> authentication.getPrincipal() instanceof ClaimAccessor)
.map(authentication -> (ClaimAccessor) authentication.getPrincipal())
.map(principal -> principal.getClaim(USER_ID_CLAIM));
}
/**
* 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 -> List.of(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 com.sisvietnamvn.web.security;
import com.sisvietnamvn.web.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 com.sisvietnamvn.web.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 com.sisvietnamvn.web.security;
@@ -0,0 +1,13 @@
package com.sisvietnamvn.web.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 com.sisvietnamvn.web.service;
import java.io.Serial;
public class InvalidPasswordException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
public InvalidPasswordException() {
super("Incorrect password");
}
}
@@ -0,0 +1,120 @@
package com.sisvietnamvn.web.service;
import com.sisvietnamvn.web.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");
}
}
@@ -0,0 +1,316 @@
package com.sisvietnamvn.web.service;
import com.sisvietnamvn.web.config.Constants;
import com.sisvietnamvn.web.domain.Authority;
import com.sisvietnamvn.web.domain.User;
import com.sisvietnamvn.web.repository.AuthorityRepository;
import com.sisvietnamvn.web.repository.UserRepository;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
import com.sisvietnamvn.web.security.SecurityUtils;
import com.sisvietnamvn.web.service.dto.AdminUserDTO;
import com.sisvietnamvn.web.service.dto.UserDTO;
import java.time.Instant;
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 AuthorityRepository authorityRepository;
private final CacheManager cacheManager;
public UserService(
UserRepository userRepository,
PasswordEncoder passwordEncoder,
AuthorityRepository authorityRepository,
CacheManager cacheManager
) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
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);
}
/**
* 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 com.sisvietnamvn.web.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 com.sisvietnamvn.web.service.dto;
import com.sisvietnamvn.web.config.Constants;
import com.sisvietnamvn.web.domain.Authority;
import com.sisvietnamvn.web.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 com.sisvietnamvn.web.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 com.sisvietnamvn.web.service.dto;
import com.sisvietnamvn.web.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 com.sisvietnamvn.web.service.dto;
@@ -0,0 +1,150 @@
package com.sisvietnamvn.web.service.mapper;
import com.sisvietnamvn.web.domain.Authority;
import com.sisvietnamvn.web.domain.User;
import com.sisvietnamvn.web.service.dto.AdminUserDTO;
import com.sisvietnamvn.web.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 Set.of();
}
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 Set.of();
}
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 com.sisvietnamvn.web.service.mapper;
@@ -0,0 +1,4 @@
/**
* Service layer.
*/
package com.sisvietnamvn.web.service;
@@ -0,0 +1,181 @@
package com.sisvietnamvn.web.web.rest;
import com.sisvietnamvn.web.domain.User;
import com.sisvietnamvn.web.repository.UserRepository;
import com.sisvietnamvn.web.security.SecurityUtils;
import com.sisvietnamvn.web.service.MailService;
import com.sisvietnamvn.web.service.UserService;
import com.sisvietnamvn.web.service.dto.AdminUserDTO;
import com.sisvietnamvn.web.service.dto.PasswordChangeDTO;
import com.sisvietnamvn.web.web.rest.errors.*;
import com.sisvietnamvn.web.web.rest.vm.KeyAndPasswordVM;
import com.sisvietnamvn.web.web.rest.vm.ManagedUserVM;
import jakarta.validation.Valid;
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.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;
public AccountResource(UserRepository userRepository, UserService userService, MailService mailService) {
this.userRepository = userRepository;
this.userService = userService;
this.mailService = mailService;
}
/**
* {@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 /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 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,125 @@
package com.sisvietnamvn.web.web.rest;
import static com.sisvietnamvn.web.security.SecurityUtils.AUTHORITIES_CLAIM;
import static com.sisvietnamvn.web.security.SecurityUtils.JWT_ALGORITHM;
import static com.sisvietnamvn.web.security.SecurityUtils.USER_ID_CLAIM;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.sisvietnamvn.web.security.DomainUserDetailsService.UserWithId;
import com.sisvietnamvn.web.web.rest.vm.LoginVM;
import jakarta.validation.Valid;
import java.security.Principal;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.web.bind.annotation.*;
/**
* Controller to authenticate users.
*/
@RestController
@RequestMapping("/api")
public class AuthenticateController {
private static final Logger LOG = LoggerFactory.getLogger(AuthenticateController.class);
private final JwtEncoder jwtEncoder;
@Value("${jhipster.security.authentication.jwt.token-validity-in-seconds:0}")
private long tokenValidityInSeconds;
@Value("${jhipster.security.authentication.jwt.token-validity-in-seconds-for-remember-me:0}")
private long tokenValidityInSecondsForRememberMe;
private final AuthenticationManagerBuilder authenticationManagerBuilder;
public AuthenticateController(JwtEncoder jwtEncoder, AuthenticationManagerBuilder authenticationManagerBuilder) {
this.jwtEncoder = jwtEncoder;
this.authenticationManagerBuilder = authenticationManagerBuilder;
}
@PostMapping("/authenticate")
public ResponseEntity<JWTToken> authorize(@Valid @RequestBody LoginVM loginVM) {
var authenticationToken = new UsernamePasswordAuthenticationToken(loginVM.getUsername(), loginVM.getPassword());
var authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = this.createToken(authentication, loginVM.isRememberMe());
var httpHeaders = new HttpHeaders();
httpHeaders.setBearerAuth(jwt);
return new ResponseEntity<>(new JWTToken(jwt), httpHeaders, HttpStatus.OK);
}
/**
* {@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();
}
public String createToken(Authentication authentication, boolean rememberMe) {
String authorities = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(" "));
var now = Instant.now();
Instant validity;
if (rememberMe) {
validity = now.plus(this.tokenValidityInSecondsForRememberMe, ChronoUnit.SECONDS);
} else {
validity = now.plus(this.tokenValidityInSeconds, ChronoUnit.SECONDS);
}
// @formatter:off
JwtClaimsSet.Builder builder = JwtClaimsSet.builder()
.issuedAt(now)
.expiresAt(validity)
.subject(authentication.getName())
.claim(AUTHORITIES_CLAIM, authorities);
if (authentication.getPrincipal() instanceof UserWithId user) {
builder.claim(USER_ID_CLAIM, user.getId());
}
JwsHeader jwsHeader = JwsHeader.with(JWT_ALGORITHM).build();
return this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, builder.build())).getTokenValue();
}
/**
* Object to return as body in JWT Authentication.
*/
static class JWTToken {
private String idToken;
JWTToken(String idToken) {
this.idToken = idToken;
}
@JsonProperty("id_token")
String getIdToken() {
return idToken;
}
void setIdToken(String idToken) {
this.idToken = idToken;
}
}
}
@@ -0,0 +1,103 @@
package com.sisvietnamvn.web.web.rest;
import com.sisvietnamvn.web.domain.Authority;
import com.sisvietnamvn.web.repository.AuthorityRepository;
import com.sisvietnamvn.web.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 com.sisvietnamvn.web.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:sisvietnamvn}")
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, false, 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, false, ENTITY_NAME, id))
.build();
}
}
@@ -0,0 +1,61 @@
package com.sisvietnamvn.web.web.rest;
import com.sisvietnamvn.web.service.UserService;
import com.sisvietnamvn.web.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 = List.of(
"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);
}
}
@@ -0,0 +1,210 @@
package com.sisvietnamvn.web.web.rest;
import com.sisvietnamvn.web.config.Constants;
import com.sisvietnamvn.web.domain.User;
import com.sisvietnamvn.web.repository.UserRepository;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
import com.sisvietnamvn.web.service.MailService;
import com.sisvietnamvn.web.service.UserService;
import com.sisvietnamvn.web.service.dto.AdminUserDTO;
import com.sisvietnamvn.web.web.rest.errors.BadRequestAlertException;
import com.sisvietnamvn.web.web.rest.errors.EmailAlreadyUsedException;
import com.sisvietnamvn.web.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 com.sisvietnamvn.web.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 = List.of(
"id",
"login",
"firstName",
"lastName",
"email",
"activated",
"langKey",
"createdBy",
"createdDate",
"lastModifiedBy",
"lastModifiedDate"
);
private static final Logger LOG = LoggerFactory.getLogger(UserResource.class);
@Value("${jhipster.clientApp.name:sisvietnamvn}")
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, "A user is created with identifier " + newUser.getLogin(), 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, "A user is updated with identifier " + userDTO.getLogin(), 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, "A user is deleted with identifier " + login, login))
.build();
}
}
@@ -0,0 +1,51 @@
package com.sisvietnamvn.web.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 com.sisvietnamvn.web.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 com.sisvietnamvn.web.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,258 @@
package com.sisvietnamvn.web.web.rest.errors;
import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation;
import jakarta.servlet.http.HttpServletRequest;
import java.net.URI;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.jspecify.annotations.Nullable;
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.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:sisvietnamvn}")
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);
}
@SuppressWarnings("java:S2638")
@Nullable
@Override
protected ResponseEntity<Object> handleExceptionInternal(
Exception ex,
@Nullable Object body,
HttpHeaders headers,
HttpStatusCode statusCode,
WebRequest request
) {
body = body == null ? wrapAndCustomizeProblem(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 com.sisvietnamvn.web.service.UsernameAlreadyUsedException
) return (ProblemDetailWithCause) new LoginAlreadyUsedException().getBody();
if (
ex instanceof com.sisvietnamvn.web.service.EmailAlreadyUsedException
) return (ProblemDetailWithCause) new EmailAlreadyUsedException().getBody();
if (
ex instanceof com.sisvietnamvn.web.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 resolveResponseStatus(throwable);
}
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 = List.of(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 Strings.CS.containsAny(message, "org.", "java.", "net.", "jakarta.", "javax.", "com.", "io.", "de.", "com.sisvietnamvn.web");
}
}
@@ -0,0 +1,34 @@
package com.sisvietnamvn.web.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 com.sisvietnamvn.web.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 com.sisvietnamvn.web.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 com.sisvietnamvn.web.web.rest.errors;
@@ -0,0 +1,4 @@
/**
* Rest layer.
*/
package com.sisvietnamvn.web.web.rest;
@@ -0,0 +1,27 @@
package com.sisvietnamvn.web.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,53 @@
package com.sisvietnamvn.web.web.rest.vm;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
/**
* View Model object for storing a user's credentials.
*/
public class LoginVM {
@NotNull
@Size(min = 1, max = 254)
private String username;
@NotNull
@Size(min = 4, max = 100)
private String password;
private boolean rememberMe;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public boolean isRememberMe() {
return rememberMe;
}
public void setRememberMe(boolean rememberMe) {
this.rememberMe = rememberMe;
}
// prettier-ignore
@Override
public String toString() {
return "LoginVM{" +
"username='" + username + '\'' +
", rememberMe=" + rememberMe +
'}';
}
}
@@ -0,0 +1,35 @@
package com.sisvietnamvn.web.web.rest.vm;
import com.sisvietnamvn.web.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 com.sisvietnamvn.web.web.rest.vm;