| @@ -0,0 +1,40 @@ | |||||
| HELP.md | |||||
| .gradle | |||||
| build/ | |||||
| !gradle/wrapper/gradle-wrapper.jar | |||||
| !**/src/main/**/build/ | |||||
| !**/src/test/**/build/ | |||||
| ### STS ### | |||||
| .apt_generated | |||||
| .classpath | |||||
| .factorypath | |||||
| .project | |||||
| .settings | |||||
| .springBeans | |||||
| .sts4-cache | |||||
| bin/ | |||||
| !**/src/main/**/bin/ | |||||
| !**/src/test/**/bin/ | |||||
| ### IntelliJ IDEA ### | |||||
| .idea | |||||
| *.iws | |||||
| *.iml | |||||
| *.ipr | |||||
| out/ | |||||
| !**/src/main/**/out/ | |||||
| !**/src/test/**/out/ | |||||
| ### NetBeans ### | |||||
| /nbproject/private/ | |||||
| /nbbuild/ | |||||
| /dist/ | |||||
| /nbdist/ | |||||
| /.nb-gradle/ | |||||
| ### VS Code ### | |||||
| .vscode/ | |||||
| ### EXTJS Plugin ### | |||||
| .sencha/ | |||||
| @@ -0,0 +1,46 @@ | |||||
| plugins { | |||||
| id 'org.springframework.boot' version '2.7.0' | |||||
| id 'io.spring.dependency-management' version '1.0.11.RELEASE' | |||||
| id 'java' | |||||
| } | |||||
| group = 'com.ffii' | |||||
| version = '0.0.1-SNAPSHOT' | |||||
| sourceCompatibility = '11' | |||||
| repositories { | |||||
| mavenCentral() | |||||
| maven { | |||||
| url 'https://jaspersoft.jfrog.io/jaspersoft/third-party-ce-artifacts/' | |||||
| } | |||||
| } | |||||
| dependencies { | |||||
| implementation 'org.springframework.boot:spring-boot-starter-data-jpa' | |||||
| implementation 'org.springframework.boot:spring-boot-starter-security' | |||||
| implementation 'org.springframework.boot:spring-boot-starter-validation' | |||||
| implementation 'org.springframework.boot:spring-boot-starter-web' | |||||
| implementation 'org.springframework.boot:spring-boot-starter-mail' | |||||
| implementation 'org.springframework.boot:spring-boot-starter-freemarker' | |||||
| implementation 'org.liquibase:liquibase-core' | |||||
| runtimeOnly 'mysql:mysql-connector-java' | |||||
| testImplementation 'org.springframework.boot:spring-boot-starter-test' | |||||
| testImplementation 'org.springframework.security:spring-security-test' | |||||
| def apachePoi = '5.2.2' | |||||
| implementation group: 'org.apache.poi', name: 'poi', version: apachePoi | |||||
| implementation group: 'org.apache.poi', name: 'poi-ooxml', version: apachePoi | |||||
| implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0' | |||||
| implementation group: 'org.apache.commons', name: 'commons-text', version: '1.9' | |||||
| implementation group: 'org.apache.tika', name: 'tika-core', version: '2.4.1' | |||||
| implementation 'net.sf.jasperreports:jasperreports:6.19.1' | |||||
| implementation group: 'org.springdoc', name: 'springdoc-openapi-ui', version: '1.6.9' | |||||
| implementation fileTree(dir: 'libs', include: '*.jar'); | |||||
| } | |||||
| tasks.named('test') { | |||||
| useJUnitPlatform() | |||||
| } | |||||
| @@ -0,0 +1,12 @@ | |||||
| { | |||||
| "configurations": [ | |||||
| { | |||||
| "type": "java", | |||||
| "name": "Backend Debug", | |||||
| "request": "launch", | |||||
| "mainClass": "com.ffii.baseapp.TownGasApplication", | |||||
| "projectName": "TOWNGAS", | |||||
| "args": "--spring.profiles.active=db-local,debug-log,local-res-win" | |||||
| } | |||||
| ] | |||||
| } | |||||
| @@ -0,0 +1,5 @@ | |||||
| distributionBase=GRADLE_USER_HOME | |||||
| distributionPath=wrapper/dists | |||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip | |||||
| zipStoreBase=GRADLE_USER_HOME | |||||
| zipStorePath=wrapper/dists | |||||
| @@ -0,0 +1,234 @@ | |||||
| #!/bin/sh | |||||
| # | |||||
| # Copyright © 2015-2021 the original authors. | |||||
| # | |||||
| # Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| # you may not use this file except in compliance with the License. | |||||
| # You may obtain a copy of the License at | |||||
| # | |||||
| # https://www.apache.org/licenses/LICENSE-2.0 | |||||
| # | |||||
| # Unless required by applicable law or agreed to in writing, software | |||||
| # distributed under the License is distributed on an "AS IS" BASIS, | |||||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| # See the License for the specific language governing permissions and | |||||
| # limitations under the License. | |||||
| # | |||||
| ############################################################################## | |||||
| # | |||||
| # Gradle start up script for POSIX generated by Gradle. | |||||
| # | |||||
| # Important for running: | |||||
| # | |||||
| # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is | |||||
| # noncompliant, but you have some other compliant shell such as ksh or | |||||
| # bash, then to run this script, type that shell name before the whole | |||||
| # command line, like: | |||||
| # | |||||
| # ksh Gradle | |||||
| # | |||||
| # Busybox and similar reduced shells will NOT work, because this script | |||||
| # requires all of these POSIX shell features: | |||||
| # * functions; | |||||
| # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», | |||||
| # «${var#prefix}», «${var%suffix}», and «$( cmd )»; | |||||
| # * compound commands having a testable exit status, especially «case»; | |||||
| # * various built-in commands including «command», «set», and «ulimit». | |||||
| # | |||||
| # Important for patching: | |||||
| # | |||||
| # (2) This script targets any POSIX shell, so it avoids extensions provided | |||||
| # by Bash, Ksh, etc; in particular arrays are avoided. | |||||
| # | |||||
| # The "traditional" practice of packing multiple parameters into a | |||||
| # space-separated string is a well documented source of bugs and security | |||||
| # problems, so this is (mostly) avoided, by progressively accumulating | |||||
| # options in "$@", and eventually passing that to Java. | |||||
| # | |||||
| # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, | |||||
| # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; | |||||
| # see the in-line comments for details. | |||||
| # | |||||
| # There are tweaks for specific operating systems such as AIX, CygWin, | |||||
| # Darwin, MinGW, and NonStop. | |||||
| # | |||||
| # (3) This script is generated from the Groovy template | |||||
| # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt | |||||
| # within the Gradle project. | |||||
| # | |||||
| # You can find Gradle at https://github.com/gradle/gradle/. | |||||
| # | |||||
| ############################################################################## | |||||
| # Attempt to set APP_HOME | |||||
| # Resolve links: $0 may be a link | |||||
| app_path=$0 | |||||
| # Need this for daisy-chained symlinks. | |||||
| while | |||||
| APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path | |||||
| [ -h "$app_path" ] | |||||
| do | |||||
| ls=$( ls -ld "$app_path" ) | |||||
| link=${ls#*' -> '} | |||||
| case $link in #( | |||||
| /*) app_path=$link ;; #( | |||||
| *) app_path=$APP_HOME$link ;; | |||||
| esac | |||||
| done | |||||
| APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit | |||||
| APP_NAME="Gradle" | |||||
| APP_BASE_NAME=${0##*/} | |||||
| # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. | |||||
| DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' | |||||
| # Use the maximum available, or set MAX_FD != -1 to use that value. | |||||
| MAX_FD=maximum | |||||
| warn () { | |||||
| echo "$*" | |||||
| } >&2 | |||||
| die () { | |||||
| echo | |||||
| echo "$*" | |||||
| echo | |||||
| exit 1 | |||||
| } >&2 | |||||
| # OS specific support (must be 'true' or 'false'). | |||||
| cygwin=false | |||||
| msys=false | |||||
| darwin=false | |||||
| nonstop=false | |||||
| case "$( uname )" in #( | |||||
| CYGWIN* ) cygwin=true ;; #( | |||||
| Darwin* ) darwin=true ;; #( | |||||
| MSYS* | MINGW* ) msys=true ;; #( | |||||
| NONSTOP* ) nonstop=true ;; | |||||
| esac | |||||
| CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar | |||||
| # Determine the Java command to use to start the JVM. | |||||
| if [ -n "$JAVA_HOME" ] ; then | |||||
| if [ -x "$JAVA_HOME/jre/sh/java" ] ; then | |||||
| # IBM's JDK on AIX uses strange locations for the executables | |||||
| JAVACMD=$JAVA_HOME/jre/sh/java | |||||
| else | |||||
| JAVACMD=$JAVA_HOME/bin/java | |||||
| fi | |||||
| if [ ! -x "$JAVACMD" ] ; then | |||||
| die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME | |||||
| Please set the JAVA_HOME variable in your environment to match the | |||||
| location of your Java installation." | |||||
| fi | |||||
| else | |||||
| JAVACMD=java | |||||
| which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. | |||||
| Please set the JAVA_HOME variable in your environment to match the | |||||
| location of your Java installation." | |||||
| fi | |||||
| # Increase the maximum file descriptors if we can. | |||||
| if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then | |||||
| case $MAX_FD in #( | |||||
| max*) | |||||
| MAX_FD=$( ulimit -H -n ) || | |||||
| warn "Could not query maximum file descriptor limit" | |||||
| esac | |||||
| case $MAX_FD in #( | |||||
| '' | soft) :;; #( | |||||
| *) | |||||
| ulimit -n "$MAX_FD" || | |||||
| warn "Could not set maximum file descriptor limit to $MAX_FD" | |||||
| esac | |||||
| fi | |||||
| # Collect all arguments for the java command, stacking in reverse order: | |||||
| # * args from the command line | |||||
| # * the main class name | |||||
| # * -classpath | |||||
| # * -D...appname settings | |||||
| # * --module-path (only if needed) | |||||
| # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. | |||||
| # For Cygwin or MSYS, switch paths to Windows format before running java | |||||
| if "$cygwin" || "$msys" ; then | |||||
| APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) | |||||
| CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) | |||||
| JAVACMD=$( cygpath --unix "$JAVACMD" ) | |||||
| # Now convert the arguments - kludge to limit ourselves to /bin/sh | |||||
| for arg do | |||||
| if | |||||
| case $arg in #( | |||||
| -*) false ;; # don't mess with options #( | |||||
| /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath | |||||
| [ -e "$t" ] ;; #( | |||||
| *) false ;; | |||||
| esac | |||||
| then | |||||
| arg=$( cygpath --path --ignore --mixed "$arg" ) | |||||
| fi | |||||
| # Roll the args list around exactly as many times as the number of | |||||
| # args, so each arg winds up back in the position where it started, but | |||||
| # possibly modified. | |||||
| # | |||||
| # NB: a `for` loop captures its iteration list before it begins, so | |||||
| # changing the positional parameters here affects neither the number of | |||||
| # iterations, nor the values presented in `arg`. | |||||
| shift # remove old arg | |||||
| set -- "$@" "$arg" # push replacement arg | |||||
| done | |||||
| fi | |||||
| # Collect all arguments for the java command; | |||||
| # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of | |||||
| # shell script including quotes and variable substitutions, so put them in | |||||
| # double quotes to make sure that they get re-expanded; and | |||||
| # * put everything else in single quotes, so that it's not re-expanded. | |||||
| set -- \ | |||||
| "-Dorg.gradle.appname=$APP_BASE_NAME" \ | |||||
| -classpath "$CLASSPATH" \ | |||||
| org.gradle.wrapper.GradleWrapperMain \ | |||||
| "$@" | |||||
| # Use "xargs" to parse quoted args. | |||||
| # | |||||
| # With -n1 it outputs one arg per line, with the quotes and backslashes removed. | |||||
| # | |||||
| # In Bash we could simply go: | |||||
| # | |||||
| # readarray ARGS < <( xargs -n1 <<<"$var" ) && | |||||
| # set -- "${ARGS[@]}" "$@" | |||||
| # | |||||
| # but POSIX shell has neither arrays nor command substitution, so instead we | |||||
| # post-process each arg (as a line of input to sed) to backslash-escape any | |||||
| # character that might be a shell metacharacter, then use eval to reverse | |||||
| # that process (while maintaining the separation between arguments), and wrap | |||||
| # the whole thing up as a single "set" statement. | |||||
| # | |||||
| # This will of course break if any of these variables contains a newline or | |||||
| # an unmatched quote. | |||||
| # | |||||
| eval "set -- $( | |||||
| printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | | |||||
| xargs -n1 | | |||||
| sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | | |||||
| tr '\n' ' ' | |||||
| )" '"$@"' | |||||
| exec "$JAVACMD" "$@" | |||||
| @@ -0,0 +1,89 @@ | |||||
| @rem | |||||
| @rem Copyright 2015 the original author or authors. | |||||
| @rem | |||||
| @rem Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| @rem you may not use this file except in compliance with the License. | |||||
| @rem You may obtain a copy of the License at | |||||
| @rem | |||||
| @rem https://www.apache.org/licenses/LICENSE-2.0 | |||||
| @rem | |||||
| @rem Unless required by applicable law or agreed to in writing, software | |||||
| @rem distributed under the License is distributed on an "AS IS" BASIS, | |||||
| @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| @rem See the License for the specific language governing permissions and | |||||
| @rem limitations under the License. | |||||
| @rem | |||||
| @if "%DEBUG%" == "" @echo off | |||||
| @rem ########################################################################## | |||||
| @rem | |||||
| @rem Gradle startup script for Windows | |||||
| @rem | |||||
| @rem ########################################################################## | |||||
| @rem Set local scope for the variables with windows NT shell | |||||
| if "%OS%"=="Windows_NT" setlocal | |||||
| set DIRNAME=%~dp0 | |||||
| if "%DIRNAME%" == "" set DIRNAME=. | |||||
| set APP_BASE_NAME=%~n0 | |||||
| set APP_HOME=%DIRNAME% | |||||
| @rem Resolve any "." and ".." in APP_HOME to make it shorter. | |||||
| for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi | |||||
| @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. | |||||
| set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" | |||||
| @rem Find java.exe | |||||
| if defined JAVA_HOME goto findJavaFromJavaHome | |||||
| set JAVA_EXE=java.exe | |||||
| %JAVA_EXE% -version >NUL 2>&1 | |||||
| if "%ERRORLEVEL%" == "0" goto execute | |||||
| echo. | |||||
| echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. | |||||
| echo. | |||||
| echo Please set the JAVA_HOME variable in your environment to match the | |||||
| echo location of your Java installation. | |||||
| goto fail | |||||
| :findJavaFromJavaHome | |||||
| set JAVA_HOME=%JAVA_HOME:"=% | |||||
| set JAVA_EXE=%JAVA_HOME%/bin/java.exe | |||||
| if exist "%JAVA_EXE%" goto execute | |||||
| echo. | |||||
| echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% | |||||
| echo. | |||||
| echo Please set the JAVA_HOME variable in your environment to match the | |||||
| echo location of your Java installation. | |||||
| goto fail | |||||
| :execute | |||||
| @rem Setup the command line | |||||
| set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar | |||||
| @rem Execute Gradle | |||||
| "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* | |||||
| :end | |||||
| @rem End local scope for the variables with windows NT shell | |||||
| if "%ERRORLEVEL%"=="0" goto mainEnd | |||||
| :fail | |||||
| rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of | |||||
| rem the _cmd.exe /c_ return code! | |||||
| if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 | |||||
| exit /b 1 | |||||
| :mainEnd | |||||
| if "%OS%"=="Windows_NT" endlocal | |||||
| :omega | |||||
| @@ -0,0 +1 @@ | |||||
| rootProject.name = 'BASE' | |||||
| @@ -0,0 +1,13 @@ | |||||
| package com.ffii; | |||||
| import org.springframework.boot.SpringApplication; | |||||
| import org.springframework.boot.autoconfigure.SpringBootApplication; | |||||
| @SpringBootApplication() | |||||
| public class BaseApplication { | |||||
| public static void main(String[] args) { | |||||
| SpringApplication.run(BaseApplication.class, args); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,103 @@ | |||||
| package com.ffii.baseapp.example.web; | |||||
| import java.io.FileNotFoundException; | |||||
| import java.io.IOException; | |||||
| import java.io.InputStream; | |||||
| import java.io.UnsupportedEncodingException; | |||||
| import java.util.Arrays; | |||||
| import java.util.Map; | |||||
| import javax.imageio.ImageIO; | |||||
| import javax.mail.internet.InternetAddress; | |||||
| import javax.servlet.http.HttpServletResponse; | |||||
| import org.springframework.core.io.ClassPathResource; | |||||
| import org.springframework.web.bind.annotation.GetMapping; | |||||
| import org.springframework.web.bind.annotation.RequestMapping; | |||||
| import com.ffii.core.common.mail.pojo.MailRequest; | |||||
| import com.ffii.core.common.mail.service.MailService; | |||||
| import com.ffii.core.exception.InternalServerErrorException; | |||||
| import com.ffii.core.utils.JasperUtils; | |||||
| import net.sf.jasperreports.engine.JRException; | |||||
| import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource; | |||||
| // @RestController | |||||
| @RequestMapping("/public/example") | |||||
| public class PublicExampleController { | |||||
| private MailService mailService; | |||||
| public PublicExampleController(MailService mailService) { | |||||
| this.mailService = mailService; | |||||
| } | |||||
| @GetMapping("/example.pdf") | |||||
| public void examplePdf(HttpServletResponse response) throws JRException, IOException { | |||||
| try (InputStream imgIn = new ClassPathResource("reports/cat.jpg").getInputStream()) { | |||||
| var jRerpot = JasperUtils.compile( | |||||
| "reports/example", | |||||
| Map.of( | |||||
| "field1", "field111", | |||||
| "subField1", "subField11111", | |||||
| "img", ImageIO.read(imgIn), | |||||
| "subreport1Records", | |||||
| new JRBeanCollectionDataSource( | |||||
| Arrays.asList(Map.of("subColumn1", "sub column1111", "subColumn2", "sub column222")))), | |||||
| new JRBeanCollectionDataSource(Arrays.asList(Map.of("column1", "column1111", "column2", "column222"))), | |||||
| Map.of("subreport1", "reports/subexample")); | |||||
| JasperUtils.responsePdf(response, jRerpot, "filename1", false); | |||||
| } | |||||
| } | |||||
| @GetMapping("/example.docx") | |||||
| public void exampleDocx(HttpServletResponse response) throws JRException, IOException { | |||||
| try (InputStream imgIn = new ClassPathResource("reports/cat.jpg").getInputStream()) { | |||||
| var jRerpot = JasperUtils.compile( | |||||
| "reports/example", | |||||
| Map.of( | |||||
| "field1", "field111", | |||||
| "subField1", "subField11111", | |||||
| "img", ImageIO.read(imgIn), | |||||
| "subreport1Records", | |||||
| new JRBeanCollectionDataSource( | |||||
| Arrays.asList(Map.of("subColumn1", "sub column1111", "subColumn2", "sub column222")))), | |||||
| new JRBeanCollectionDataSource(Arrays.asList(Map.of("column1", "column1111", "column2", "column222"))), | |||||
| Map.of("subreport1", "reports/subexample")); | |||||
| JasperUtils.responseDocx(response, jRerpot, "filename1"); | |||||
| } | |||||
| } | |||||
| private MailRequest genMailReq() { | |||||
| try (InputStream imgIn = new ClassPathResource("reports/cat.jpg").getInputStream()) { | |||||
| return MailRequest.builder() | |||||
| .subject("hi") | |||||
| .template("mail/content") | |||||
| .args(Map.of("content", "hi")) | |||||
| .priority(MailRequest.PRIORITY_HIGHEST) | |||||
| .addTo(new InternetAddress("[email protected]", "fung")) | |||||
| // .addCc(new InternetAddress("[email protected]", "matthew")) | |||||
| // .replyTo(new InternetAddress("[email protected]", "matthew")) | |||||
| .addAttachment("cat.jpg", imgIn.readAllBytes()) | |||||
| .build(); | |||||
| } catch (UnsupportedEncodingException e) { | |||||
| throw new InternalServerErrorException(); | |||||
| } catch (FileNotFoundException e) { | |||||
| throw new InternalServerErrorException(); | |||||
| } catch (IOException e) { | |||||
| throw new InternalServerErrorException(); | |||||
| } | |||||
| } | |||||
| @GetMapping("/mail") | |||||
| public void mail() { | |||||
| mailService.send(genMailReq()); | |||||
| } | |||||
| @GetMapping("/mail-async") | |||||
| public void mailAsync() { | |||||
| mailService.asyncSend(genMailReq()); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,26 @@ | |||||
| package com.ffii.core.common; | |||||
| public class ErrorCodes { | |||||
| public static final String FILE_UPLOAD_ERROR = "FILE_UPLOAD_ERROR"; | |||||
| public static final String STOCK_IN_WRONG_POST = "STOCK_IN_WRONG_POST"; | |||||
| public static final String USER_WRONG_NEW_PWD = "USER_WRONG_NEW_PWD"; | |||||
| public static final String SEND_EMAIL_ERROR = "SEND_EMAIL_ERROR"; | |||||
| public static final String USERNAME_NOT_AVAILABLE = "USERNAME_NOT_AVAILABLE"; | |||||
| public static final String INIT_EXCEL_ERROR = "INIT_EXCEL_ERROR"; | |||||
| // | |||||
| public static final String DELETE_DEPARTMENT_ERROR = "DELETE_DEPARTMENT_ERROR"; | |||||
| public static final String DELETE_EQUIPMENT_ERROR = "DELETE_EQUIPMENT_ERROR"; | |||||
| public static final String DELETE_EQUIPMENT_TYPE_ERROR = "DELETE_EQUIPMENT_TYPE_ERROR"; | |||||
| public static final String DELETE_MANUFACTURER_ERROR = "DELETE_MANUFACTURER_ERROR"; | |||||
| public static final String DELETE_MODEL_ERROR = "DELETE_MODEL_ERROR"; | |||||
| public static final String DELETE_PART_ERROR = "DELETE_PART_ERROR"; | |||||
| public static final String DELETE_SYMPTOM_ERROR = "DELETE_SYMPTOM_ERROR"; | |||||
| public static final String DELETE_USER_GROUP_ERROR = "DELETE_USER_GROUP_ERROR"; | |||||
| public static final String USER_NOT_EXIST = "USER_NOT_EXIST"; | |||||
| } | |||||
| @@ -0,0 +1,55 @@ | |||||
| package com.ffii.core.common; | |||||
| import org.apache.commons.lang3.StringUtils; | |||||
| import com.ffii.core.settings.service.SettingsService; | |||||
| public class MailSMTP { | |||||
| private String host; | |||||
| private int port; | |||||
| private String username; | |||||
| private String password; | |||||
| public MailSMTP(SettingsService settingsService) { | |||||
| if (settingsService == null) | |||||
| throw new IllegalArgumentException("settingsService"); | |||||
| this.host = settingsService.getString(SettingNames.MAIL_SMTP_HOST); | |||||
| this.port = settingsService.getInt(SettingNames.MAIL_SMTP_PORT); | |||||
| this.username = settingsService.getString(SettingNames.MAIL_SMTP_USERNAME); | |||||
| this.password = settingsService.getString(SettingNames.MAIL_SMTP_PASSWORD); | |||||
| } | |||||
| public String getHost() { | |||||
| return host; | |||||
| } | |||||
| public int getPort() { | |||||
| return port; | |||||
| } | |||||
| public String getUsername() { | |||||
| return username; | |||||
| } | |||||
| public String getPassword() { | |||||
| return password; | |||||
| } | |||||
| @Override | |||||
| public boolean equals(Object obj) { | |||||
| if (obj == null || !(obj instanceof MailSMTP)) | |||||
| return false; | |||||
| MailSMTP o = (MailSMTP) obj; | |||||
| if (StringUtils.equals(this.getHost(), o.getHost()) && | |||||
| this.getPort() == o.getPort() && | |||||
| StringUtils.equals(this.getUsername(), o.getUsername()) && | |||||
| StringUtils.equals(this.getPassword(), o.getPassword())) { | |||||
| return true; | |||||
| } | |||||
| return false; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,114 @@ | |||||
| package com.ffii.core.common; | |||||
| import com.fasterxml.jackson.annotation.JsonIgnore; | |||||
| import com.ffii.core.utils.PasswordUtils.IPasswordRule; | |||||
| import com.ffii.core.settings.service.SettingsService; | |||||
| public class PasswordRule implements IPasswordRule { | |||||
| private Integer min; | |||||
| private Integer max; | |||||
| private Boolean number; | |||||
| private Boolean upperEng; | |||||
| private Boolean lowerEng; | |||||
| private Boolean specialChar; | |||||
| public PasswordRule(SettingsService settingsService) { | |||||
| if (settingsService == null) | |||||
| throw new IllegalArgumentException("settingsService"); | |||||
| this.min = settingsService.getInt(SettingNames.SYS_PASSWORD_RULE_MIN); | |||||
| this.max = settingsService.getInt(SettingNames.SYS_PASSWORD_RULE_MAX); | |||||
| this.number = settingsService.getBoolean(SettingNames.SYS_PASSWORD_RULE_NUMBER); | |||||
| this.upperEng = settingsService.getBoolean(SettingNames.SYS_PASSWORD_RULE_UPPER_ENG); | |||||
| this.lowerEng = settingsService.getBoolean(SettingNames.SYS_PASSWORD_RULE_LOWER_ENG); | |||||
| this.specialChar = settingsService.getBoolean(SettingNames.SYS_PASSWORD_RULE_SPECIAL); | |||||
| } | |||||
| @Override | |||||
| public int getMin() { | |||||
| return min; | |||||
| } | |||||
| @Override | |||||
| public int getMax() { | |||||
| return max; | |||||
| } | |||||
| @Override | |||||
| public boolean needNumberChar() { | |||||
| return number; | |||||
| } | |||||
| @Override | |||||
| public boolean needUpperEngChar() { | |||||
| return upperEng; | |||||
| } | |||||
| @Override | |||||
| public boolean needLowerEngChar() { | |||||
| return lowerEng; | |||||
| } | |||||
| @Override | |||||
| public boolean needSpecialChar() { | |||||
| return specialChar; | |||||
| } | |||||
| public void setMin(Integer min) { | |||||
| this.min = min; | |||||
| } | |||||
| public void setMax(Integer max) { | |||||
| this.max = max; | |||||
| } | |||||
| public Boolean getNumber() { | |||||
| return number; | |||||
| } | |||||
| public void setNumber(Boolean number) { | |||||
| this.number = number; | |||||
| } | |||||
| public Boolean getUpperEng() { | |||||
| return upperEng; | |||||
| } | |||||
| public void setUpperEng(Boolean upperEng) { | |||||
| this.upperEng = upperEng; | |||||
| } | |||||
| public Boolean getLowerEng() { | |||||
| return lowerEng; | |||||
| } | |||||
| public void setLowerEng(Boolean lowerEng) { | |||||
| this.lowerEng = lowerEng; | |||||
| } | |||||
| public Boolean getSpecialChar() { | |||||
| return specialChar; | |||||
| } | |||||
| public void setSpecialChar(Boolean specialChar) { | |||||
| this.specialChar = specialChar; | |||||
| } | |||||
| @JsonIgnore | |||||
| public String getWrongMsg() { | |||||
| StringBuilder msg = new StringBuilder("Please Following Password Rule.\n"); | |||||
| msg.append("Minimum " + getMin() + " Characters\n"); | |||||
| msg.append("Maximum " + getMax() + " Characters\n"); | |||||
| if (needNumberChar()) | |||||
| msg.append("Numbers\n"); | |||||
| if (needLowerEngChar()) | |||||
| msg.append("Lower-Case Letters\n"); | |||||
| if (needUpperEngChar()) | |||||
| msg.append("Capital Letters\n"); | |||||
| if (needSpecialChar()) | |||||
| msg.append("Symbols\n"); | |||||
| return msg.toString(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,146 @@ | |||||
| package com.ffii.core.common; | |||||
| import java.util.Optional; | |||||
| import org.springframework.dao.DataAccessException; | |||||
| import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | |||||
| 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.core.userdetails.UserDetailsService; | |||||
| import org.springframework.security.core.userdetails.UsernameNotFoundException; | |||||
| import com.ffii.core.user.entity.User; | |||||
| /** | |||||
| * Security Utils - for Spring Security | |||||
| * | |||||
| * @author Patrick | |||||
| */ | |||||
| public class SecurityUtils { | |||||
| /** | |||||
| * Obtains the current {@code SecurityContext}. | |||||
| * | |||||
| * @return the security context (never {@code null}) | |||||
| */ | |||||
| public static final SecurityContext getSecurityContext() { | |||||
| return SecurityContextHolder.getContext(); | |||||
| } | |||||
| /** | |||||
| * @return the authenticated {@code Principal}) | |||||
| * @see Authentication#getPrincipal() | |||||
| */ | |||||
| public static final Optional<User> getUser() { | |||||
| try { | |||||
| return Optional.of((User) getSecurityContext().getAuthentication().getPrincipal()); | |||||
| } catch (ClassCastException e) { | |||||
| // no authenticated principal | |||||
| return Optional.empty(); | |||||
| } catch (NullPointerException e) { | |||||
| // no authentication information is available | |||||
| return Optional.empty(); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Updates the Authentication Token with the user (e.g. user changed the password) | |||||
| * | |||||
| * @see SecurityContext#setAuthentication(Authentication) | |||||
| */ | |||||
| public static final void updateUserAuthentication(final UserDetails user) { | |||||
| getSecurityContext().setAuthentication(new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities())); | |||||
| } | |||||
| /** | |||||
| * Checks if the current user is GRANTED the {@code role} | |||||
| * | |||||
| * @param role | |||||
| * the {@code role} to check for | |||||
| * @return {@code true} if the current user is GRANTED the {@code role}, else {@code false} | |||||
| */ | |||||
| public static final boolean isGranted(String role) { | |||||
| Authentication authentication = getSecurityContext().getAuthentication(); | |||||
| if (authentication == null) return false; | |||||
| for (GrantedAuthority auth : authentication.getAuthorities()) { | |||||
| if (role.equals(auth.getAuthority())) return true; | |||||
| } | |||||
| return false; | |||||
| } | |||||
| /** | |||||
| * Checks if the current user is NOT GRANTED the {@code role} | |||||
| * | |||||
| * @param role | |||||
| * the {@code role} to check for | |||||
| * @return {@code true} if the current user is NOT GRANTED the {@code role}, else {@code false} | |||||
| */ | |||||
| public static final boolean isNotGranted(String role) { | |||||
| return !isGranted(role); | |||||
| } | |||||
| /** | |||||
| * Checks if the current user is GRANTED ANY of the {@code role}s | |||||
| * | |||||
| * @param roles | |||||
| * the {@code role}s to check for | |||||
| * @return {@code true} if the current user is GRANTED ANY of the {@code role}s, else {@code false} | |||||
| */ | |||||
| public static final boolean isGrantedAny(String... roles) { | |||||
| for (int i = 0; i < roles.length; i++) { | |||||
| if (isGranted(roles[i])) return true; | |||||
| } | |||||
| return false; | |||||
| } | |||||
| /** | |||||
| * Checks if the current user is NOT GRANTED ANY of the {@code role}s | |||||
| * | |||||
| * @param roles | |||||
| * the {@code role}s to check for | |||||
| * @return {@code true} if the current user is NOT GRANTED ANY of the {@code role}s, else {@code false} | |||||
| */ | |||||
| public static final boolean isNotGrantedAny(String... roles) { | |||||
| return !isGrantedAny(roles); | |||||
| } | |||||
| /** | |||||
| * Checks if the current user is GRANTED ALL of the {@code role}s | |||||
| * | |||||
| * @param roles | |||||
| * the {@code role}s to check for | |||||
| * @return {@code true} if the current user is GRANTED ALL of the {@code role}s, else {@code false} | |||||
| */ | |||||
| public static final boolean isGrantedAll(String... roles) { | |||||
| for (int i = 0; i < roles.length; i++) { | |||||
| if (isNotGranted(roles[i])) return false; | |||||
| } | |||||
| return true; | |||||
| } | |||||
| /** | |||||
| * Login a user non-interactively | |||||
| * | |||||
| * @param userService | |||||
| * any implementation of {@link UserDetailsService} | |||||
| * @param username | |||||
| * the username | |||||
| * | |||||
| * @throws UsernameNotFoundException | |||||
| * if the user could not be found or the user has no GrantedAuthority | |||||
| * @throws DataAccessException | |||||
| * if user could not be found for a repository-specific reason | |||||
| */ | |||||
| public static final void loginUser(UserDetailsService userService, String username) { | |||||
| /* load the user, throw exception if user not found */ | |||||
| UserDetails userDetails = userService.loadUserByUsername(username); | |||||
| /* create authentication token for the specified user */ | |||||
| Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities()); | |||||
| getSecurityContext().setAuthentication(authentication); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,59 @@ | |||||
| package com.ffii.core.common; | |||||
| public abstract class SettingNames { | |||||
| /* | |||||
| * System-wide settings | |||||
| */ | |||||
| /** Define all available language names as comma separated string */ | |||||
| public static final String SYS_AVAILABLE_LANGUAGES = "SYS.availableLanguages"; | |||||
| /** Define all available locales as comma separated string */ | |||||
| public static final String SYS_AVAILABLE_LOCALES = "SYS.availableLocales"; | |||||
| /** Define the system default locale as string */ | |||||
| public static final String SYS_DEFAULT_LOCALE = "SYS.defaultLocale"; | |||||
| /** Define the system available currencies as comma separated string */ | |||||
| public static final String SYS_CURRENCIES = "SYS.currencies"; | |||||
| /** Define the system modules (authorities.module) */ | |||||
| public static final String SYS_ROLE_MODULES = "SYS.modules"; | |||||
| /* | |||||
| * Mail settings | |||||
| */ | |||||
| /** Mail - SMTP host */ | |||||
| public static final String MAIL_SMTP_HOST = "MAIL.smtp.host"; | |||||
| /** Mail - SMTP port */ | |||||
| public static final String MAIL_SMTP_PORT = "MAIL.smtp.port"; | |||||
| /** Mail - SMTP username */ | |||||
| public static final String MAIL_SMTP_USERNAME = "MAIL.smtp.username"; | |||||
| /** Mail - SMTP password */ | |||||
| public static final String MAIL_SMTP_PASSWORD = "MAIL.smtp.password"; | |||||
| public static final String MAIL_SMTP_RECIPIENTS = "MAIL.smtp.recipients"; | |||||
| public static final String JS_VERSION = "JS.version"; | |||||
| public static final String REPORT_DAILYMAINT_RECIPIENTS_MECH = "REPORT.dailyMaint.recipients.mech"; | |||||
| public static final String REPORT_DAILYMAINT_RECIPIENTS_VOGUE = "REPORT.dailyMaint.recipients.vogue"; | |||||
| public static final String REPORT_DAILYMAINT_RECIPIENTS_VOGUE_CC = "REPORT.dailyMaint.recipients.vogue.cc"; | |||||
| public static final String SYS_PASSWORD_RULE_MIN = "SYS.password.rule.length.min"; | |||||
| public static final String SYS_PASSWORD_RULE_MAX = "SYS.password.rule.length.max"; | |||||
| public static final String SYS_PASSWORD_RULE_NUMBER = "SYS.password.rule.number"; | |||||
| public static final String SYS_PASSWORD_RULE_UPPER_ENG = "SYS.password.rule.upper.eng"; | |||||
| public static final String SYS_PASSWORD_RULE_LOWER_ENG = "SYS.password.rule.lower.eng"; | |||||
| public static final String SYS_PASSWORD_RULE_SPECIAL = "SYS.password.rule.special"; | |||||
| public static final String AUTO_SCHEDULE_MAX_SCHEDULE_DATE = "AUTO_SCHEDULE.maxScheduleDate"; | |||||
| /** PM_CHECKLIST - vogue's signature */ | |||||
| public static final String PM_CHECKLIST_USER_SIGN_ID = "PM_CHECKLIST.vogueSign"; | |||||
| } | |||||
| @@ -0,0 +1,46 @@ | |||||
| package com.ffii.core.common.file; | |||||
| public class FileRefType { | |||||
| /** ref by User ID */ | |||||
| public static final String USER = "user"; | |||||
| /** ref by User ID */ | |||||
| public static final String USER_IMAGE = "user_image"; | |||||
| /** ref by Printing Order ID */ | |||||
| public static final String PRINT_ORDER = "print_order"; | |||||
| /** ref by Job ID */ | |||||
| public static final String JOB = "job"; | |||||
| /** ref by PM Plan ID */ | |||||
| public static final String PM_PLAN = "pmPlan"; | |||||
| /** ref by Incident ID */ | |||||
| public static final String INCIDENT = "incident"; | |||||
| /** ref by Part Log ID */ | |||||
| public static final String PART_LOG = "partLog"; | |||||
| /** ref by Service Task ID */ | |||||
| public static final String TASK = "task"; | |||||
| /** ref by Service Task ID */ | |||||
| public static final String TASK_QC = "taskQc"; | |||||
| /** ref by Service Task ID */ | |||||
| public static final String EQUIP_ALERT = "equipAlert"; | |||||
| /** ref by User ID */ | |||||
| public static final String USER_SIGN = "userSign"; | |||||
| public static final String MANUFACTURER = "manufacturer"; | |||||
| /** for image ref type validation */ | |||||
| public static final String[] VALID_IMAGE_REF_TYPE = { USER_IMAGE }; | |||||
| /** for ref type (non-image) validation */ | |||||
| public static final String[] VALID_REF_TYPE = { USER, JOB, PRINT_ORDER }; | |||||
| } | |||||
| @@ -0,0 +1,13 @@ | |||||
| package com.ffii.core.common.file.dao; | |||||
| import java.util.Optional; | |||||
| import com.ffii.core.common.file.entity.FileBlob; | |||||
| import com.ffii.core.support.AbstractDao; | |||||
| /** @author Fung */ | |||||
| public interface FileBlobDao extends AbstractDao<FileBlob, Integer> { | |||||
| public Optional<FileBlob> findByFileId(int fileId); | |||||
| public int deleteByFileId(int fileId); | |||||
| } | |||||
| @@ -0,0 +1,15 @@ | |||||
| package com.ffii.core.common.file.dao; | |||||
| import java.util.Optional; | |||||
| import com.ffii.core.common.file.entity.File; | |||||
| import com.ffii.core.support.AbstractDao; | |||||
| /** @author Fung */ | |||||
| public interface FileDao extends AbstractDao<File, Integer> { | |||||
| public Optional<File> findByIdAndSkey(int id, String skey); | |||||
| public Optional<File> findByIdAndSkeyAndFilenameAndExtension(int id, String skey, String filename, String extension); | |||||
| public int deleteByIdAndSkey(int id, String skey); | |||||
| } | |||||
| @@ -0,0 +1,17 @@ | |||||
| package com.ffii.core.common.file.dao; | |||||
| import java.util.Optional; | |||||
| import com.ffii.core.common.file.entity.FileRef; | |||||
| import com.ffii.core.support.AbstractDao; | |||||
| /** @author Fung */ | |||||
| public interface FileRefDao extends AbstractDao<FileRef, Integer> { | |||||
| public Optional<FileRef> findByFileId(int fileId); | |||||
| public Optional<FileRef> findByRefType(String refType); | |||||
| public Optional<FileRef> findByRefTypeAndRefId(String refType, int refId); | |||||
| public int deleteByFileId(int fileId); | |||||
| } | |||||
| @@ -0,0 +1,87 @@ | |||||
| package com.ffii.core.common.file.entity; | |||||
| import javax.persistence.Column; | |||||
| import javax.persistence.Entity; | |||||
| import javax.persistence.Table; | |||||
| import javax.validation.constraints.NotBlank; | |||||
| import javax.validation.constraints.NotNull; | |||||
| import com.ffii.core.entity.BaseEntity; | |||||
| /** @author Fung */ | |||||
| @Entity | |||||
| @Table(name = "file") | |||||
| public class File extends BaseEntity<Integer> { | |||||
| @NotBlank | |||||
| @Column | |||||
| private String skey; | |||||
| @NotBlank | |||||
| @Column | |||||
| private String filename; | |||||
| @NotBlank | |||||
| @Column | |||||
| private String extension; | |||||
| @NotBlank | |||||
| @Column | |||||
| private String mimetype; | |||||
| @NotNull | |||||
| @Column | |||||
| private Long filesize; | |||||
| @Column | |||||
| private String remarks; | |||||
| public String getSkey() { | |||||
| return this.skey; | |||||
| } | |||||
| public void setSkey(String skey) { | |||||
| this.skey = skey; | |||||
| } | |||||
| public String getFilename() { | |||||
| return this.filename; | |||||
| } | |||||
| public void setFilename(String filename) { | |||||
| this.filename = filename; | |||||
| } | |||||
| public String getExtension() { | |||||
| return this.extension; | |||||
| } | |||||
| public void setExtension(String extension) { | |||||
| this.extension = extension; | |||||
| } | |||||
| public String getMimetype() { | |||||
| return this.mimetype; | |||||
| } | |||||
| public void setMimetype(String mimetype) { | |||||
| this.mimetype = mimetype; | |||||
| } | |||||
| public Long getFilesize() { | |||||
| return this.filesize; | |||||
| } | |||||
| public void setFilesize(Long filesize) { | |||||
| this.filesize = filesize; | |||||
| } | |||||
| public String getRemarks() { | |||||
| return this.remarks; | |||||
| } | |||||
| public void setRemarks(String remarks) { | |||||
| this.remarks = remarks; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,47 @@ | |||||
| package com.ffii.core.common.file.entity; | |||||
| import javax.persistence.Column; | |||||
| import javax.persistence.Entity; | |||||
| import javax.persistence.Table; | |||||
| import javax.validation.constraints.NotNull; | |||||
| import com.ffii.core.entity.IdEntity; | |||||
| /** @author Fung */ | |||||
| @Entity | |||||
| @Table(name = "file_blob") | |||||
| public class FileBlob extends IdEntity<Integer> { | |||||
| @NotNull | |||||
| @Column | |||||
| private Integer fileId; | |||||
| @NotNull | |||||
| @Column | |||||
| private byte[] bytes; | |||||
| public FileBlob() { | |||||
| } | |||||
| public FileBlob(Integer fileId, byte[] bytes) { | |||||
| this.fileId = fileId; | |||||
| this.bytes = bytes; | |||||
| } | |||||
| public Integer getFileId() { | |||||
| return this.fileId; | |||||
| } | |||||
| public void setFileId(Integer fileId) { | |||||
| this.fileId = fileId; | |||||
| } | |||||
| public byte[] getBytes() { | |||||
| return this.bytes; | |||||
| } | |||||
| public void setBytes(byte[] bytes) { | |||||
| this.bytes = bytes; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,74 @@ | |||||
| package com.ffii.core.common.file.entity; | |||||
| import javax.persistence.Column; | |||||
| import javax.persistence.Entity; | |||||
| import javax.persistence.Table; | |||||
| import javax.validation.constraints.NotBlank; | |||||
| import javax.validation.constraints.NotNull; | |||||
| import com.ffii.core.entity.IdEntity; | |||||
| /** @author Fung */ | |||||
| @Entity | |||||
| @Table(name = "file_ref") | |||||
| public class FileRef extends IdEntity<Integer> { | |||||
| @NotBlank | |||||
| @Column | |||||
| private String refType; | |||||
| @NotNull | |||||
| @Column | |||||
| private Integer refId; | |||||
| @Column | |||||
| private String refCode; | |||||
| @NotNull | |||||
| @Column | |||||
| private Integer fileId; | |||||
| public FileRef() { | |||||
| } | |||||
| public FileRef(String refType, Integer refId, String refCode, Integer fileId) { | |||||
| this.refType = refType; | |||||
| this.refId = refId; | |||||
| this.refCode = refCode; | |||||
| this.fileId = fileId; | |||||
| } | |||||
| public String getRefType() { | |||||
| return this.refType; | |||||
| } | |||||
| public void setRefType(String refType) { | |||||
| this.refType = refType; | |||||
| } | |||||
| public Integer getRefId() { | |||||
| return this.refId; | |||||
| } | |||||
| public void setRefId(Integer refId) { | |||||
| this.refId = refId; | |||||
| } | |||||
| public String getRefCode() { | |||||
| return this.refCode; | |||||
| } | |||||
| public void setRefCode(String refCode) { | |||||
| this.refCode = refCode; | |||||
| } | |||||
| public Integer getFileId() { | |||||
| return this.fileId; | |||||
| } | |||||
| public void setFileId(Integer fileId) { | |||||
| this.fileId = fileId; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,169 @@ | |||||
| package com.ffii.core.common.file.service; | |||||
| import java.io.IOException; | |||||
| import java.util.List; | |||||
| import java.util.Map; | |||||
| import java.util.Optional; | |||||
| import javax.validation.Valid; | |||||
| import org.apache.commons.io.FilenameUtils; | |||||
| import org.apache.commons.lang3.RandomStringUtils; | |||||
| import org.apache.commons.lang3.tuple.ImmutablePair; | |||||
| import org.apache.commons.lang3.tuple.Pair; | |||||
| import org.apache.tika.Tika; | |||||
| import org.springframework.beans.BeanUtils; | |||||
| import org.springframework.stereotype.Service; | |||||
| import org.springframework.transaction.annotation.Transactional; | |||||
| import org.springframework.util.Assert; | |||||
| import org.springframework.web.multipart.MultipartFile; | |||||
| import com.ffii.core.exception.NotFoundException; | |||||
| import com.ffii.core.support.AbstractService; | |||||
| import com.ffii.core.support.JdbcDao; | |||||
| import com.ffii.core.common.file.dao.FileBlobDao; | |||||
| import com.ffii.core.common.file.dao.FileDao; | |||||
| import com.ffii.core.common.file.dao.FileRefDao; | |||||
| import com.ffii.core.common.file.entity.File; | |||||
| import com.ffii.core.common.file.entity.FileBlob; | |||||
| import com.ffii.core.common.file.entity.FileRef; | |||||
| import com.ffii.core.common.file.web.FileController.UpdateFileInfoReq; | |||||
| /** @author Fung */ | |||||
| @Service | |||||
| public class FileService extends AbstractService { | |||||
| private FileDao dao; | |||||
| private FileRefDao refDao; | |||||
| private FileBlobDao blobDao; | |||||
| public FileService(JdbcDao jdbcDao, FileDao dao, FileRefDao refDao, FileBlobDao blobDao) { | |||||
| super(jdbcDao); | |||||
| this.dao = dao; | |||||
| this.refDao = refDao; | |||||
| this.blobDao = blobDao; | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public Map<String, Object> upload(Integer refId, String refType, String refCode, MultipartFile multipartFile, | |||||
| String remarks) | |||||
| throws IOException { | |||||
| Assert.notNull(refType, "refType cannot null"); | |||||
| Assert.notNull(refId, "refId cannot null"); | |||||
| Assert.notNull(multipartFile, "multipartFile cannot null"); | |||||
| String filename = multipartFile.getOriginalFilename(); | |||||
| File file = new File(); | |||||
| file.setSkey(RandomStringUtils.randomAlphanumeric(32)); | |||||
| file.setFilename(FilenameUtils.getBaseName(filename)); | |||||
| file.setExtension(FilenameUtils.getExtension(filename)); | |||||
| file.setMimetype(new Tika().detect(filename)); | |||||
| file.setFilesize(multipartFile.getSize()); | |||||
| dao.save(file); | |||||
| blobDao.save(new FileBlob(file.getId(), multipartFile.getBytes())); | |||||
| refDao.save(new FileRef(refType, refId, refCode, file.getId())); | |||||
| return Map.of( | |||||
| "id", file.getId(), | |||||
| "refId", refId == null ? "" : refId, | |||||
| "refType", refType == null ? "" : refType, | |||||
| "refCode", refCode == null ? "" : refCode, | |||||
| "skey", file.getSkey(), | |||||
| "filename", file.getFilename(), | |||||
| "extension", file.getExtension(), | |||||
| "filesize", file.getFilesize(), | |||||
| "mimetype", file.getMimetype()); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public Map<String, Object> upload(Integer refId, String refType, String refCode, MultipartFile multipartFile, | |||||
| String filename, | |||||
| String remarks) | |||||
| throws IOException { | |||||
| Assert.notNull(refType, "refType cannot null"); | |||||
| Assert.notNull(refId, "refId cannot null"); | |||||
| Assert.notNull(multipartFile, "multipartFile cannot null"); | |||||
| File file = new File(); | |||||
| file.setSkey(RandomStringUtils.randomAlphanumeric(32)); | |||||
| file.setFilename(FilenameUtils.getBaseName(filename)); | |||||
| file.setExtension(FilenameUtils.getExtension(filename)); | |||||
| file.setMimetype(new Tika().detect(filename)); | |||||
| file.setFilesize(multipartFile.getSize()); | |||||
| dao.save(file); | |||||
| blobDao.save(new FileBlob(file.getId(), multipartFile.getBytes())); | |||||
| refDao.save(new FileRef(refType, refId, refCode, file.getId())); | |||||
| return Map.of( | |||||
| "id", file.getId(), | |||||
| "refId", refId == null ? "" : refId, | |||||
| "refType", refType == null ? "" : refType, | |||||
| "refCode", refCode == null ? "" : refCode, | |||||
| "skey", file.getSkey(), | |||||
| "filename", file.getFilename(), | |||||
| "extension", file.getExtension(), | |||||
| "filesize", file.getFilesize(), | |||||
| "mimetype", file.getMimetype()); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public boolean delete(int id, String skey) { | |||||
| if (dao.deleteByIdAndSkey(id, skey) > 0) { | |||||
| blobDao.deleteByFileId(id); | |||||
| refDao.deleteByFileId(id); | |||||
| return true; | |||||
| } else { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| public Pair<File, FileBlob> loadFile(int id, String skey, String filename) { | |||||
| Optional<File> opt = dao.findByIdAndSkeyAndFilenameAndExtension(id, skey, | |||||
| FilenameUtils.getBaseName(filename), | |||||
| FilenameUtils.getExtension(filename)); | |||||
| return opt.isPresent() ? ImmutablePair.of(opt.get(), blobDao.findByFileId(id).orElse(null)) | |||||
| : ImmutablePair.nullPair(); | |||||
| } | |||||
| // #App | |||||
| public List<Map<String, Object>> list(String refType, int refId) { | |||||
| return jdbcDao.queryForList( | |||||
| "SELECT" | |||||
| + " ref.fileId," | |||||
| + " ref.refType," | |||||
| + " ref.refId," | |||||
| + " ref.refCode," | |||||
| + " f.skey," | |||||
| + " f.filename," | |||||
| + " f.extension," | |||||
| + " f.mimetype," | |||||
| + " f.filesize," | |||||
| + " f.remarks," | |||||
| + " f.created," | |||||
| + " f.createdBy," | |||||
| + " u.name AS createdByName" | |||||
| + " FROM file_ref ref" | |||||
| + " INNER JOIN file f ON f.id = ref.fileId" | |||||
| + " LEFT JOIN user u ON u.username = f.createdBy" | |||||
| + " WHERE ref.refType = :refType" | |||||
| + " AND ref.refId = :refId", | |||||
| Map.of("refType", refType, "refId", refId)); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void updateFileInfo(int id, String skey, @Valid UpdateFileInfoReq req) { | |||||
| File file = dao.findByIdAndSkey(id, skey).orElseThrow(NotFoundException::new); | |||||
| BeanUtils.copyProperties(req, file); | |||||
| dao.save(file); | |||||
| } | |||||
| public Optional<FileRef> findRefByFileId(int fileId) { | |||||
| return refDao.findByFileId(fileId); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public FileRef saveRef(FileRef ref) { | |||||
| return refDao.save(ref); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,78 @@ | |||||
| package com.ffii.core.common.file.web; | |||||
| import java.util.Map; | |||||
| import javax.validation.Valid; | |||||
| import javax.validation.constraints.NotBlank; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.web.bind.annotation.DeleteMapping; | |||||
| import org.springframework.web.bind.annotation.GetMapping; | |||||
| import org.springframework.web.bind.annotation.PatchMapping; | |||||
| import org.springframework.web.bind.annotation.PathVariable; | |||||
| import org.springframework.web.bind.annotation.RequestBody; | |||||
| import org.springframework.web.bind.annotation.RequestMapping; | |||||
| import org.springframework.web.bind.annotation.ResponseStatus; | |||||
| import org.springframework.web.bind.annotation.RestController; | |||||
| import com.ffii.core.common.file.service.FileService; | |||||
| import com.ffii.core.exception.NotFoundException; | |||||
| import com.ffii.core.response.RecordsRes; | |||||
| /** @author Fung */ | |||||
| @RestController | |||||
| @RequestMapping("/protected/file") | |||||
| public class FileController { | |||||
| private FileService service; | |||||
| public FileController(FileService service) { | |||||
| this.service = service; | |||||
| } | |||||
| @GetMapping("/{refType}/{refId}") | |||||
| public RecordsRes<Map<String, Object>> list(@PathVariable String refType, @PathVariable int refId) { | |||||
| return new RecordsRes<>(service.list(refType, refId)); | |||||
| } | |||||
| // #App delete | |||||
| @DeleteMapping("/{id}/{skey}") | |||||
| @ResponseStatus(HttpStatus.NO_CONTENT) | |||||
| public void delete(@PathVariable int id, @PathVariable String skey) { | |||||
| if (!service.delete(id, skey)) { | |||||
| throw new NotFoundException(); | |||||
| } | |||||
| } | |||||
| // #App updateFileInfo | |||||
| @PatchMapping("/{id}/{skey}") | |||||
| @ResponseStatus(HttpStatus.NO_CONTENT) | |||||
| public void updateFileInfo(@PathVariable int id, @PathVariable String skey, | |||||
| @RequestBody @Valid UpdateFileInfoReq req) { | |||||
| service.updateFileInfo(id, skey, req); | |||||
| } | |||||
| public static class UpdateFileInfoReq { | |||||
| @NotBlank | |||||
| private String filename; | |||||
| private String remarks; | |||||
| public String getRemarks() { | |||||
| return remarks; | |||||
| } | |||||
| public void setRemarks(String remarks) { | |||||
| this.remarks = remarks; | |||||
| } | |||||
| public String getFilename() { | |||||
| return filename; | |||||
| } | |||||
| public void setFilename(String filename) { | |||||
| this.filename = filename; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,125 @@ | |||||
| package com.ffii.core.common.file.web; | |||||
| import java.awt.image.BufferedImage; | |||||
| import java.io.ByteArrayInputStream; | |||||
| import java.io.ByteArrayOutputStream; | |||||
| import java.io.IOException; | |||||
| import java.net.URLEncoder; | |||||
| import javax.imageio.ImageIO; | |||||
| import javax.servlet.ServletOutputStream; | |||||
| import javax.servlet.http.HttpServletResponse; | |||||
| import org.apache.commons.lang3.tuple.Pair; | |||||
| import org.springframework.web.bind.annotation.GetMapping; | |||||
| import org.springframework.web.bind.annotation.PathVariable; | |||||
| import org.springframework.web.bind.annotation.RequestMapping; | |||||
| import org.springframework.web.bind.annotation.RequestParam; | |||||
| import org.springframework.web.bind.annotation.RestController; | |||||
| import com.ffii.core.common.file.entity.File; | |||||
| import com.ffii.core.common.file.entity.FileBlob; | |||||
| import com.ffii.core.common.file.service.FileService; | |||||
| import com.ffii.core.exception.NotFoundException; | |||||
| import com.ffii.core.support.AbstractController; | |||||
| /** @author Fung */ | |||||
| @RestController | |||||
| @RequestMapping("/protected/file/dl") | |||||
| public class FileDownloadController extends AbstractController { | |||||
| private FileService service; | |||||
| public FileDownloadController(FileService service) { | |||||
| this.service = service; | |||||
| } | |||||
| @GetMapping("/{id}/{skey}/{filename}") | |||||
| public void download( | |||||
| HttpServletResponse response, | |||||
| @RequestParam(defaultValue = "false") boolean dl, | |||||
| @PathVariable int id, | |||||
| @PathVariable String skey, | |||||
| @PathVariable String filename) throws IOException { | |||||
| Pair<File, FileBlob> pair = service.loadFile(id, skey, filename); | |||||
| if (pair.getLeft() == null || pair.getRight() == null) { | |||||
| throw new NotFoundException(); | |||||
| } | |||||
| File file = pair.getLeft(); | |||||
| FileBlob blob = pair.getRight(); | |||||
| response.setContentType(file.getMimetype()); | |||||
| response.setContentLengthLong(file.getFilesize()); | |||||
| response.setHeader("content-disposition", | |||||
| String.format("%s; filename=\"%s\"", dl ? "attachment" : "inline", | |||||
| URLEncoder.encode(filename, "UTF-8"))); | |||||
| try (final ServletOutputStream out = response.getOutputStream()) { | |||||
| out.write(blob.getBytes()); | |||||
| } | |||||
| } | |||||
| @GetMapping("/thumbnail/{id}/{skey}/{filename}") | |||||
| public void thumbnail(HttpServletResponse response, @RequestParam(defaultValue = "false") boolean dl, | |||||
| @PathVariable int id, @PathVariable String skey, | |||||
| @PathVariable String filename) throws IOException { | |||||
| Pair<File, FileBlob> pair = service.loadFile(id, skey, filename); | |||||
| if (pair.getLeft() == null || pair.getRight() == null) { | |||||
| throw new NotFoundException(); | |||||
| } | |||||
| File file = pair.getLeft(); | |||||
| FileBlob blob = pair.getRight(); | |||||
| response.reset(); | |||||
| response.setContentType(file.getMimetype()); | |||||
| // response.setContentLength((int) file.getFilesize()); | |||||
| response.setHeader("Content-Transfer-Encoding", "binary"); | |||||
| response.setHeader("Content-Disposition", String.format("%s; filename=\"%s\"", dl ? "attachment" : "inline", | |||||
| response.encodeURL(file.getFilename() + "." + file.getExtension()))); | |||||
| int limit = 100; | |||||
| BufferedImage image = ImageIO.read(new ByteArrayInputStream(blob.getBytes())); | |||||
| int width = image.getWidth(); | |||||
| int height = image.getHeight(); | |||||
| if (width > height) { | |||||
| if (width > limit) { | |||||
| height = height * limit / width; | |||||
| width = limit; | |||||
| } | |||||
| } else { | |||||
| if (height > limit) { | |||||
| width = width * limit / height; | |||||
| height = limit; | |||||
| } | |||||
| } | |||||
| image = scale(image, width, height); | |||||
| try (ByteArrayOutputStream tmp = new ByteArrayOutputStream()) { | |||||
| ImageIO.write(image, file.getExtension(), tmp); | |||||
| response.setContentLength((int) tmp.size()); | |||||
| try (ServletOutputStream out = response.getOutputStream()) { | |||||
| out.write(tmp.toByteArray()); | |||||
| } catch (IOException e) { | |||||
| logger.warn(e.getMessage()); | |||||
| } | |||||
| } | |||||
| } | |||||
| public static BufferedImage scale(BufferedImage originalImage, int w, int h) { | |||||
| BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); | |||||
| int x, y; | |||||
| int ww = originalImage.getWidth(); | |||||
| int hh = originalImage.getHeight(); | |||||
| for (x = 0; x < w; x++) { | |||||
| for (y = 0; y < h; y++) { | |||||
| int col = originalImage.getRGB(x * ww / w, y * hh / h); | |||||
| img.setRGB(x, y, col); | |||||
| } | |||||
| } | |||||
| return img; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,80 @@ | |||||
| package com.ffii.core.common.file.web; | |||||
| import java.util.ArrayList; | |||||
| import java.util.List; | |||||
| import java.util.Map; | |||||
| import org.springframework.beans.factory.annotation.Value; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.web.bind.annotation.PathVariable; | |||||
| import org.springframework.web.bind.annotation.PostMapping; | |||||
| import org.springframework.web.bind.annotation.RequestMapping; | |||||
| import org.springframework.web.bind.annotation.RequestParam; | |||||
| import org.springframework.web.bind.annotation.ResponseStatus; | |||||
| import org.springframework.web.bind.annotation.RestController; | |||||
| import org.springframework.web.multipart.MultipartFile; | |||||
| import org.springframework.web.server.ResponseStatusException; | |||||
| import com.ffii.core.common.SecurityUtils; | |||||
| import com.ffii.core.common.file.FileRefType; | |||||
| import com.ffii.core.common.file.service.FileService; | |||||
| /** @author Fung */ | |||||
| @RestController | |||||
| @RequestMapping("/protected/file/ul") | |||||
| public class FileUploadController { | |||||
| private final int MAX_FILE_SIZE; | |||||
| private FileService service; | |||||
| public FileUploadController( | |||||
| @Value("${app.upload.max-size:20971520}") int maxFileSize, | |||||
| FileService service) { | |||||
| this.MAX_FILE_SIZE = maxFileSize; | |||||
| this.service = service; | |||||
| } | |||||
| @PostMapping("/{refType}/{refId}") | |||||
| @ResponseStatus(HttpStatus.CREATED) | |||||
| public Map<String, Object> upload( | |||||
| @PathVariable String refType, | |||||
| @PathVariable int refId, | |||||
| @RequestParam(required = false) String refCode, | |||||
| @RequestParam(required = false) String remarks, | |||||
| @RequestParam(required = false) Boolean needReturn, | |||||
| @RequestParam List<MultipartFile> multipartFiles) { | |||||
| checkAuth(refType); | |||||
| List<String> reason = new ArrayList<>(); | |||||
| List<Map<String, Object>> fileList = new ArrayList<>(); | |||||
| multipartFiles | |||||
| .forEach(multipartFile -> { | |||||
| if (multipartFile.getSize() == 0L) { | |||||
| reason.add(multipartFile.getOriginalFilename() + " size zero."); | |||||
| } else if (multipartFile.getSize() > MAX_FILE_SIZE) { | |||||
| reason.add(multipartFile.getOriginalFilename() + " size too large."); | |||||
| } else { | |||||
| try { | |||||
| Map<String, Object> ref = this.service.upload(refId, refType, refCode, multipartFile, | |||||
| remarks); | |||||
| fileList.add(ref); | |||||
| } catch (Exception e) { | |||||
| e.printStackTrace(); | |||||
| reason.add(multipartFile.getOriginalFilename() + " cannot upload."); | |||||
| } | |||||
| } | |||||
| }); | |||||
| if (needReturn != null && needReturn) { | |||||
| return Map.of("errors", reason, "files", fileList); | |||||
| } | |||||
| return Map.of("errors", reason); | |||||
| } | |||||
| private void checkAuth(String refType) { | |||||
| if (FileRefType.MANUFACTURER.equals(refType) && SecurityUtils.isNotGranted("EDIT_MANUFACTURER")) { | |||||
| throw new ResponseStatusException(HttpStatus.FORBIDDEN); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,11 @@ | |||||
| package com.ffii.core.common.id.dao; | |||||
| import java.util.Optional; | |||||
| import com.ffii.core.support.AbstractDao; | |||||
| import com.ffii.core.common.id.entity.IdCounter; | |||||
| /** @author Fung */ | |||||
| public interface IdCounterDao extends AbstractDao<IdCounter, Integer> { | |||||
| public Optional<IdCounter> findByName(String name); | |||||
| } | |||||
| @@ -0,0 +1,56 @@ | |||||
| package com.ffii.core.common.id.entity; | |||||
| import javax.persistence.Column; | |||||
| import javax.persistence.Entity; | |||||
| import javax.persistence.Table; | |||||
| import javax.validation.constraints.NotBlank; | |||||
| import javax.validation.constraints.NotNull; | |||||
| import com.ffii.core.entity.IdEntity; | |||||
| /** @author Fung */ | |||||
| @Entity | |||||
| @Table(name = "id_counter") | |||||
| public class IdCounter extends IdEntity<Integer> { | |||||
| @NotBlank | |||||
| @Column | |||||
| private String name; | |||||
| @NotNull | |||||
| @Column | |||||
| private Integer count; | |||||
| public IdCounter() { | |||||
| } | |||||
| public IdCounter(String name) { | |||||
| this(name, 0); | |||||
| } | |||||
| public IdCounter(String name, int length) { | |||||
| this.name = name; | |||||
| this.count = 0; | |||||
| } | |||||
| public void addOneCount() { | |||||
| this.setCount(this.getCount() + 1); | |||||
| } | |||||
| public String getName() { | |||||
| return this.name; | |||||
| } | |||||
| public void setName(String name) { | |||||
| this.name = name; | |||||
| } | |||||
| public Integer getCount() { | |||||
| return this.count; | |||||
| } | |||||
| public void setCount(Integer count) { | |||||
| this.count = count; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,28 @@ | |||||
| package com.ffii.core.common.id.service; | |||||
| import org.springframework.stereotype.Service; | |||||
| import org.springframework.transaction.annotation.Isolation; | |||||
| import org.springframework.transaction.annotation.Propagation; | |||||
| import org.springframework.transaction.annotation.Transactional; | |||||
| import com.ffii.core.common.id.dao.IdCounterDao; | |||||
| import com.ffii.core.common.id.entity.IdCounter; | |||||
| import com.ffii.core.support.AbstractIdEntityService; | |||||
| import com.ffii.core.support.JdbcDao; | |||||
| /** @author Fung */ | |||||
| @Service | |||||
| public class IdCounterService extends AbstractIdEntityService<IdCounter, Integer, IdCounterDao> { | |||||
| public IdCounterService(JdbcDao jdbcDao, IdCounterDao dao) { | |||||
| super(jdbcDao, dao); | |||||
| } | |||||
| @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE) | |||||
| public String getNextIdCount(String name, int length) { | |||||
| IdCounter instance = dao.findByName(name).orElseGet(() -> new IdCounter(name)); | |||||
| instance.addOneCount(); | |||||
| save(instance); | |||||
| return String.format("%0" + length + "d", instance.getCount()); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,356 @@ | |||||
| package com.ffii.core.common.mail.pojo; | |||||
| import java.util.ArrayList; | |||||
| import java.util.HashMap; | |||||
| import java.util.List; | |||||
| import java.util.Map; | |||||
| import javax.mail.internet.AddressException; | |||||
| import javax.mail.internet.InternetAddress; | |||||
| public class MailRequest { | |||||
| public final static int PRIORITY_HIGHEST = 1; | |||||
| public final static int PRIORITY_HIGH = 2; | |||||
| public final static int PRIORITY_NORMAL = 3; | |||||
| public final static int PRIORITY_LOW = 4; | |||||
| public final static int PRIORITY_LOWEST = 5; | |||||
| private InternetAddress from; | |||||
| private List<InternetAddress> to; | |||||
| private String subject; | |||||
| private String template; | |||||
| private Map<String, ?> args; | |||||
| private Integer priority; | |||||
| private InternetAddress replyTo; | |||||
| private List<InternetAddress> cc; | |||||
| private List<InternetAddress> bcc; | |||||
| private Map<String, byte[]> attachments; | |||||
| public MailRequest() { | |||||
| } | |||||
| public static Builder builder() { | |||||
| return new Builder(); | |||||
| } | |||||
| public void addAttachment(String attachmentFilename, byte[] byteArray) { | |||||
| if (this.attachments == null) | |||||
| this.attachments = new HashMap<>(); | |||||
| this.attachments.put(attachmentFilename, byteArray); | |||||
| } | |||||
| public void addTo(InternetAddress to) { | |||||
| if (this.to == null) | |||||
| this.to = new ArrayList<>(); | |||||
| this.to.add(to); | |||||
| } | |||||
| public void addCc(InternetAddress cc) { | |||||
| if (this.cc == null) | |||||
| this.cc = new ArrayList<>(); | |||||
| this.cc.add(cc); | |||||
| } | |||||
| public void addBcc(InternetAddress bcc) { | |||||
| if (this.bcc == null) | |||||
| this.bcc = new ArrayList<>(); | |||||
| this.bcc.add(bcc); | |||||
| } | |||||
| // getter setter | |||||
| public InternetAddress getFrom() { | |||||
| return from; | |||||
| } | |||||
| public void setFrom(InternetAddress from) { | |||||
| this.from = from; | |||||
| } | |||||
| public List<InternetAddress> getTo() { | |||||
| return to; | |||||
| } | |||||
| public void setTo(List<InternetAddress> to) { | |||||
| this.to = to; | |||||
| } | |||||
| public void setTo(String[] to) throws AddressException { | |||||
| if (to == null) { | |||||
| this.to = null; | |||||
| } else { | |||||
| for (String a : to) { | |||||
| this.addTo(new InternetAddress(a)); | |||||
| } | |||||
| } | |||||
| } | |||||
| public void setStringListTo(List<String> to) throws AddressException { | |||||
| if (to == null) { | |||||
| this.to = null; | |||||
| } else { | |||||
| for (String a : to) { | |||||
| this.addTo(new InternetAddress(a)); | |||||
| } | |||||
| } | |||||
| } | |||||
| public String getSubject() { | |||||
| return subject; | |||||
| } | |||||
| public void setSubject(String subject) { | |||||
| this.subject = subject; | |||||
| } | |||||
| public String getTemplate() { | |||||
| return template; | |||||
| } | |||||
| public void setTemplate(String template) { | |||||
| this.template = template; | |||||
| } | |||||
| public Map<String, ?> getArgs() { | |||||
| return args; | |||||
| } | |||||
| public void setArgs(Map<String, ?> args) { | |||||
| this.args = args; | |||||
| } | |||||
| public InternetAddress getReplyTo() { | |||||
| return replyTo; | |||||
| } | |||||
| public void setReplyTo(InternetAddress replyTo) { | |||||
| this.replyTo = replyTo; | |||||
| } | |||||
| public List<InternetAddress> getCc() { | |||||
| return cc; | |||||
| } | |||||
| public void setCc(List<InternetAddress> cc) { | |||||
| this.cc = cc; | |||||
| } | |||||
| public void setCc(String[] cc) throws AddressException { | |||||
| if (cc == null) { | |||||
| this.cc = null; | |||||
| } else { | |||||
| for (String a : cc) { | |||||
| this.addCc(new InternetAddress(a)); | |||||
| } | |||||
| } | |||||
| } | |||||
| public void setStringListCc(List<String> cc) throws AddressException { | |||||
| if (cc == null) { | |||||
| this.cc = null; | |||||
| } else { | |||||
| for (String a : cc) { | |||||
| this.addCc(new InternetAddress(a)); | |||||
| } | |||||
| } | |||||
| } | |||||
| public List<InternetAddress> getBcc() { | |||||
| return bcc; | |||||
| } | |||||
| public void setBcc(List<InternetAddress> bcc) { | |||||
| this.bcc = bcc; | |||||
| } | |||||
| public void setBcc(String[] bcc) throws AddressException { | |||||
| if (bcc == null) { | |||||
| this.bcc = null; | |||||
| } else { | |||||
| for (String a : bcc) { | |||||
| this.addBcc(new InternetAddress(a)); | |||||
| } | |||||
| } | |||||
| } | |||||
| public void setStringListBcc(List<String> bcc) throws AddressException { | |||||
| if (bcc == null) { | |||||
| this.bcc = null; | |||||
| } else { | |||||
| for (String a : bcc) { | |||||
| this.addBcc(new InternetAddress(a)); | |||||
| } | |||||
| } | |||||
| } | |||||
| public Map<String, byte[]> getAttachments() { | |||||
| return attachments; | |||||
| } | |||||
| public void setAttachments(Map<String, byte[]> attachments) { | |||||
| this.attachments = attachments; | |||||
| } | |||||
| public Integer getPriority() { | |||||
| return priority; | |||||
| } | |||||
| public void setPriority(Integer priority) { | |||||
| this.priority = priority; | |||||
| } | |||||
| // classes | |||||
| public static class Builder { | |||||
| private MailRequest mailRequest; | |||||
| private Builder() { | |||||
| this.mailRequest = new MailRequest(); | |||||
| } | |||||
| public MailRequest build() { | |||||
| return this.mailRequest; | |||||
| } | |||||
| public Builder addAttachment(String attachmentFilename, byte[] byteArray) { | |||||
| this.mailRequest.addAttachment(attachmentFilename, byteArray); | |||||
| return this; | |||||
| } | |||||
| public Builder addTo(InternetAddress to) { | |||||
| this.mailRequest.addTo(to); | |||||
| return this; | |||||
| } | |||||
| public Builder addCc(InternetAddress cc) { | |||||
| this.mailRequest.addCc(cc); | |||||
| return this; | |||||
| } | |||||
| public Builder addBcc(InternetAddress bcc) { | |||||
| this.mailRequest.addBcc(bcc); | |||||
| return this; | |||||
| } | |||||
| public Builder from(InternetAddress from) { | |||||
| this.mailRequest.setFrom(from); | |||||
| return this; | |||||
| } | |||||
| public Builder to(List<InternetAddress> to) { | |||||
| this.mailRequest.setTo(to); | |||||
| return this; | |||||
| } | |||||
| public Builder to(String[] to) throws AddressException { | |||||
| if (to == null) { | |||||
| this.mailRequest.setTo((List<InternetAddress>) null); | |||||
| } else { | |||||
| for (String a : to) { | |||||
| this.addTo(new InternetAddress(a)); | |||||
| } | |||||
| } | |||||
| return this; | |||||
| } | |||||
| public Builder stringListTo(List<String> to) throws AddressException { | |||||
| if (to == null) { | |||||
| this.mailRequest.setTo((List<InternetAddress>) null); | |||||
| } else { | |||||
| for (String a : to) { | |||||
| this.addTo(new InternetAddress(a)); | |||||
| } | |||||
| } | |||||
| return this; | |||||
| } | |||||
| public Builder subject(String subject) { | |||||
| this.mailRequest.setSubject(subject); | |||||
| return this; | |||||
| } | |||||
| public Builder template(String template) { | |||||
| this.mailRequest.setTemplate(template); | |||||
| return this; | |||||
| } | |||||
| public Builder args(Map<String, ?> args) { | |||||
| this.mailRequest.setArgs(args); | |||||
| return this; | |||||
| } | |||||
| public Builder replyTo(InternetAddress replyTo) { | |||||
| this.mailRequest.setReplyTo(replyTo); | |||||
| return this; | |||||
| } | |||||
| public Builder cc(List<InternetAddress> cc) { | |||||
| this.mailRequest.setCc(cc); | |||||
| return this; | |||||
| } | |||||
| public Builder cc(String[] cc) throws AddressException { | |||||
| if (cc == null) { | |||||
| this.mailRequest.setCc((List<InternetAddress>) null); | |||||
| } else { | |||||
| for (String a : cc) { | |||||
| this.addCc(new InternetAddress(a)); | |||||
| } | |||||
| } | |||||
| return this; | |||||
| } | |||||
| public Builder stringListCc(List<String> cc) throws AddressException { | |||||
| if (cc == null) { | |||||
| this.mailRequest.setCc((List<InternetAddress>) null); | |||||
| } else { | |||||
| for (String a : cc) { | |||||
| this.addCc(new InternetAddress(a)); | |||||
| } | |||||
| } | |||||
| return this; | |||||
| } | |||||
| public Builder bcc(List<InternetAddress> bcc) { | |||||
| this.mailRequest.setBcc(bcc); | |||||
| return this; | |||||
| } | |||||
| public Builder bcc(String[] bcc) throws AddressException { | |||||
| if (bcc == null) { | |||||
| this.mailRequest.setBcc((List<InternetAddress>) null); | |||||
| } else { | |||||
| for (String a : bcc) { | |||||
| this.addCc(new InternetAddress(a)); | |||||
| } | |||||
| } | |||||
| return this; | |||||
| } | |||||
| public Builder stringListBcc(List<String> bcc) throws AddressException { | |||||
| if (bcc == null) { | |||||
| this.mailRequest.setBcc((List<InternetAddress>) null); | |||||
| } else { | |||||
| for (String a : bcc) { | |||||
| this.addCc(new InternetAddress(a)); | |||||
| } | |||||
| } | |||||
| return this; | |||||
| } | |||||
| public Builder attachments(Map<String, byte[]> attachments) { | |||||
| this.mailRequest.setAttachments(attachments); | |||||
| return this; | |||||
| } | |||||
| public Builder priority(Integer priority) { | |||||
| this.mailRequest.setPriority(priority); | |||||
| return this; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,49 @@ | |||||
| package com.ffii.core.common.mail.service; | |||||
| import java.util.Properties; | |||||
| import org.springframework.mail.javamail.JavaMailSender; | |||||
| import org.springframework.mail.javamail.JavaMailSenderImpl; | |||||
| import org.springframework.stereotype.Service; | |||||
| import com.ffii.core.common.MailSMTP; | |||||
| import com.ffii.core.settings.service.SettingsService; | |||||
| /** caching mail sender if config no changed */ | |||||
| @Service | |||||
| public class MailSenderService { | |||||
| private SettingsService settingsService; | |||||
| private JavaMailSender sender; | |||||
| private MailSMTP mailConfigCachs; | |||||
| public MailSenderService(SettingsService settingsService) { | |||||
| this.settingsService = settingsService; | |||||
| } | |||||
| public JavaMailSender get() { | |||||
| MailSMTP config = new MailSMTP(settingsService); | |||||
| if (this.sender == null || mailConfigCachs == null || !config.equals(this.mailConfigCachs)) { | |||||
| this.mailConfigCachs = config; | |||||
| JavaMailSenderImpl sender = new JavaMailSenderImpl(); | |||||
| Properties props = new Properties(); | |||||
| props.put("mail.smtp.timeout", "20000"); | |||||
| props.put("mail.smtp.connectiontimeout", "10000"); | |||||
| props.put("mail.smtp.auth", "true"); | |||||
| props.put("mail.smtp.starttls.enable", "true"); | |||||
| sender.setHost(config.getHost()); | |||||
| sender.setPort(config.getPort()); | |||||
| sender.setUsername(config.getUsername()); | |||||
| sender.setPassword(config.getPassword()); | |||||
| sender.setJavaMailProperties(props); | |||||
| this.sender = sender; | |||||
| } | |||||
| return this.sender; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,143 @@ | |||||
| package com.ffii.core.common.mail.service; | |||||
| import java.io.IOException; | |||||
| import java.util.Arrays; | |||||
| import java.util.List; | |||||
| import java.util.Locale; | |||||
| import java.util.Map; | |||||
| import javax.mail.MessagingException; | |||||
| import javax.mail.internet.InternetAddress; | |||||
| import javax.mail.internet.MimeMessage; | |||||
| import org.apache.commons.logging.Log; | |||||
| import org.apache.commons.logging.LogFactory; | |||||
| import org.springframework.core.io.ByteArrayResource; | |||||
| 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.springframework.ui.freemarker.FreeMarkerTemplateUtils; | |||||
| import com.ffii.core.common.ErrorCodes; | |||||
| import com.ffii.core.common.SettingNames; | |||||
| import com.ffii.core.common.mail.pojo.MailRequest; | |||||
| import com.ffii.core.exception.InternalServerErrorException; | |||||
| import com.ffii.core.settings.service.SettingsService; | |||||
| import com.ffii.core.utils.LocaleUtils; | |||||
| import freemarker.core.ParseException; | |||||
| import freemarker.template.Configuration; | |||||
| import freemarker.template.MalformedTemplateNameException; | |||||
| import freemarker.template.Template; | |||||
| import freemarker.template.TemplateException; | |||||
| import freemarker.template.TemplateNotFoundException; | |||||
| @Service | |||||
| public class MailService { | |||||
| protected final Log logger = LogFactory.getLog(getClass()); | |||||
| private MailSenderService mailSenderService; | |||||
| private Configuration freemarkerConfig; | |||||
| private SettingsService settingsService; | |||||
| public MailService(MailSenderService mailSenderService, Configuration freemarkerConfig, | |||||
| SettingsService settingsService) { | |||||
| this.mailSenderService = mailSenderService; | |||||
| this.freemarkerConfig = freemarkerConfig; | |||||
| this.settingsService = settingsService; | |||||
| } | |||||
| private void doSend(List<MailRequest> mailRequests, Locale locale) | |||||
| throws MessagingException, TemplateNotFoundException, MalformedTemplateNameException, ParseException, IOException, | |||||
| TemplateException { | |||||
| JavaMailSender sender = mailSenderService.get(); | |||||
| for (MailRequest mailRequest : mailRequests) { | |||||
| MimeMessage mimeMessage = sender.createMimeMessage(); | |||||
| MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8"); | |||||
| helper.setSubject(mailRequest.getSubject()); | |||||
| Template template; | |||||
| try { | |||||
| template = freemarkerConfig | |||||
| .getTemplate(mailRequest.getTemplate() + "_" + LocaleUtils.toLocaleStr(locale) + ".ftl"); | |||||
| } catch (TemplateNotFoundException e) { | |||||
| template = freemarkerConfig.getTemplate(mailRequest.getTemplate() + ".ftl"); | |||||
| } | |||||
| helper.setText( | |||||
| FreeMarkerTemplateUtils.processTemplateIntoString(template, mailRequest.getArgs()), | |||||
| true); | |||||
| if (mailRequest.getFrom() != null) { | |||||
| helper.setFrom(mailRequest.getFrom()); | |||||
| } else { | |||||
| helper.setFrom(settingsService.getString(SettingNames.MAIL_SMTP_USERNAME)); | |||||
| } | |||||
| if (mailRequest.getPriority() != null) | |||||
| helper.setPriority(mailRequest.getPriority()); | |||||
| if (mailRequest.getReplyTo() != null) | |||||
| helper.setReplyTo(mailRequest.getReplyTo()); | |||||
| if (mailRequest.getTo() != null) | |||||
| helper.setTo(mailRequest.getTo().toArray(new InternetAddress[mailRequest.getTo().size()])); | |||||
| if (mailRequest.getCc() != null) | |||||
| helper.setCc(mailRequest.getCc().toArray(new InternetAddress[mailRequest.getCc().size()])); | |||||
| if (mailRequest.getBcc() != null) | |||||
| helper.setBcc(mailRequest.getBcc().toArray(new InternetAddress[mailRequest.getBcc().size()])); | |||||
| if (mailRequest.getAttachments() != null) { | |||||
| for (Map.Entry<String, byte[]> entry : mailRequest.getAttachments().entrySet()) { | |||||
| helper.addAttachment(entry.getKey(), new ByteArrayResource(entry.getValue())); | |||||
| } | |||||
| } | |||||
| sender.send(mimeMessage); | |||||
| } | |||||
| } | |||||
| public void send(List<MailRequest> mailRequests, Locale locale) { | |||||
| try { | |||||
| doSend(mailRequests, locale); | |||||
| } catch (MessagingException | IOException | TemplateException e) { | |||||
| throw new InternalServerErrorException(ErrorCodes.SEND_EMAIL_ERROR, e); | |||||
| } | |||||
| } | |||||
| @Async | |||||
| public void asyncSend(List<MailRequest> mailRequests, Locale locale) { | |||||
| try { | |||||
| doSend(mailRequests, locale); | |||||
| } catch (MessagingException | IOException | TemplateException e) { | |||||
| logger.error("send email error", e); | |||||
| } | |||||
| } | |||||
| public void send(List<MailRequest> mailRequests) { | |||||
| send(mailRequests, LocaleUtils.getLocale()); | |||||
| } | |||||
| @Async | |||||
| public void asyncSend(List<MailRequest> mailRequests) { | |||||
| asyncSend(mailRequests, LocaleUtils.getLocale()); | |||||
| } | |||||
| public void send(MailRequest mailRequest) { | |||||
| send(Arrays.asList(mailRequest)); | |||||
| } | |||||
| @Async | |||||
| public void asyncSend(MailRequest mailRequest) { | |||||
| asyncSend(Arrays.asList(mailRequest)); | |||||
| } | |||||
| public void send(MailRequest mailRequest, Locale locale) { | |||||
| send(Arrays.asList(mailRequest), locale); | |||||
| } | |||||
| @Async | |||||
| public void asyncSend(MailRequest mailRequest, Locale locale) { | |||||
| asyncSend(Arrays.asList(mailRequest), locale); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,15 @@ | |||||
| package com.ffii.core.common.mobile.dao; | |||||
| import java.util.Optional; | |||||
| import com.ffii.core.common.mobile.entity.AccessToken; | |||||
| import com.ffii.core.support.AbstractDao; | |||||
| /** @author Fung */ | |||||
| public interface AccessTokenDao extends AbstractDao<AccessToken, Integer> { | |||||
| public Optional<AccessToken> findByDeviceIdAndAccessToken(String deviceId, String accessToken); | |||||
| public int deleteByDeviceIdAndAccessToken(String deviceId,String accessToken); | |||||
| public int deleteByUsername(String username); | |||||
| } | |||||
| @@ -0,0 +1,81 @@ | |||||
| package com.ffii.core.common.mobile.entity; | |||||
| import java.time.LocalDateTime; | |||||
| import javax.persistence.Column; | |||||
| import javax.persistence.Entity; | |||||
| import javax.persistence.PrePersist; | |||||
| import javax.persistence.Table; | |||||
| import javax.validation.constraints.NotBlank; | |||||
| import javax.validation.constraints.NotNull; | |||||
| import com.ffii.core.entity.IdEntity; | |||||
| /** @author Fung */ | |||||
| @Entity | |||||
| @Table(name = "access_token") | |||||
| public class AccessToken extends IdEntity<Integer> { | |||||
| @NotBlank | |||||
| @Column | |||||
| private String username; | |||||
| @NotBlank | |||||
| @Column | |||||
| private String deviceId; | |||||
| @NotBlank | |||||
| @Column | |||||
| private String accessToken; | |||||
| @NotNull | |||||
| @Column(updatable = false) | |||||
| private LocalDateTime created; | |||||
| public AccessToken() { | |||||
| } | |||||
| public AccessToken(String username, String deviceId, String accessToken) { | |||||
| this.username = username; | |||||
| this.deviceId = deviceId; | |||||
| this.accessToken = accessToken; | |||||
| } | |||||
| @PrePersist | |||||
| public void autoSetCreated() { | |||||
| this.created = LocalDateTime.now(); | |||||
| } | |||||
| public String getUsername() { | |||||
| return this.username; | |||||
| } | |||||
| public void setUsername(String username) { | |||||
| this.username = username; | |||||
| } | |||||
| public String getDeviceId() { | |||||
| return this.deviceId; | |||||
| } | |||||
| public void setDeviceId(String deviceId) { | |||||
| this.deviceId = deviceId; | |||||
| } | |||||
| public String getAccessToken() { | |||||
| return this.accessToken; | |||||
| } | |||||
| public void setAccessToken(String accessToken) { | |||||
| this.accessToken = accessToken; | |||||
| } | |||||
| public LocalDateTime getCreated() { | |||||
| return this.created; | |||||
| } | |||||
| public void setCreated(LocalDateTime created) { | |||||
| this.created = created; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,33 @@ | |||||
| package com.ffii.core.common.mobile.service; | |||||
| import java.util.Optional; | |||||
| import org.springframework.stereotype.Service; | |||||
| import org.springframework.transaction.annotation.Transactional; | |||||
| import com.ffii.core.common.mobile.dao.AccessTokenDao; | |||||
| import com.ffii.core.common.mobile.entity.AccessToken; | |||||
| import com.ffii.core.support.AbstractIdEntityService; | |||||
| import com.ffii.core.support.JdbcDao; | |||||
| /** @author Fung */ | |||||
| @Service | |||||
| public class AccessTokenService extends AbstractIdEntityService<AccessToken, Integer, AccessTokenDao> { | |||||
| public AccessTokenService(JdbcDao jdbcDao, AccessTokenDao dao) { | |||||
| super(jdbcDao, dao); | |||||
| } | |||||
| public Optional<AccessToken> findByDeviceIdAndAccessToken(String deviceId, String accessToken) { | |||||
| return dao.findByDeviceIdAndAccessToken(deviceId, accessToken); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void logout(String deviceId, String accessToken) { | |||||
| dao.deleteByDeviceIdAndAccessToken(deviceId, accessToken); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void logoutAll(String username) { | |||||
| dao.deleteByUsername(username); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,59 @@ | |||||
| package com.ffii.core.common.mobile.web; | |||||
| import javax.servlet.http.HttpServletRequest; | |||||
| import org.springframework.beans.factory.annotation.Value; | |||||
| import org.springframework.context.support.MessageSourceAccessor; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.security.core.Authentication; | |||||
| import org.springframework.security.core.SpringSecurityMessageSource; | |||||
| import org.springframework.web.bind.annotation.PostMapping; | |||||
| import org.springframework.web.bind.annotation.RequestMapping; | |||||
| import org.springframework.web.bind.annotation.ResponseStatus; | |||||
| import org.springframework.web.bind.annotation.RestController; | |||||
| import com.ffii.core.common.mobile.service.AccessTokenService; | |||||
| import com.ffii.core.support.AbstractController; | |||||
| import io.swagger.v3.oas.annotations.Operation; | |||||
| /** @author Fung */ | |||||
| @RestController | |||||
| @RequestMapping("/protected/mobile") | |||||
| public class ProtectedMobileLogoutController extends AbstractController { | |||||
| protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); | |||||
| private final String AUTH_HEADER_DEVICE_ID; | |||||
| private final String AUTH_HEADER_ACCESS_TOKEN; | |||||
| private AccessTokenService accessTokenService; | |||||
| public ProtectedMobileLogoutController( | |||||
| @Value("${app.mobile.auth-header.deviceId:X-ATT-DeviceId}") String authHeaderDeviceId, | |||||
| @Value("${app.mobile.auth-header.accessToken:X-ATT-Access-Token}") String authHeaderAccessToken, | |||||
| AccessTokenService accessTokenService) { | |||||
| this.AUTH_HEADER_DEVICE_ID = authHeaderDeviceId; | |||||
| this.AUTH_HEADER_ACCESS_TOKEN = authHeaderAccessToken; | |||||
| this.accessTokenService = accessTokenService; | |||||
| } | |||||
| @Operation(summary = "logout current session") | |||||
| @PostMapping("/logout") | |||||
| @ResponseStatus(HttpStatus.NO_CONTENT) | |||||
| public void logout(HttpServletRequest request) { | |||||
| String deviceId = request.getHeader(this.AUTH_HEADER_DEVICE_ID); | |||||
| String accessToken = request.getHeader(this.AUTH_HEADER_ACCESS_TOKEN); | |||||
| if (deviceId != null && accessToken != null) { | |||||
| accessTokenService.logout(deviceId, accessToken); | |||||
| } | |||||
| } | |||||
| @Operation(summary = "logout all mobile session") | |||||
| @PostMapping("/logout-all") | |||||
| @ResponseStatus(HttpStatus.NO_CONTENT) | |||||
| public void logoutAll(Authentication Authentication) { | |||||
| accessTokenService.logoutAll(Authentication.getName()); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,281 @@ | |||||
| package com.ffii.core.common.mobile.web; | |||||
| import java.util.Collection; | |||||
| import java.util.UUID; | |||||
| import javax.validation.Valid; | |||||
| import javax.validation.constraints.NotBlank; | |||||
| import org.springframework.beans.factory.annotation.Value; | |||||
| import org.springframework.context.support.MessageSourceAccessor; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.security.authentication.AccountExpiredException; | |||||
| import org.springframework.security.authentication.BadCredentialsException; | |||||
| import org.springframework.security.authentication.CredentialsExpiredException; | |||||
| import org.springframework.security.authentication.DisabledException; | |||||
| import org.springframework.security.authentication.LockedException; | |||||
| import org.springframework.security.core.AuthenticationException; | |||||
| import org.springframework.security.core.GrantedAuthority; | |||||
| import org.springframework.security.core.SpringSecurityMessageSource; | |||||
| import org.springframework.security.core.userdetails.UserDetails; | |||||
| import org.springframework.security.crypto.password.PasswordEncoder; | |||||
| import org.springframework.web.bind.annotation.PostMapping; | |||||
| import org.springframework.web.bind.annotation.RequestBody; | |||||
| import org.springframework.web.bind.annotation.RequestMapping; | |||||
| import org.springframework.web.bind.annotation.RestController; | |||||
| import org.springframework.web.server.ResponseStatusException; | |||||
| import com.ffii.core.common.mobile.entity.AccessToken; | |||||
| import com.ffii.core.common.mobile.service.AccessTokenService; | |||||
| import com.ffii.core.department.entity.Department; | |||||
| import com.ffii.core.department.service.DepartmentService; | |||||
| import com.ffii.core.support.AbstractController; | |||||
| import com.ffii.core.user.entity.User; | |||||
| import com.ffii.core.user.service.UserAttemptService; | |||||
| import com.ffii.core.user.service.UserService; | |||||
| import io.swagger.v3.oas.annotations.Operation; | |||||
| import io.swagger.v3.oas.annotations.media.Content; | |||||
| import io.swagger.v3.oas.annotations.responses.ApiResponse; | |||||
| /** @author Fung */ | |||||
| @RestController | |||||
| @RequestMapping("/public/mobile") | |||||
| public class PublicMobileLoginController extends AbstractController { | |||||
| protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); | |||||
| private final int AUTO_LOCK_COUNT; | |||||
| private PasswordEncoder passwordEncoder; | |||||
| private AccessTokenService accessTokenService; | |||||
| private UserService userService; | |||||
| private UserAttemptService userAttemptService; | |||||
| private DepartmentService departmentService; | |||||
| public PublicMobileLoginController( | |||||
| @Value("${app.login.attempt-lock:5}") int autoLockCount, | |||||
| PasswordEncoder passwordEncoder, | |||||
| AccessTokenService accessTokenService, | |||||
| UserService userService, | |||||
| UserAttemptService userAttemptService, | |||||
| DepartmentService departmentService) { | |||||
| this.AUTO_LOCK_COUNT = autoLockCount; | |||||
| this.passwordEncoder = passwordEncoder; | |||||
| this.accessTokenService = accessTokenService; | |||||
| this.userService = userService; | |||||
| this.userAttemptService = userAttemptService; | |||||
| this.departmentService = departmentService; | |||||
| } | |||||
| @Operation(summary = "mobile login", responses = { | |||||
| @ApiResponse(responseCode = "200"), | |||||
| @ApiResponse(responseCode = "400", description = "bad request", content = @Content), | |||||
| @ApiResponse(responseCode = "401", description = "login failure", content = @Content), | |||||
| @ApiResponse(responseCode = "403", description = "account unavailable", content = @Content) | |||||
| }) | |||||
| @PostMapping("/login") | |||||
| public LoginResponse login(@RequestBody @Valid LoginRequest loginRequest) { | |||||
| try { | |||||
| String accessToken = this.authenticate(loginRequest.getDeviceId(), loginRequest.getUsername(), | |||||
| loginRequest.getPassword()); | |||||
| this.userAttemptService.loginSuccess(loginRequest.getUsername()); | |||||
| User user = userService.loadUserByUsername(loginRequest.getUsername()); | |||||
| Department department = departmentService.find(user.getDeptId()).orElseThrow(); | |||||
| return new LoginResponse(accessToken, user, department); | |||||
| } catch (AuthenticationException exception) { | |||||
| userAttemptService.loginFailure(loginRequest.getUsername()); | |||||
| if (userAttemptService.findAttempt(loginRequest.getUsername()) >= AUTO_LOCK_COUNT) { | |||||
| userService.findByUsername(loginRequest.getUsername()).ifPresent(user -> { | |||||
| if (!user.isLocked()) { | |||||
| user.setLocked(Boolean.TRUE); | |||||
| userService.save(user); | |||||
| } | |||||
| }); | |||||
| } | |||||
| if (exception instanceof LockedException || | |||||
| exception instanceof DisabledException || | |||||
| exception instanceof AccountExpiredException || | |||||
| exception instanceof CredentialsExpiredException) { | |||||
| throw new ResponseStatusException(HttpStatus.FORBIDDEN); | |||||
| } else { | |||||
| throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); | |||||
| } | |||||
| } | |||||
| } | |||||
| /** @return access token */ | |||||
| private String authenticate(String deviceId, String username, String password) throws AuthenticationException { | |||||
| this.check( | |||||
| userService.findByUsername(username) | |||||
| .orElseThrow(() -> new BadCredentialsException( | |||||
| messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"))), | |||||
| password); | |||||
| return accessTokenService.save(new AccessToken(username, deviceId, UUID.randomUUID().toString())) | |||||
| .getAccessToken(); | |||||
| } | |||||
| private void check(UserDetails user, String password) throws AuthenticationException { | |||||
| if (!user.isAccountNonLocked()) { | |||||
| logger.debug("User account is locked"); | |||||
| throw new LockedException(messages.getMessage( | |||||
| "AbstractUserDetailsAuthenticationProvider.locked", | |||||
| "User account is locked")); | |||||
| } | |||||
| if (!user.isEnabled()) { | |||||
| logger.debug("User account is disabled"); | |||||
| throw new DisabledException(messages.getMessage( | |||||
| "AbstractUserDetailsAuthenticationProvider.disabled", | |||||
| "User is disabled")); | |||||
| } | |||||
| if (!user.isAccountNonExpired()) { | |||||
| logger.debug("User account is expired"); | |||||
| throw new AccountExpiredException(messages.getMessage( | |||||
| "AbstractUserDetailsAuthenticationProvider.expired", | |||||
| "User account has expired")); | |||||
| } | |||||
| if (!passwordEncoder.matches(password, user.getPassword())) { | |||||
| logger.debug("Authentication failed: password does not match stored value"); | |||||
| throw new BadCredentialsException(messages.getMessage( | |||||
| "AbstractUserDetailsAuthenticationProvider.badCredentials", | |||||
| "Bad credentials")); | |||||
| } | |||||
| } | |||||
| public static class LoginRequest { | |||||
| @NotBlank | |||||
| private String deviceId; | |||||
| @NotBlank | |||||
| private String username; | |||||
| @NotBlank | |||||
| private String password; | |||||
| public String getDeviceId() { | |||||
| return this.deviceId; | |||||
| } | |||||
| public void setDeviceId(String deviceId) { | |||||
| this.deviceId = deviceId; | |||||
| } | |||||
| public String getUsername() { | |||||
| return this.username; | |||||
| } | |||||
| public void setUsername(String username) { | |||||
| this.username = username; | |||||
| } | |||||
| public String getPassword() { | |||||
| return this.password; | |||||
| } | |||||
| public void setPassword(String password) { | |||||
| this.password = password; | |||||
| } | |||||
| } | |||||
| public static class LoginResponse { | |||||
| String accessToken; | |||||
| LoginResponse.User user; | |||||
| public LoginResponse(String accessToken, com.ffii.core.user.entity.User user, Department department) { | |||||
| this.accessToken = accessToken; | |||||
| this.user = new LoginResponse.User(user.getUsername(), user.getName(), user.getId(), user.getDeptId(), | |||||
| department == null ? null : department.getCaseOrderBy(), user.getAuthorities()); | |||||
| } | |||||
| public String getAccessToken() { | |||||
| return accessToken; | |||||
| } | |||||
| public void setAccessToken(String accessToken) { | |||||
| this.accessToken = accessToken; | |||||
| } | |||||
| public LoginResponse.User getUser() { | |||||
| return user; | |||||
| } | |||||
| public void setUser(LoginResponse.User user) { | |||||
| this.user = user; | |||||
| } | |||||
| public static class User { | |||||
| String username; | |||||
| String fullname; | |||||
| int id; | |||||
| Integer departmentId; | |||||
| Integer caseOrderBy; | |||||
| Collection<? extends GrantedAuthority> authorities; | |||||
| public User(String username, String fullname, int id, Integer departmentId, Integer caseOrderBy, | |||||
| Collection<? extends GrantedAuthority> authorities) { | |||||
| this.username = username; | |||||
| this.fullname = fullname; | |||||
| this.id = id; | |||||
| this.departmentId = departmentId; | |||||
| this.caseOrderBy = caseOrderBy; | |||||
| this.authorities = authorities; | |||||
| } | |||||
| public String getUsername() { | |||||
| return username; | |||||
| } | |||||
| public void setUsername(String username) { | |||||
| this.username = username; | |||||
| } | |||||
| public int getId() { | |||||
| return id; | |||||
| } | |||||
| public void setId(int id) { | |||||
| this.id = id; | |||||
| } | |||||
| public Integer getDepartmentId() { | |||||
| return departmentId; | |||||
| } | |||||
| public void setDepartmentId(Integer departmentId) { | |||||
| this.departmentId = departmentId; | |||||
| } | |||||
| public Collection<? extends GrantedAuthority> getAuthorities() { | |||||
| return authorities; | |||||
| } | |||||
| public void setAuthorities(Collection<? extends GrantedAuthority> authorities) { | |||||
| this.authorities = authorities; | |||||
| } | |||||
| public Integer getCaseOrderBy() { | |||||
| return caseOrderBy; | |||||
| } | |||||
| public void setCaseOrderBy(Integer caseOrderBy) { | |||||
| this.caseOrderBy = caseOrderBy; | |||||
| } | |||||
| public String getFullname() { | |||||
| return fullname; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,21 @@ | |||||
| package com.ffii.core.common.res; | |||||
| public class LocaleRes { | |||||
| private String locale; | |||||
| public LocaleRes() { | |||||
| } | |||||
| public LocaleRes(String locale) { | |||||
| this.locale = locale; | |||||
| } | |||||
| public String getLocale() { | |||||
| return locale; | |||||
| } | |||||
| public void setLocale(String locale) { | |||||
| this.locale = locale; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,53 @@ | |||||
| package com.ffii.core.common.res; | |||||
| import java.util.List; | |||||
| public class MeRes { | |||||
| private Integer id; | |||||
| private String name; | |||||
| private String landingPage; | |||||
| private List<String> authorities; | |||||
| public Integer getId() { | |||||
| return id; | |||||
| } | |||||
| public void setId(Integer id) { | |||||
| this.id = id; | |||||
| } | |||||
| public String getName() { | |||||
| return name; | |||||
| } | |||||
| public void setName(String name) { | |||||
| this.name = name; | |||||
| } | |||||
| public List<String> getAuthorities() { | |||||
| return authorities; | |||||
| } | |||||
| public void setAuthorities(List<String> authorities) { | |||||
| this.authorities = authorities; | |||||
| } | |||||
| public String getLocale() { | |||||
| return locale; | |||||
| } | |||||
| public void setLocale(String locale) { | |||||
| this.locale = locale; | |||||
| } | |||||
| private String locale; | |||||
| public String getLandingPage() { | |||||
| return landingPage; | |||||
| } | |||||
| public void setLandingPage(String landingPage) { | |||||
| this.landingPage = landingPage; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,104 @@ | |||||
| package com.ffii.core.common.web; | |||||
| import java.util.Locale; | |||||
| import java.util.stream.Collectors; | |||||
| import javax.servlet.http.HttpServletRequest; | |||||
| import javax.servlet.http.HttpServletResponse; | |||||
| import javax.validation.Valid; | |||||
| import javax.validation.constraints.NotBlank; | |||||
| import org.apache.commons.lang3.StringUtils; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.security.core.GrantedAuthority; | |||||
| import org.springframework.web.bind.annotation.GetMapping; | |||||
| import org.springframework.web.bind.annotation.PostMapping; | |||||
| import org.springframework.web.bind.annotation.RequestBody; | |||||
| import org.springframework.web.bind.annotation.RequestMapping; | |||||
| import org.springframework.web.bind.annotation.ResponseStatus; | |||||
| import org.springframework.web.bind.annotation.RestController; | |||||
| import org.springframework.web.servlet.LocaleResolver; | |||||
| import com.ffii.core.common.SecurityUtils; | |||||
| import com.ffii.core.common.res.LocaleRes; | |||||
| import com.ffii.core.common.res.MeRes; | |||||
| import com.ffii.core.user.entity.User; | |||||
| import com.ffii.core.user.service.UserService; | |||||
| import com.ffii.core.utils.LocaleUtils; | |||||
| import io.swagger.v3.oas.annotations.Operation; | |||||
| @RestController | |||||
| @RequestMapping("/protected") | |||||
| public class CommonProtectedController { | |||||
| private LocaleResolver localeResolver; | |||||
| private UserService userService; | |||||
| public CommonProtectedController(LocaleResolver localeResolver, UserService userService) { | |||||
| this.localeResolver = localeResolver; | |||||
| this.userService = userService; | |||||
| } | |||||
| @Operation(summary = "list current details") | |||||
| @GetMapping("/me") | |||||
| public MeRes me() { | |||||
| User u = SecurityUtils.getUser().get(); | |||||
| MeRes res = new MeRes(); | |||||
| res.setId(u.getId()); | |||||
| res.setName(u.getName()); | |||||
| res.setLandingPage(u.getLandingPage()); | |||||
| res.setAuthorities(u.getAuthorities().stream() | |||||
| .map(GrantedAuthority::getAuthority) | |||||
| .collect(Collectors.toList())); | |||||
| res.setLocale(LocaleUtils.getLocaleStr()); | |||||
| return res; | |||||
| } | |||||
| @Operation(summary = "get current locale") | |||||
| @GetMapping("/locale") | |||||
| public LocaleRes currentLocale() { | |||||
| return new LocaleRes(LocaleUtils.getLocaleStr()); | |||||
| }; | |||||
| @Operation(summary = "set locale") | |||||
| @PostMapping("/locale") | |||||
| @ResponseStatus(HttpStatus.NO_CONTENT) | |||||
| public void locale(HttpServletRequest request, HttpServletResponse response, | |||||
| @RequestBody @Valid ChangeLocaleReq req) { | |||||
| Locale locale = StringUtils.isNotBlank(req.getCountry()) | |||||
| ? new Locale(req.getLanguage(), req.getCountry()) | |||||
| : new Locale(req.getLanguage()); | |||||
| localeResolver.setLocale(request, response, locale); | |||||
| SecurityUtils.getUser() | |||||
| .ifPresent(user -> { | |||||
| userService.setUserLocale(user.getId(), locale); | |||||
| }); | |||||
| } | |||||
| public static class ChangeLocaleReq { | |||||
| @NotBlank | |||||
| private String language; | |||||
| private String country; | |||||
| public String getLanguage() { | |||||
| return language; | |||||
| } | |||||
| public void setLanguage(String language) { | |||||
| this.language = language; | |||||
| } | |||||
| public String getCountry() { | |||||
| return country; | |||||
| } | |||||
| public void setCountry(String country) { | |||||
| this.country = country; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,40 @@ | |||||
| package com.ffii.core.common.web; | |||||
| import java.time.LocalDateTime; | |||||
| import org.springframework.web.bind.annotation.GetMapping; | |||||
| import org.springframework.web.bind.annotation.RequestMapping; | |||||
| import org.springframework.web.bind.annotation.RestController; | |||||
| import io.swagger.v3.oas.annotations.Operation; | |||||
| @RestController | |||||
| @RequestMapping("/public") | |||||
| public class CommonPublicController { | |||||
| @Operation(summary = "get system current time") | |||||
| @GetMapping("/now") | |||||
| public TimeRes now() { | |||||
| return new TimeRes(LocalDateTime.now()); | |||||
| } | |||||
| public static class TimeRes { | |||||
| private LocalDateTime time; | |||||
| public TimeRes() { | |||||
| } | |||||
| public TimeRes(LocalDateTime time) { | |||||
| this.time = time; | |||||
| } | |||||
| public LocalDateTime getTime() { | |||||
| return time; | |||||
| } | |||||
| public void setTime(LocalDateTime time) { | |||||
| this.time = time; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,15 @@ | |||||
| package com.ffii.core.common.web; | |||||
| import org.springframework.security.core.Authentication; | |||||
| import org.springframework.stereotype.Controller; | |||||
| import org.springframework.web.bind.annotation.GetMapping; | |||||
| /** @author Fung */ | |||||
| @Controller | |||||
| public class RootController { | |||||
| @GetMapping("/") | |||||
| public String root(Authentication authentication) { | |||||
| return authentication == null ? "forward:/static/public/app/login.html" | |||||
| : "logged-in"; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,39 @@ | |||||
| package com.ffii.core.config; | |||||
| import java.util.Locale; | |||||
| import javax.sql.DataSource; | |||||
| import org.springframework.context.annotation.Bean; | |||||
| import org.springframework.context.annotation.Configuration; | |||||
| import org.springframework.scheduling.annotation.EnableAsync; | |||||
| import org.springframework.scheduling.annotation.EnableScheduling; | |||||
| import org.springframework.web.servlet.LocaleResolver; | |||||
| import org.springframework.web.servlet.i18n.SessionLocaleResolver; | |||||
| import com.ffii.core.support.ErrorHandler; | |||||
| import com.ffii.core.support.JdbcDao; | |||||
| /** @author Fung */ | |||||
| @Configuration | |||||
| @EnableScheduling | |||||
| @EnableAsync | |||||
| public class AppConfig { | |||||
| @Bean | |||||
| public JdbcDao jdbcDao(DataSource dataSource) { | |||||
| return new JdbcDao(dataSource); | |||||
| } | |||||
| @Bean | |||||
| public LocaleResolver localeResolver() { | |||||
| var localeResolver = new SessionLocaleResolver(); | |||||
| localeResolver.setDefaultLocale(Locale.ENGLISH); | |||||
| return localeResolver; | |||||
| } | |||||
| @Bean | |||||
| public ErrorHandler errorHandler() { | |||||
| return new ErrorHandler(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,8 @@ | |||||
| package com.ffii.core.config; | |||||
| import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; | |||||
| import org.springframework.stereotype.Component; | |||||
| @Component | |||||
| public class AppPasswordEncoder extends BCryptPasswordEncoder{ | |||||
| } | |||||
| @@ -0,0 +1,115 @@ | |||||
| package com.ffii.core.config; | |||||
| import javax.validation.Valid; | |||||
| import org.springdoc.core.GroupedOpenApi; | |||||
| import org.springframework.beans.factory.annotation.Value; | |||||
| import org.springframework.context.annotation.Bean; | |||||
| import org.springframework.context.annotation.Configuration; | |||||
| import org.springframework.core.MethodParameter; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.security.access.prepost.PreAuthorize; | |||||
| import org.springframework.validation.annotation.Validated; | |||||
| import org.springframework.web.bind.annotation.RequestParam; | |||||
| import io.swagger.v3.oas.models.Components; | |||||
| import io.swagger.v3.oas.models.OpenAPI; | |||||
| import io.swagger.v3.oas.models.info.Info; | |||||
| import io.swagger.v3.oas.models.responses.ApiResponse; | |||||
| import io.swagger.v3.oas.models.responses.ApiResponses; | |||||
| import io.swagger.v3.oas.models.security.SecurityRequirement; | |||||
| import io.swagger.v3.oas.models.security.SecurityScheme; | |||||
| @Configuration | |||||
| public class OpenApiConfig { | |||||
| private final String AUTH_HEADER_DEVICE_ID; | |||||
| private final String AUTH_HEADER_ACCESS_TOKEN; | |||||
| public OpenApiConfig( | |||||
| @Value("${app.mobile.auth-header.deviceId:X-ATT-DeviceId}") String authHeaderDeviceId, | |||||
| @Value("${app.mobile.auth-header.accessToken:X-ATT-Access-Token}") String authHeaderAccessToken) { | |||||
| this.AUTH_HEADER_DEVICE_ID = authHeaderDeviceId; | |||||
| this.AUTH_HEADER_ACCESS_TOKEN = authHeaderAccessToken; | |||||
| } | |||||
| @Bean | |||||
| public GroupedOpenApi protectedApi() { | |||||
| return GroupedOpenApi.builder() | |||||
| .group("protected-api") | |||||
| .pathsToMatch("/protected/**") | |||||
| .addOperationCustomizer((operation, handlerMethod) -> { | |||||
| ApiResponses responses = operation.getResponses(); | |||||
| for (MethodParameter p : handlerMethod.getMethodParameters()) { | |||||
| RequestParam rp = p.getParameterAnnotation(RequestParam.class); | |||||
| if (rp != null && rp.required()) { | |||||
| responses.addApiResponse("400", new ApiResponse().description(HttpStatus.BAD_REQUEST.getReasonPhrase())); | |||||
| break; | |||||
| } else if (p.getParameterAnnotation(Valid.class) != null | |||||
| || p.getParameterAnnotation(Validated.class) != null) { | |||||
| responses.addApiResponse("400", new ApiResponse().description(HttpStatus.BAD_REQUEST.getReasonPhrase())); | |||||
| break; | |||||
| } | |||||
| } | |||||
| responses.addApiResponse("401", new ApiResponse().description(HttpStatus.UNAUTHORIZED.getReasonPhrase())); | |||||
| if (handlerMethod.getMethodAnnotation(PreAuthorize.class) != null) { | |||||
| responses.addApiResponse("403", new ApiResponse().description(HttpStatus.FORBIDDEN.getReasonPhrase())); | |||||
| } | |||||
| return operation; | |||||
| }) | |||||
| .addOpenApiCustomiser(api -> { | |||||
| api.addSecurityItem( | |||||
| new SecurityRequirement() | |||||
| .addList("deviceId", "mobile") | |||||
| .addList("accessToken", "mobile")); | |||||
| }) | |||||
| .build(); | |||||
| } | |||||
| @Bean | |||||
| public GroupedOpenApi publicApi() { | |||||
| return GroupedOpenApi.builder() | |||||
| .group("public-api") | |||||
| .pathsToMatch("/public/**") | |||||
| .addOperationCustomizer((operation, handlerMethod) -> { | |||||
| ApiResponses responses = operation.getResponses(); | |||||
| for (MethodParameter p : handlerMethod.getMethodParameters()) { | |||||
| RequestParam rp = p.getParameterAnnotation(RequestParam.class); | |||||
| if (rp != null && rp.required()) { | |||||
| responses.addApiResponse("400", new ApiResponse().description(HttpStatus.BAD_REQUEST.getReasonPhrase())); | |||||
| break; | |||||
| } else if (p.getParameterAnnotation(Valid.class) != null | |||||
| || p.getParameterAnnotation(Validated.class) != null) { | |||||
| responses.addApiResponse("400", new ApiResponse().description(HttpStatus.BAD_REQUEST.getReasonPhrase())); | |||||
| break; | |||||
| } | |||||
| } | |||||
| return operation; | |||||
| }) | |||||
| .build(); | |||||
| } | |||||
| @Bean | |||||
| public OpenAPI openAPI() { | |||||
| return new OpenAPI() | |||||
| .components(new Components() | |||||
| .addSecuritySchemes( | |||||
| "deviceId", | |||||
| new SecurityScheme() | |||||
| .type(SecurityScheme.Type.APIKEY) | |||||
| .in(SecurityScheme.In.HEADER) | |||||
| .name(this.AUTH_HEADER_DEVICE_ID)) | |||||
| .addSecuritySchemes( | |||||
| "accessToken", | |||||
| new SecurityScheme() | |||||
| .type(SecurityScheme.Type.APIKEY) | |||||
| .in(SecurityScheme.In.HEADER) | |||||
| .name(this.AUTH_HEADER_ACCESS_TOKEN))) | |||||
| .info(new Info().title("App API") | |||||
| .description("application api") | |||||
| .version("v0.0.1")); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,88 @@ | |||||
| package com.ffii.core.config; | |||||
| import org.springframework.beans.factory.annotation.Value; | |||||
| import org.springframework.context.annotation.Bean; | |||||
| import org.springframework.context.annotation.Configuration; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; | |||||
| import org.springframework.security.config.annotation.web.builders.HttpSecurity; | |||||
| import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | |||||
| import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; | |||||
| import org.springframework.security.web.SecurityFilterChain; | |||||
| import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; | |||||
| import org.springframework.web.servlet.LocaleResolver; | |||||
| import com.ffii.core.common.mobile.service.AccessTokenService; | |||||
| import com.ffii.core.config.auth.FailureHandler; | |||||
| import com.ffii.core.config.auth.SuccessHandler; | |||||
| import com.ffii.core.config.filter.TokenAuthFilter; | |||||
| import com.ffii.core.user.service.UserService; | |||||
| /** @author Fung */ | |||||
| @Configuration | |||||
| @EnableWebSecurity | |||||
| @EnableGlobalMethodSecurity(prePostEnabled = true) | |||||
| public class SecurityConfig { | |||||
| private SuccessHandler successHandler; | |||||
| private FailureHandler failureHandler; | |||||
| private final String AUTH_HEADER_DEVICE_ID; | |||||
| private final String AUTH_HEADER_ACCESS_TOKEN; | |||||
| private AccessTokenService accessTokenService; | |||||
| private UserService userService; | |||||
| private LocaleResolver localeResolver; | |||||
| public SecurityConfig( | |||||
| SuccessHandler successHandler, | |||||
| FailureHandler failureHandler, | |||||
| @Value("${app.mobile.auth-header.deviceId:X-ATT-DeviceId}") String authHeaderDeviceId, | |||||
| @Value("${app.mobile.auth-header.accessToken:X-ATT-Access-Token}") String authHeaderAccessToken, | |||||
| AccessTokenService accessTokenService, | |||||
| UserService userService, | |||||
| LocaleResolver localeResolver) { | |||||
| this.successHandler = successHandler; | |||||
| this.failureHandler = failureHandler; | |||||
| this.AUTH_HEADER_DEVICE_ID = authHeaderDeviceId; | |||||
| this.AUTH_HEADER_ACCESS_TOKEN = authHeaderAccessToken; | |||||
| this.accessTokenService = accessTokenService; | |||||
| this.userService = userService; | |||||
| this.localeResolver = localeResolver; | |||||
| } | |||||
| @Bean | |||||
| public WebSecurityCustomizer webSecurityCustomizer() { | |||||
| return (web) -> web.ignoring().antMatchers( | |||||
| "/static/public/**", | |||||
| "/swagger**/**", "/v3/**" // swagger | |||||
| ); | |||||
| } | |||||
| @Bean | |||||
| public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { | |||||
| return http | |||||
| .csrf().disable() | |||||
| .requestCache().disable() | |||||
| .httpBasic(httpBasic -> httpBasic.authenticationEntryPoint( | |||||
| (request, response, authException) -> response.sendError(HttpStatus.UNAUTHORIZED.value()))) | |||||
| .authorizeRequests(authorizeRequests -> authorizeRequests | |||||
| .antMatchers("/", "/public/**").permitAll() | |||||
| .anyRequest().authenticated()) | |||||
| .formLogin(formLogin -> formLogin | |||||
| .loginPage("/") | |||||
| .loginProcessingUrl("/login") | |||||
| .successHandler(successHandler) | |||||
| .failureHandler(failureHandler)) | |||||
| .sessionManagement(sessionManagement -> sessionManagement.maximumSessions(1)) | |||||
| .addFilterBefore(new TokenAuthFilter( | |||||
| this.AUTH_HEADER_DEVICE_ID, | |||||
| this.AUTH_HEADER_ACCESS_TOKEN, | |||||
| this.accessTokenService, | |||||
| this.userService, | |||||
| this.localeResolver), BasicAuthenticationFilter.class) | |||||
| .build(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,65 @@ | |||||
| package com.ffii.core.config.auth; | |||||
| import java.io.IOException; | |||||
| import javax.servlet.ServletException; | |||||
| import javax.servlet.http.HttpServletRequest; | |||||
| import javax.servlet.http.HttpServletResponse; | |||||
| import org.springframework.beans.factory.annotation.Value; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.security.authentication.AccountExpiredException; | |||||
| import org.springframework.security.authentication.CredentialsExpiredException; | |||||
| import org.springframework.security.authentication.DisabledException; | |||||
| import org.springframework.security.authentication.LockedException; | |||||
| import org.springframework.security.core.AuthenticationException; | |||||
| import org.springframework.security.web.authentication.AuthenticationFailureHandler; | |||||
| import org.springframework.stereotype.Component; | |||||
| import com.ffii.core.user.service.UserAttemptService; | |||||
| import com.ffii.core.user.service.UserService; | |||||
| /** @author Fung */ | |||||
| @Component | |||||
| public class FailureHandler implements AuthenticationFailureHandler { | |||||
| private final int AUTO_LOCK_COUNT; | |||||
| private UserAttemptService userAttemptService; | |||||
| private UserService userService; | |||||
| public FailureHandler( | |||||
| @Value("${app.login.attempt-lock:5}") int autoLockCount, | |||||
| UserAttemptService userAttemptService, | |||||
| UserService userService) { | |||||
| this.AUTO_LOCK_COUNT = autoLockCount; | |||||
| this.userAttemptService = userAttemptService; | |||||
| this.userService = userService; | |||||
| } | |||||
| @Override | |||||
| public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, | |||||
| AuthenticationException exception) throws IOException, ServletException { | |||||
| String username = request.getParameter("username"); | |||||
| userAttemptService.loginFailure(username); | |||||
| if (userAttemptService.findAttempt(username) >= AUTO_LOCK_COUNT) { | |||||
| userService.findByUsername(username).ifPresent(user -> { | |||||
| if (!user.isLocked()) { | |||||
| user.setLocked(Boolean.TRUE); | |||||
| userService.save(user); | |||||
| } | |||||
| }); | |||||
| } | |||||
| if (exception instanceof LockedException || | |||||
| exception instanceof DisabledException || | |||||
| exception instanceof AccountExpiredException || | |||||
| exception instanceof CredentialsExpiredException) { | |||||
| response.setStatus(HttpStatus.FORBIDDEN.value()); | |||||
| } else { | |||||
| response.setStatus(HttpStatus.UNAUTHORIZED.value()); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,40 @@ | |||||
| package com.ffii.core.config.auth; | |||||
| import java.io.IOException; | |||||
| import javax.servlet.ServletException; | |||||
| import javax.servlet.http.HttpServletRequest; | |||||
| import javax.servlet.http.HttpServletResponse; | |||||
| import org.apache.commons.lang3.StringUtils; | |||||
| import org.springframework.security.core.Authentication; | |||||
| import org.springframework.security.web.authentication.AuthenticationSuccessHandler; | |||||
| import org.springframework.stereotype.Component; | |||||
| import org.springframework.web.servlet.LocaleResolver; | |||||
| import com.ffii.core.user.entity.User; | |||||
| import com.ffii.core.user.service.UserAttemptService; | |||||
| import com.ffii.core.utils.LocaleUtils; | |||||
| /** @author Fung */ | |||||
| @Component | |||||
| public class SuccessHandler implements AuthenticationSuccessHandler { | |||||
| private LocaleResolver localeResolver; | |||||
| private UserAttemptService userAttemptService; | |||||
| public SuccessHandler(LocaleResolver localeResolver, UserAttemptService userAttemptService) { | |||||
| this.localeResolver = localeResolver; | |||||
| this.userAttemptService = userAttemptService; | |||||
| } | |||||
| @Override | |||||
| public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, | |||||
| Authentication authentication) throws IOException, ServletException { | |||||
| User user = (User) authentication.getPrincipal(); | |||||
| if (StringUtils.isNotBlank(user.getLocale())) | |||||
| localeResolver.setLocale(request, response, LocaleUtils.from(user.getLocale())); | |||||
| userAttemptService.loginSuccess(authentication.getName()); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,106 @@ | |||||
| package com.ffii.core.config.filter; | |||||
| import java.io.IOException; | |||||
| import java.util.function.Supplier; | |||||
| import javax.servlet.FilterChain; | |||||
| import javax.servlet.ServletException; | |||||
| import javax.servlet.ServletRequest; | |||||
| import javax.servlet.ServletResponse; | |||||
| import javax.servlet.http.HttpServletRequest; | |||||
| import javax.servlet.http.HttpServletResponse; | |||||
| import org.apache.commons.lang3.StringUtils; | |||||
| import org.springframework.beans.factory.annotation.Value; | |||||
| import org.springframework.context.support.MessageSourceAccessor; | |||||
| import org.springframework.security.authentication.BadCredentialsException; | |||||
| import org.springframework.security.core.Authentication; | |||||
| import org.springframework.security.core.AuthenticationException; | |||||
| import org.springframework.security.core.SpringSecurityMessageSource; | |||||
| import org.springframework.security.core.context.SecurityContextHolder; | |||||
| import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; | |||||
| import org.springframework.web.filter.GenericFilterBean; | |||||
| import org.springframework.web.servlet.LocaleResolver; | |||||
| import com.ffii.core.common.mobile.service.AccessTokenService; | |||||
| import com.ffii.core.user.entity.User; | |||||
| import com.ffii.core.user.service.UserService; | |||||
| import com.ffii.core.utils.LocaleUtils; | |||||
| /** @author Fung */ | |||||
| // @Component | |||||
| public class TokenAuthFilter extends GenericFilterBean { | |||||
| protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); | |||||
| private final String AUTH_HEADER_DEVICE_ID; | |||||
| private final String AUTH_HEADER_ACCESS_TOKEN; | |||||
| private AccessTokenService accessTokenService; | |||||
| private UserService userService; | |||||
| private LocaleResolver localeResolver; | |||||
| public TokenAuthFilter( | |||||
| @Value("${app.mobile.auth-header.deviceId:X-ATT-DeviceId}") String authHeaderDeviceId, | |||||
| @Value("${app.mobile.auth-header.accessToken:X-ATT-Access-Token}") String authHeaderAccessToken, | |||||
| AccessTokenService accessTokenService, | |||||
| UserService userService, | |||||
| LocaleResolver localeResolver) { | |||||
| this.AUTH_HEADER_DEVICE_ID = authHeaderDeviceId; | |||||
| this.AUTH_HEADER_ACCESS_TOKEN = authHeaderAccessToken; | |||||
| this.accessTokenService = accessTokenService; | |||||
| this.userService = userService; | |||||
| this.localeResolver = localeResolver; | |||||
| } | |||||
| @Override | |||||
| public void doFilter( | |||||
| ServletRequest request, | |||||
| ServletResponse response, | |||||
| FilterChain chain) throws IOException, ServletException { | |||||
| HttpServletRequest httpRequest = (HttpServletRequest) request; | |||||
| HttpServletResponse httpResponse = (HttpServletResponse) response; | |||||
| String deviceId = httpRequest.getHeader(this.AUTH_HEADER_DEVICE_ID); | |||||
| String accessToken = httpRequest.getHeader(this.AUTH_HEADER_ACCESS_TOKEN); | |||||
| if (deviceId != null && accessToken != null) { | |||||
| try { | |||||
| Supplier<BadCredentialsException> badCredentials = () -> new BadCredentialsException(messages.getMessage( | |||||
| "AbstractUserDetailsAuthenticationProvider.badCredentials", | |||||
| "Bad credentials")); | |||||
| User user = userService | |||||
| .loadUserByUsername(accessTokenService.findByDeviceIdAndAccessToken(deviceId, accessToken) | |||||
| .orElseThrow(badCredentials) | |||||
| .getUsername()); | |||||
| if (user.isAccountNonLocked() && user.isAccountNonExpired() && user.isCredentialsNonExpired() && user.isEnabled()) { | |||||
| Authentication authResult = new PreAuthenticatedAuthenticationToken(user, null, | |||||
| user.getAuthorities()); | |||||
| SecurityContextHolder.getContext().setAuthentication(authResult); | |||||
| // success | |||||
| if (StringUtils.isNotBlank(user.getLocale())) | |||||
| localeResolver.setLocale(httpRequest, httpResponse, LocaleUtils.from(user.getLocale())); | |||||
| chain.doFilter(request, response); | |||||
| SecurityContextHolder.getContext().setAuthentication(null); | |||||
| SecurityContextHolder.clearContext(); | |||||
| } else { | |||||
| throw badCredentials.get(); | |||||
| } | |||||
| } catch (AuthenticationException failed) { | |||||
| SecurityContextHolder.getContext().setAuthentication(null); | |||||
| SecurityContextHolder.clearContext(); | |||||
| if (this.logger.isDebugEnabled()) { | |||||
| this.logger.debug("Authentication request for failed!", failed); | |||||
| } | |||||
| chain.doFilter(request, response); | |||||
| } | |||||
| } else { | |||||
| chain.doFilter(request, response); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,15 @@ | |||||
| package com.ffii.core.department.dao; | |||||
| import java.util.Optional; | |||||
| import com.ffii.core.department.entity.Department; | |||||
| import com.ffii.core.support.AbstractDao; | |||||
| public interface DepartmentDao extends AbstractDao<Department, Integer> { | |||||
| public Optional<Department> findByIdAndDeletedFalse(int id); | |||||
| // public Optional<Department> findByCodeAndDeletedFalse(String code); | |||||
| } | |||||
| @@ -0,0 +1,67 @@ | |||||
| package com.ffii.core.department.entity; | |||||
| import javax.persistence.Column; | |||||
| import javax.persistence.Entity; | |||||
| import javax.persistence.Table; | |||||
| import com.ffii.core.entity.BaseEntity; | |||||
| @Entity | |||||
| @Table(name = "department") | |||||
| public class Department extends BaseEntity<Integer> { | |||||
| // #App | |||||
| public static final int CASE_ORDER_BY_DATE = 0; | |||||
| public static final int CASE_ORDER_BY_PRIORITY = 1; | |||||
| public static final int CASE_ORDER_BY_SEVERITY = 2; | |||||
| // #App | |||||
| public static final int VIEW_TYPE_OWN_DEPT = 0; | |||||
| public static final int VIEW_TYPE_ALL_DEPT = 1; | |||||
| public static final int VIEW_TYPE_EQ_TYPE = 2; | |||||
| @Column | |||||
| private String name; | |||||
| @Column | |||||
| private String description; | |||||
| @Column | |||||
| private Integer viewType; | |||||
| @Column | |||||
| private Integer caseOrderBy; | |||||
| public String getName() { | |||||
| return name; | |||||
| } | |||||
| public void setName(String name) { | |||||
| this.name = name; | |||||
| } | |||||
| public String getDescription() { | |||||
| return description; | |||||
| } | |||||
| public void setDescription(String description) { | |||||
| this.description = description; | |||||
| } | |||||
| public Integer getViewType() { | |||||
| return viewType; | |||||
| } | |||||
| public void setViewType(Integer viewType) { | |||||
| this.viewType = viewType; | |||||
| } | |||||
| public Integer getCaseOrderBy() { | |||||
| return caseOrderBy; | |||||
| } | |||||
| public void setCaseOrderBy(Integer caseOrderBy) { | |||||
| this.caseOrderBy = caseOrderBy; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,42 @@ | |||||
| package com.ffii.core.department.req; | |||||
| public class SaveDepartmentReq { | |||||
| private String name; | |||||
| private String description; | |||||
| private Integer viewType; | |||||
| private Integer caseOrderBy; | |||||
| public String getName() { | |||||
| return name; | |||||
| } | |||||
| public void setName(String name) { | |||||
| this.name = name; | |||||
| } | |||||
| public String getDescription() { | |||||
| return description; | |||||
| } | |||||
| public void setDescription(String description) { | |||||
| this.description = description; | |||||
| } | |||||
| public Integer getViewType() { | |||||
| return viewType; | |||||
| } | |||||
| public void setViewType(Integer viewType) { | |||||
| this.viewType = viewType; | |||||
| } | |||||
| public Integer getCaseOrderBy() { | |||||
| return caseOrderBy; | |||||
| } | |||||
| public void setCaseOrderBy(Integer caseOrderBy) { | |||||
| this.caseOrderBy = caseOrderBy; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,99 @@ | |||||
| package com.ffii.core.department.service; | |||||
| import java.util.List; | |||||
| import java.util.Map; | |||||
| import org.springframework.stereotype.Service; | |||||
| import com.ffii.core.department.dao.DepartmentDao; | |||||
| import com.ffii.core.department.entity.Department; | |||||
| import com.ffii.core.support.AbstractBaseEntityService; | |||||
| import com.ffii.core.support.JdbcDao; | |||||
| import com.ffii.core.utils.Params; | |||||
| @Service | |||||
| public class DepartmentService extends AbstractBaseEntityService<Department, Integer, DepartmentDao> { | |||||
| public DepartmentService(JdbcDao jdbcDao, DepartmentDao dao) { | |||||
| super(jdbcDao, dao); | |||||
| } | |||||
| public List<Map<String, Object>> searchForCombo(Map<String, Object> args) { | |||||
| StringBuilder sql = new StringBuilder("SELECT" | |||||
| + " d.id," | |||||
| + " d.name" | |||||
| + " FROM department d" | |||||
| + " WHERE d.deleted = 0"); | |||||
| if (args != null) { | |||||
| if (args.containsKey(Params.QUERY)) | |||||
| sql.append(" AND d.name LIKE :query"); | |||||
| if (args.containsKey(Params.ID)) | |||||
| sql.append(" AND d.id = :id"); | |||||
| if (args.containsKey(Params.NAME)) | |||||
| sql.append(" AND d.name LIKE :name"); | |||||
| } | |||||
| sql.append(" ORDER BY d.name"); | |||||
| return jdbcDao.queryForList(sql.toString(), args); | |||||
| } | |||||
| public List<Map<String, Object>> search(Map<String, Object> args) { | |||||
| StringBuilder sql = new StringBuilder("SELECT" | |||||
| + " d.*" | |||||
| + " FROM department d" | |||||
| + " WHERE d.deleted = 0"); | |||||
| if (args != null) { | |||||
| if (args.containsKey(Params.QUERY)) | |||||
| sql.append(" AND d.name LIKE :query"); | |||||
| if (args.containsKey(Params.ID)) | |||||
| sql.append(" AND d.id = :id"); | |||||
| if (args.containsKey(Params.NAME)) | |||||
| sql.append(" AND d.name LIKE :name"); | |||||
| if (args.containsKey("description")) | |||||
| sql.append(" AND d.description LIKE :description"); | |||||
| } | |||||
| sql.append(" ORDER BY d.name"); | |||||
| if (args != null) { | |||||
| if (args.containsKey(Params.START) && args.containsKey(Params.LIMIT)) | |||||
| sql.append(" LIMIT :start, :limit"); | |||||
| } | |||||
| return jdbcDao.queryForList(sql.toString(), args); | |||||
| } | |||||
| public int searchTotalCount(Map<String, Object> args) { | |||||
| StringBuilder sql = new StringBuilder("SELECT" | |||||
| + " COUNT(*) AS count" | |||||
| + " FROM department d" | |||||
| + " WHERE d.deleted = FALSE"); | |||||
| if (args != null) { | |||||
| if (args.containsKey(Params.NAME)) | |||||
| sql.append(" AND ( d.name LIKE :name OR d.description LIKE :name)"); | |||||
| } | |||||
| return jdbcDao.queryForInt(sql.toString(), args); | |||||
| } | |||||
| public String getDeptNameById(Integer deptId) { | |||||
| return jdbcDao.queryForString("SELECT" | |||||
| + " d.name" | |||||
| + " FROM department d" | |||||
| + " WHERE d.id = " | |||||
| + deptId); | |||||
| } | |||||
| public boolean existsEqType(int id) { | |||||
| return jdbcDao.queryForBoolean(" select exists" | |||||
| + " (select 1 from equipment_type et where deleted = 0 and caseOwnerId = :id)", Map.of("id", id)); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,100 @@ | |||||
| package com.ffii.core.department.web; | |||||
| import java.util.Map; | |||||
| import javax.servlet.http.HttpServletRequest; | |||||
| import javax.validation.Valid; | |||||
| import org.springframework.beans.BeanUtils; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.security.access.prepost.PreAuthorize; | |||||
| import org.springframework.web.bind.ServletRequestBindingException; | |||||
| import org.springframework.web.bind.annotation.DeleteMapping; | |||||
| import org.springframework.web.bind.annotation.GetMapping; | |||||
| import org.springframework.web.bind.annotation.PathVariable; | |||||
| import org.springframework.web.bind.annotation.PostMapping; | |||||
| import org.springframework.web.bind.annotation.PutMapping; | |||||
| import org.springframework.web.bind.annotation.RequestBody; | |||||
| import org.springframework.web.bind.annotation.RequestMapping; | |||||
| import org.springframework.web.bind.annotation.ResponseStatus; | |||||
| import org.springframework.web.bind.annotation.RestController; | |||||
| import com.ffii.core.common.ErrorCodes; | |||||
| import com.ffii.core.department.entity.Department; | |||||
| import com.ffii.core.department.req.SaveDepartmentReq; | |||||
| import com.ffii.core.department.service.DepartmentService; | |||||
| import com.ffii.core.exception.NotFoundException; | |||||
| import com.ffii.core.exception.UnprocessableEntityException; | |||||
| import com.ffii.core.response.IdRes; | |||||
| import com.ffii.core.response.RecordsRes; | |||||
| import com.ffii.core.support.AbstractController; | |||||
| import com.ffii.core.utils.CriteriaArgsBuilder; | |||||
| import com.ffii.core.utils.Params; | |||||
| @RestController | |||||
| @RequestMapping("/protected/department") | |||||
| public class DepartmentController extends AbstractController { | |||||
| private DepartmentService departmentService; | |||||
| public DepartmentController(DepartmentService departmentService) { | |||||
| this.departmentService = departmentService; | |||||
| } | |||||
| @PostMapping | |||||
| @ResponseStatus(HttpStatus.CREATED) | |||||
| @PreAuthorize("hasAuthority('EDIT_DEPARTMENT')") | |||||
| public IdRes add(@RequestBody @Valid SaveDepartmentReq req) { | |||||
| Department instance = new Department(); | |||||
| BeanUtils.copyProperties(req, instance); | |||||
| instance = departmentService.save(instance); | |||||
| return new IdRes(instance.getId()); | |||||
| } | |||||
| @PutMapping("/{id}") | |||||
| @ResponseStatus(HttpStatus.NO_CONTENT) | |||||
| @PreAuthorize("hasAuthority('EDIT_DEPARTMENT')") | |||||
| public void update(@RequestBody @Valid SaveDepartmentReq req, @PathVariable int id) { | |||||
| Department instance = departmentService.find(id).orElseThrow(NotFoundException::new); | |||||
| BeanUtils.copyProperties(req, instance); | |||||
| instance = departmentService.save(instance); | |||||
| } | |||||
| @DeleteMapping("/{id}") | |||||
| @ResponseStatus(HttpStatus.NO_CONTENT) | |||||
| @PreAuthorize("hasAuthority('EDIT_DEPARTMENT')") | |||||
| public void delete(@PathVariable int id) { | |||||
| if (!departmentService.existsEqType(id)) { | |||||
| departmentService.markDelete(departmentService.find(id).orElseThrow(NotFoundException::new)); | |||||
| } else { | |||||
| throw new UnprocessableEntityException(ErrorCodes.DELETE_DEPARTMENT_ERROR); | |||||
| } | |||||
| } | |||||
| @GetMapping | |||||
| @PreAuthorize("hasAuthority('VIEW_DEPARTMENT')") | |||||
| public RecordsRes<Map<String, Object>> listJson(HttpServletRequest request) throws ServletRequestBindingException { | |||||
| Map<String, Object> args = CriteriaArgsBuilder.withRequest(request) | |||||
| .addStringLike(Params.QUERY) | |||||
| .addInteger(Params.ID) | |||||
| .addStringLike(Params.NAME) | |||||
| .addStringLike("description") | |||||
| .addInteger(Params.START) | |||||
| .addInteger(Params.LIMIT) | |||||
| .build(); | |||||
| return new RecordsRes<>(departmentService.search(args), departmentService.searchTotalCount(args)); | |||||
| } | |||||
| // #App comboJson | |||||
| @GetMapping("/combo") | |||||
| // @PreAuthorize("hasAuthority('VIEW_DEPARTMENT')") | |||||
| public RecordsRes<Map<String, Object>> comboJson(HttpServletRequest request) throws ServletRequestBindingException { | |||||
| return new RecordsRes<>(departmentService.searchForCombo( | |||||
| CriteriaArgsBuilder.withRequest(request) | |||||
| .addStringLike(Params.QUERY) | |||||
| .addInteger(Params.ID) | |||||
| .addStringLike(Params.NAME) | |||||
| .build())); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,124 @@ | |||||
| package com.ffii.core.entity; | |||||
| import java.io.Serializable; | |||||
| import java.time.LocalDateTime; | |||||
| import java.util.Optional; | |||||
| import javax.persistence.Column; | |||||
| import javax.persistence.MappedSuperclass; | |||||
| import javax.persistence.PrePersist; | |||||
| import javax.persistence.PreUpdate; | |||||
| import javax.persistence.Version; | |||||
| import javax.validation.constraints.NotNull; | |||||
| import org.springframework.security.core.context.SecurityContextHolder; | |||||
| /** @author Fung */ | |||||
| @MappedSuperclass | |||||
| public abstract class BaseEntity<ID extends Serializable> extends IdEntity<ID> { | |||||
| /** start from 0 */ | |||||
| @NotNull | |||||
| @Version | |||||
| @Column | |||||
| private Integer version; | |||||
| @NotNull | |||||
| @Column(updatable = false) | |||||
| private LocalDateTime created; | |||||
| @Column(updatable = false) | |||||
| private String createdBy; | |||||
| @NotNull | |||||
| @Column | |||||
| private LocalDateTime modified; | |||||
| @Column | |||||
| private String modifiedBy; | |||||
| @NotNull | |||||
| @Column | |||||
| private Boolean deleted; | |||||
| @PrePersist | |||||
| public void autoSetCreated() { | |||||
| this.setCreated(LocalDateTime.now()); | |||||
| this.setModified(LocalDateTime.now()); | |||||
| this.setDeleted(Boolean.FALSE); | |||||
| Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) | |||||
| .ifPresentOrElse( | |||||
| authentication -> { | |||||
| this.setCreatedBy(authentication.getName()); | |||||
| this.setModifiedBy(authentication.getName()); | |||||
| }, | |||||
| () -> { | |||||
| this.setCreatedBy(null); | |||||
| this.setModifiedBy(null); | |||||
| }); | |||||
| } | |||||
| @PreUpdate | |||||
| public void autoSetModified() { | |||||
| this.setModified(LocalDateTime.now()); | |||||
| Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()).ifPresentOrElse( | |||||
| authentication -> this.setModifiedBy(authentication.getName()), | |||||
| () -> this.setModifiedBy(null)); | |||||
| } | |||||
| // getter and setter | |||||
| public Integer getVersion() { | |||||
| return this.version; | |||||
| } | |||||
| public void setVersion(Integer version) { | |||||
| this.version = version; | |||||
| } | |||||
| public LocalDateTime getCreated() { | |||||
| return this.created; | |||||
| } | |||||
| public void setCreated(LocalDateTime created) { | |||||
| this.created = created; | |||||
| } | |||||
| public String getCreatedBy() { | |||||
| return this.createdBy; | |||||
| } | |||||
| public void setCreatedBy(String createdBy) { | |||||
| this.createdBy = createdBy; | |||||
| } | |||||
| public LocalDateTime getModified() { | |||||
| return this.modified; | |||||
| } | |||||
| public void setModified(LocalDateTime modified) { | |||||
| this.modified = modified; | |||||
| } | |||||
| public String getModifiedBy() { | |||||
| return this.modifiedBy; | |||||
| } | |||||
| public void setModifiedBy(String modifiedBy) { | |||||
| this.modifiedBy = modifiedBy; | |||||
| } | |||||
| public Boolean isDeleted() { | |||||
| return this.deleted; | |||||
| } | |||||
| public Boolean getDeleted() { | |||||
| return this.deleted; | |||||
| } | |||||
| public void setDeleted(Boolean deleted) { | |||||
| this.deleted = deleted; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,49 @@ | |||||
| package com.ffii.core.entity; | |||||
| import java.io.Serializable; | |||||
| import javax.persistence.GeneratedValue; | |||||
| import javax.persistence.GenerationType; | |||||
| import javax.persistence.Id; | |||||
| import javax.persistence.MappedSuperclass; | |||||
| import javax.persistence.PostLoad; | |||||
| import javax.persistence.PrePersist; | |||||
| import javax.persistence.Transient; | |||||
| import org.springframework.data.domain.Persistable; | |||||
| import com.fasterxml.jackson.annotation.JsonIgnore; | |||||
| /** @author Fung */ | |||||
| @MappedSuperclass | |||||
| public abstract class IdEntity<ID extends Serializable> implements Persistable<ID> { | |||||
| @Id | |||||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | |||||
| private ID id; | |||||
| @Transient | |||||
| private boolean isNew = true; | |||||
| @JsonIgnore | |||||
| @Override | |||||
| public boolean isNew() { | |||||
| return isNew; | |||||
| } | |||||
| @PrePersist | |||||
| @PostLoad | |||||
| void markNotNew() { | |||||
| this.isNew = false; | |||||
| } | |||||
| // getter and setter | |||||
| public ID getId() { | |||||
| return id; | |||||
| } | |||||
| public void setId(ID id) { | |||||
| this.id = id; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,15 @@ | |||||
| package com.ffii.core.exception; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.web.server.ResponseStatusException; | |||||
| public class BadRequestException extends ResponseStatusException { | |||||
| public BadRequestException() { | |||||
| super(HttpStatus.BAD_REQUEST); | |||||
| } | |||||
| public BadRequestException(String reason) { | |||||
| super(HttpStatus.BAD_REQUEST, reason); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,16 @@ | |||||
| package com.ffii.core.exception; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.web.server.ResponseStatusException; | |||||
| /* e.g. sub record not under record */ | |||||
| public class ConflictException extends ResponseStatusException { | |||||
| public ConflictException() { | |||||
| super(HttpStatus.CONFLICT); | |||||
| } | |||||
| public ConflictException(String reason) { | |||||
| super(HttpStatus.CONFLICT, reason); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,19 @@ | |||||
| package com.ffii.core.exception; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.web.server.ResponseStatusException; | |||||
| public class InternalServerErrorException extends ResponseStatusException { | |||||
| public InternalServerErrorException() { | |||||
| super(HttpStatus.INTERNAL_SERVER_ERROR); | |||||
| } | |||||
| public InternalServerErrorException(String reason) { | |||||
| super(HttpStatus.INTERNAL_SERVER_ERROR, reason); | |||||
| } | |||||
| public InternalServerErrorException(String reason, Throwable e) { | |||||
| super(HttpStatus.INTERNAL_SERVER_ERROR, reason, e); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,13 @@ | |||||
| package com.ffii.core.exception; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.web.server.ResponseStatusException; | |||||
| /* main record not found (e.g. item record) */ | |||||
| public class NotFoundException extends ResponseStatusException{ | |||||
| public NotFoundException() { | |||||
| super(HttpStatus.NOT_FOUND); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,17 @@ | |||||
| package com.ffii.core.exception; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.web.server.ResponseStatusException; | |||||
| /* sub record not found (e.g. item_line record) */ | |||||
| public class UnprocessableEntityException extends ResponseStatusException { | |||||
| public UnprocessableEntityException() { | |||||
| super(HttpStatus.UNPROCESSABLE_ENTITY); | |||||
| } | |||||
| public UnprocessableEntityException(String reason) { | |||||
| super(HttpStatus.UNPROCESSABLE_ENTITY, reason); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,21 @@ | |||||
| package com.ffii.core.response; | |||||
| public class DataRes<T> { | |||||
| private T data; | |||||
| public DataRes() { | |||||
| } | |||||
| public DataRes(T data) { | |||||
| this.data = data; | |||||
| } | |||||
| public T getData() { | |||||
| return data; | |||||
| } | |||||
| public void setData(T data) { | |||||
| this.data = data; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,36 @@ | |||||
| package com.ffii.core.response; | |||||
| import java.time.LocalDateTime; | |||||
| public class ErrorRes { | |||||
| private LocalDateTime timestamp; | |||||
| private String traceId; | |||||
| public ErrorRes() { | |||||
| this.timestamp = LocalDateTime.now(); | |||||
| } | |||||
| public ErrorRes(String traceId) { | |||||
| this.timestamp = LocalDateTime.now(); | |||||
| this.traceId = traceId; | |||||
| } | |||||
| public LocalDateTime getTimestamp() { | |||||
| return timestamp; | |||||
| } | |||||
| public void setTimestamp(LocalDateTime timestamp) { | |||||
| this.timestamp = timestamp; | |||||
| } | |||||
| public String getTraceId() { | |||||
| return traceId; | |||||
| } | |||||
| public void setTraceId(String traceId) { | |||||
| this.traceId = traceId; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,39 @@ | |||||
| package com.ffii.core.response; | |||||
| import java.time.LocalDateTime; | |||||
| import com.fasterxml.jackson.annotation.JsonInclude; | |||||
| public class FailureRes { | |||||
| private LocalDateTime timestamp; | |||||
| @JsonInclude(JsonInclude.Include.NON_NULL) | |||||
| private String error; | |||||
| public FailureRes() { | |||||
| this.timestamp = LocalDateTime.now(); | |||||
| } | |||||
| public FailureRes(String error) { | |||||
| this.timestamp = LocalDateTime.now(); | |||||
| this.error = error; | |||||
| } | |||||
| public LocalDateTime getTimestamp() { | |||||
| return timestamp; | |||||
| } | |||||
| public void setTimestamp(LocalDateTime timestamp) { | |||||
| this.timestamp = timestamp; | |||||
| } | |||||
| public String getError() { | |||||
| return error; | |||||
| } | |||||
| public void setError(String error) { | |||||
| this.error = error; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,21 @@ | |||||
| package com.ffii.core.response; | |||||
| public class IdRes { | |||||
| private Integer id; | |||||
| public IdRes() { | |||||
| } | |||||
| public IdRes(int id) { | |||||
| this.id = id; | |||||
| } | |||||
| public Integer getId() { | |||||
| return id; | |||||
| } | |||||
| public void setId(Integer id) { | |||||
| this.id = id; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,41 @@ | |||||
| package com.ffii.core.response; | |||||
| import java.util.List; | |||||
| import com.fasterxml.jackson.annotation.JsonInclude; | |||||
| public class RecordsRes<T> { | |||||
| private List<T> records; | |||||
| @JsonInclude(JsonInclude.Include.NON_NULL) | |||||
| private Integer total; | |||||
| public RecordsRes() { | |||||
| } | |||||
| public RecordsRes(List<T> records) { | |||||
| this.records = records; | |||||
| } | |||||
| public RecordsRes(List<T> records, int total) { | |||||
| this.records = records; | |||||
| this.total = total; | |||||
| } | |||||
| public List<T> getRecords() { | |||||
| return records; | |||||
| } | |||||
| public void setRecords(List<T> records) { | |||||
| this.records = records; | |||||
| } | |||||
| public Integer getTotal() { | |||||
| return total; | |||||
| } | |||||
| public void setTotal(Integer total) { | |||||
| this.total = total; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,10 @@ | |||||
| package com.ffii.core.settings.dao; | |||||
| import java.util.Optional; | |||||
| import com.ffii.core.settings.entity.Settings; | |||||
| import com.ffii.core.support.AbstractDao; | |||||
| public interface SettingsDao extends AbstractDao<Settings, Integer>{ | |||||
| public Optional<Settings> findByName(String name); | |||||
| } | |||||
| @@ -0,0 +1,74 @@ | |||||
| package com.ffii.core.settings.entity; | |||||
| import javax.persistence.Column; | |||||
| import javax.persistence.Entity; | |||||
| import javax.persistence.Table; | |||||
| import javax.validation.constraints.NotNull; | |||||
| import com.ffii.core.entity.IdEntity; | |||||
| @Entity | |||||
| @Table(name = "settings") | |||||
| public class Settings extends IdEntity<Integer> { | |||||
| public static String TYPE_STRING = "string"; | |||||
| public static String TYPE_INT = "integer"; | |||||
| public static String TYPE_FLOAT = "float"; | |||||
| public static String TYPE_BOOLEAN = "boolean"; | |||||
| public static String TYPE_DATE = "date"; | |||||
| public static String TYPE_TIME = "time"; | |||||
| public static String TYPE_DATETIME = "datetime"; | |||||
| // other "A/B" value must "A" or "B" | |||||
| // lowercase | |||||
| public static String VALUE_BOOLEAN_TRUE = "true"; | |||||
| public static String VALUE_BOOLEAN_FALSE = "false"; | |||||
| // TODO: pattern?? | |||||
| @NotNull | |||||
| @Column | |||||
| private String name; | |||||
| @NotNull | |||||
| @Column | |||||
| private String value; | |||||
| @Column | |||||
| private String category; | |||||
| @Column | |||||
| private String type; | |||||
| public String getName() { | |||||
| return name; | |||||
| } | |||||
| public void setName(String name) { | |||||
| this.name = name; | |||||
| } | |||||
| public String getValue() { | |||||
| return value; | |||||
| } | |||||
| public void setValue(String value) { | |||||
| this.value = value; | |||||
| } | |||||
| public String getCategory() { | |||||
| return category; | |||||
| } | |||||
| public void setCategory(String category) { | |||||
| this.category = category; | |||||
| } | |||||
| public String getType() { | |||||
| return type; | |||||
| } | |||||
| public void setType(String type) { | |||||
| this.type = type; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,210 @@ | |||||
| package com.ffii.core.settings.service; | |||||
| import java.time.LocalDate; | |||||
| import java.time.LocalDateTime; | |||||
| import java.time.LocalTime; | |||||
| import java.time.format.DateTimeFormatter; | |||||
| import java.time.format.DateTimeParseException; | |||||
| import java.util.Optional; | |||||
| import org.apache.commons.lang3.StringUtils; | |||||
| import org.springframework.stereotype.Service; | |||||
| import org.springframework.transaction.annotation.Transactional; | |||||
| import com.ffii.core.exception.InternalServerErrorException; | |||||
| import com.ffii.core.support.AbstractIdEntityService; | |||||
| import com.ffii.core.support.JdbcDao; | |||||
| import com.ffii.core.settings.dao.SettingsDao; | |||||
| import com.ffii.core.settings.entity.Settings; | |||||
| @Service | |||||
| public class SettingsService extends AbstractIdEntityService<Settings, Integer, SettingsDao> { | |||||
| public SettingsService(JdbcDao jdbcDao, SettingsDao dao) { | |||||
| super(jdbcDao, dao); | |||||
| } | |||||
| public Optional<Settings> findByName(String name) { | |||||
| return this.dao.findByName(name); | |||||
| } | |||||
| public boolean validateType(String type, String value) { | |||||
| if (StringUtils.isBlank(type)) | |||||
| return true; | |||||
| if (Settings.TYPE_STRING.equals(type)) | |||||
| return true; | |||||
| if (Settings.TYPE_BOOLEAN.equals(type)) { | |||||
| return Settings.VALUE_BOOLEAN_TRUE.equals(value) || Settings.VALUE_BOOLEAN_FALSE.equals(value); | |||||
| } | |||||
| if (Settings.TYPE_INT.equals(type)) { | |||||
| try { | |||||
| Integer.parseInt(value); | |||||
| return true; | |||||
| } catch (NumberFormatException e) { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| if (Settings.TYPE_FLOAT.equals(type)) { | |||||
| try { | |||||
| Float.parseFloat(value); | |||||
| return true; | |||||
| } catch (NumberFormatException e) { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| if (Settings.TYPE_DATE.equals(type)) { | |||||
| try { | |||||
| LocalDate.parse(value, DateTimeFormatter.ISO_DATE); | |||||
| return true; | |||||
| } catch (DateTimeParseException e) { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| if (Settings.TYPE_TIME.equals(type)) { | |||||
| try { | |||||
| LocalTime.parse(value, DateTimeFormatter.ISO_TIME); | |||||
| return true; | |||||
| } catch (DateTimeParseException e) { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| if (Settings.TYPE_DATETIME.equals(type)) { | |||||
| try { | |||||
| LocalDateTime.parse(value, DateTimeFormatter.ISO_DATE_TIME); | |||||
| return true; | |||||
| } catch (DateTimeParseException e) { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| if (StringUtils.indexOf(type, "/") >= 0) { | |||||
| for (String t : type.split("/")) { | |||||
| if (t.equals(value)) | |||||
| return true; | |||||
| } | |||||
| return false; | |||||
| } | |||||
| return false; | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void update(String name, String value) { | |||||
| Settings settings = this.findByName(name) | |||||
| .orElseThrow(InternalServerErrorException::new); | |||||
| if (!validateType(settings.getType(), value)) { | |||||
| throw new InternalServerErrorException(); | |||||
| } | |||||
| settings.setValue(value); | |||||
| this.save(settings); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void update(String name, LocalDate date) { | |||||
| this.update(name, date.format(DateTimeFormatter.ISO_DATE)); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void update(String name, LocalDateTime datetime) { | |||||
| this.update(name, datetime.format(DateTimeFormatter.ISO_DATE_TIME)); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void update(String name, LocalTime time) { | |||||
| this.update(name, time.format(DateTimeFormatter.ISO_TIME)); | |||||
| } | |||||
| public String getString(String name) { | |||||
| return this.findByName(name) | |||||
| .map(Settings::getValue) | |||||
| .orElseThrow(InternalServerErrorException::new); | |||||
| } | |||||
| public int getInt(String name) { | |||||
| return this.findByName(name) | |||||
| .map(Settings::getValue) | |||||
| .map(v -> { | |||||
| try { | |||||
| return Integer.parseInt(v); | |||||
| } catch (final NumberFormatException nfe) { | |||||
| return null; | |||||
| } | |||||
| }) | |||||
| .orElseThrow(InternalServerErrorException::new); | |||||
| } | |||||
| public double getDouble(String name) { | |||||
| return this.findByName(name) | |||||
| .map(Settings::getValue) | |||||
| .map(v -> { | |||||
| try { | |||||
| return Double.parseDouble(v); | |||||
| } catch (final NumberFormatException nfe) { | |||||
| return null; | |||||
| } | |||||
| }) | |||||
| .orElseThrow(InternalServerErrorException::new); | |||||
| } | |||||
| public boolean getBoolean(String name) { | |||||
| return this.findByName(name) | |||||
| .map(Settings::getValue) | |||||
| .map(Settings.VALUE_BOOLEAN_TRUE::equals) | |||||
| .orElseThrow(InternalServerErrorException::new); | |||||
| } | |||||
| public LocalDate getDate(String name) { | |||||
| return this.getDate(name, DateTimeFormatter.ISO_DATE); | |||||
| } | |||||
| private LocalDate getDate(String name, DateTimeFormatter formatter) { | |||||
| return this.findByName(name) | |||||
| .map(Settings::getValue) | |||||
| .map(v -> { | |||||
| try { | |||||
| return LocalDate.parse(v, formatter); | |||||
| } catch (DateTimeParseException e) { | |||||
| return null; | |||||
| } | |||||
| }) | |||||
| .orElseThrow(InternalServerErrorException::new); | |||||
| } | |||||
| public LocalDateTime getDatetime(String name) { | |||||
| return this.getDatetime(name, DateTimeFormatter.ISO_DATE_TIME); | |||||
| } | |||||
| private LocalDateTime getDatetime(String name, DateTimeFormatter formatter) { | |||||
| return this.findByName(name) | |||||
| .map(Settings::getValue) | |||||
| .map(v -> { | |||||
| try { | |||||
| return LocalDateTime.parse(v, formatter); | |||||
| } catch (DateTimeParseException e) { | |||||
| return null; | |||||
| } | |||||
| }) | |||||
| .orElseThrow(InternalServerErrorException::new); | |||||
| } | |||||
| public LocalTime getTime(String name) { | |||||
| return this.getTime(name, DateTimeFormatter.ISO_TIME); | |||||
| } | |||||
| private LocalTime getTime(String name, DateTimeFormatter formatter) { | |||||
| return this.findByName(name) | |||||
| .map(Settings::getValue) | |||||
| .map(v -> { | |||||
| try { | |||||
| return LocalTime.parse(v, formatter); | |||||
| } catch (DateTimeParseException e) { | |||||
| return null; | |||||
| } | |||||
| }) | |||||
| .orElseThrow(InternalServerErrorException::new); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,69 @@ | |||||
| package com.ffii.core.settings.web; | |||||
| import java.util.List; | |||||
| import javax.validation.Valid; | |||||
| import javax.validation.constraints.NotBlank; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.web.bind.annotation.GetMapping; | |||||
| import org.springframework.web.bind.annotation.PatchMapping; | |||||
| import org.springframework.web.bind.annotation.PathVariable; | |||||
| import org.springframework.web.bind.annotation.RequestBody; | |||||
| import org.springframework.web.bind.annotation.RequestMapping; | |||||
| import org.springframework.web.bind.annotation.ResponseStatus; | |||||
| import org.springframework.web.bind.annotation.RestController; | |||||
| import com.ffii.core.exception.BadRequestException; | |||||
| import com.ffii.core.exception.NotFoundException; | |||||
| import com.ffii.core.settings.entity.Settings; | |||||
| import com.ffii.core.settings.service.SettingsService; | |||||
| import com.ffii.core.support.AbstractController; | |||||
| import io.swagger.v3.oas.annotations.Operation; | |||||
| @RestController | |||||
| @RequestMapping("/protected/settings") | |||||
| public class SettingsController extends AbstractController { | |||||
| private SettingsService settingsService; | |||||
| public SettingsController(SettingsService settingsService) { | |||||
| this.settingsService = settingsService; | |||||
| } | |||||
| @Operation(summary = "list system settings") | |||||
| @GetMapping | |||||
| // @PreAuthorize("hasAuthority('ADMIN')") | |||||
| public List<Settings> listAll() { | |||||
| return this.settingsService.listAll(); | |||||
| } | |||||
| @Operation(summary = "update system setting") | |||||
| @PatchMapping("/{name}") | |||||
| // @PreAuthorize("hasAuthority('ADMIN')") | |||||
| @ResponseStatus(HttpStatus.NO_CONTENT) | |||||
| public void update(@PathVariable String name, @RequestBody @Valid UpdateReq body) { | |||||
| Settings entity = this.settingsService.findByName(name) | |||||
| .orElseThrow(NotFoundException::new); | |||||
| if (!this.settingsService.validateType(entity.getType(), body.value)) { | |||||
| throw new BadRequestException(); | |||||
| } | |||||
| entity.setValue(body.value); | |||||
| this.settingsService.save(entity); | |||||
| } | |||||
| public static class UpdateReq { | |||||
| @NotBlank | |||||
| private String value; | |||||
| public String getValue() { | |||||
| return value; | |||||
| } | |||||
| public void setValue(String value) { | |||||
| this.value = value; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,42 @@ | |||||
| package com.ffii.core.support; | |||||
| import java.io.Serializable; | |||||
| import java.util.Optional; | |||||
| import org.springframework.transaction.annotation.Transactional; | |||||
| import org.springframework.util.Assert; | |||||
| import com.ffii.core.entity.BaseEntity; | |||||
| import com.ffii.core.exception.ConflictException; | |||||
| /** @author Fung */ | |||||
| public abstract class AbstractBaseEntityService<T extends BaseEntity<ID>, ID extends Serializable, D extends AbstractDao<T, ID>> | |||||
| extends AbstractIdEntityService<T, ID, D> { | |||||
| public AbstractBaseEntityService(JdbcDao jdbcDao, D dao) { | |||||
| super(jdbcDao, dao); | |||||
| } | |||||
| /** find and check versionId */ | |||||
| public Optional<T> find(ID id, int version) { | |||||
| Assert.notNull(id, "id must not be null"); | |||||
| return dao.findById(id) | |||||
| .map(entity -> { | |||||
| if (entity.getVersion() != version) throw new ConflictException("OPTIMISTIC_LOCK"); | |||||
| return entity; | |||||
| }); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void markDelete(ID id) { | |||||
| Assert.notNull(id, "id must not be null"); | |||||
| find(id).ifPresent(t -> markDelete(t)); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void markDelete(T entity) { | |||||
| Assert.notNull(entity, "entity must not be null"); | |||||
| entity.setDeleted(Boolean.TRUE); | |||||
| save(entity); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,10 @@ | |||||
| package com.ffii.core.support; | |||||
| import org.apache.commons.logging.Log; | |||||
| import org.apache.commons.logging.LogFactory; | |||||
| /** @author Fung */ | |||||
| public class AbstractController { | |||||
| protected final Log logger = LogFactory.getLog(getClass()); | |||||
| } | |||||
| @@ -0,0 +1,16 @@ | |||||
| package com.ffii.core.support; | |||||
| import java.io.Serializable; | |||||
| import org.springframework.data.jpa.repository.JpaRepository; | |||||
| import org.springframework.data.repository.NoRepositoryBean; | |||||
| import com.ffii.core.entity.IdEntity; | |||||
| /** | |||||
| * @author Fung | |||||
| * @see https://docs.spring.io/spring-data/jpa/docs/2.7.0/reference/html/#jpa.query-methods.query-creation | |||||
| */ | |||||
| @NoRepositoryBean | |||||
| public interface AbstractDao<T extends IdEntity<ID>, ID extends Serializable> extends JpaRepository<T, ID> { | |||||
| } | |||||
| @@ -0,0 +1,65 @@ | |||||
| package com.ffii.core.support; | |||||
| import java.io.Serializable; | |||||
| import java.util.List; | |||||
| import java.util.Optional; | |||||
| import org.springframework.transaction.annotation.Transactional; | |||||
| import org.springframework.util.Assert; | |||||
| import com.ffii.core.entity.IdEntity; | |||||
| /** @author Fung */ | |||||
| public abstract class AbstractIdEntityService<T extends IdEntity<ID>, ID extends Serializable, D extends AbstractDao<T, ID>> | |||||
| extends AbstractService { | |||||
| protected D dao; | |||||
| public AbstractIdEntityService(JdbcDao jdbcDao, D dao) { | |||||
| super(jdbcDao); | |||||
| this.dao = dao; | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public T save(T entity) { | |||||
| Assert.notNull(entity, "entity must not be null"); | |||||
| return this.dao.save(entity); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public T saveAndFlush(T entity) { | |||||
| Assert.notNull(entity, "entity must not be null"); | |||||
| return this.dao.saveAndFlush(entity); | |||||
| } | |||||
| public List<T> listAll() { | |||||
| return this.dao.findAll(); | |||||
| } | |||||
| public Optional<T> find(ID id) { | |||||
| Assert.notNull(id, "id must not be null"); | |||||
| return this.dao.findById(id); | |||||
| } | |||||
| public boolean existsById(ID id) { | |||||
| Assert.notNull(id, "id must not be null"); | |||||
| return this.dao.existsById(id); | |||||
| } | |||||
| public List<T> findAllByIds(List<ID> ids) { | |||||
| Assert.notNull(ids, "ids must not be null"); | |||||
| return this.dao.findAllById(ids); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void delete(ID id) { | |||||
| Assert.notNull(id, "id must not be null"); | |||||
| this.dao.deleteById(id); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void delete(T entity) { | |||||
| Assert.notNull(entity, "entity must not be null"); | |||||
| this.dao.delete(entity); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,15 @@ | |||||
| package com.ffii.core.support; | |||||
| import org.apache.commons.logging.Log; | |||||
| import org.apache.commons.logging.LogFactory; | |||||
| /** @author Fung */ | |||||
| public abstract class AbstractService { | |||||
| protected final Log logger = LogFactory.getLog(getClass()); | |||||
| protected JdbcDao jdbcDao; | |||||
| public AbstractService(JdbcDao jdbcDao) { | |||||
| this.jdbcDao = jdbcDao; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,44 @@ | |||||
| package com.ffii.core.support; | |||||
| import java.util.UUID; | |||||
| import org.apache.commons.logging.Log; | |||||
| import org.apache.commons.logging.LogFactory; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.http.ResponseEntity; | |||||
| import org.springframework.security.access.AccessDeniedException; | |||||
| import org.springframework.web.bind.annotation.ExceptionHandler; | |||||
| import org.springframework.web.bind.annotation.ResponseStatus; | |||||
| import org.springframework.web.bind.annotation.RestControllerAdvice; | |||||
| import org.springframework.web.server.ResponseStatusException; | |||||
| import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; | |||||
| import com.ffii.core.exception.ConflictException; | |||||
| import com.ffii.core.exception.InternalServerErrorException; | |||||
| import com.ffii.core.response.ErrorRes; | |||||
| import com.ffii.core.response.FailureRes; | |||||
| @RestControllerAdvice | |||||
| public class ErrorHandler extends ResponseEntityExceptionHandler { | |||||
| private final Log logger = LogFactory.getLog(getClass()); | |||||
| @ExceptionHandler({ ConflictException.class, ResponseStatusException.class }) | |||||
| public ResponseEntity<FailureRes> error409422(final Exception ex) { | |||||
| ResponseStatusException e = (ResponseStatusException) ex; | |||||
| return new ResponseEntity<>(new FailureRes(e.getReason()), e.getStatus()); | |||||
| } | |||||
| @ExceptionHandler(AccessDeniedException.class) | |||||
| public ResponseEntity<?> error403(final Exception ex) { | |||||
| return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); | |||||
| } | |||||
| @ExceptionHandler({ InternalServerErrorException.class, Exception.class }) | |||||
| @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) | |||||
| public ResponseEntity<ErrorRes> error500(final Exception ex) { | |||||
| UUID traceId = UUID.randomUUID(); | |||||
| logger.error("traceId: " + traceId, ex); | |||||
| return new ResponseEntity<>(new ErrorRes(traceId.toString()), HttpStatus.INTERNAL_SERVER_ERROR); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,432 @@ | |||||
| package com.ffii.core.support; | |||||
| import java.math.BigDecimal; | |||||
| import java.time.LocalDate; | |||||
| import java.time.LocalDateTime; | |||||
| import java.util.List; | |||||
| import java.util.Map; | |||||
| import java.util.Optional; | |||||
| import javax.sql.DataSource; | |||||
| import org.apache.commons.lang3.StringUtils; | |||||
| import org.springframework.dao.EmptyResultDataAccessException; | |||||
| import org.springframework.dao.IncorrectResultSizeDataAccessException; | |||||
| import org.springframework.dao.InvalidDataAccessApiUsageException; | |||||
| import org.springframework.jdbc.BadSqlGrammarException; | |||||
| import org.springframework.jdbc.IncorrectResultSetColumnCountException; | |||||
| import org.springframework.jdbc.core.BeanPropertyRowMapper; | |||||
| import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; | |||||
| import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; | |||||
| import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils; | |||||
| /** @author Fung */ | |||||
| public class JdbcDao { | |||||
| private NamedParameterJdbcTemplate template; | |||||
| public JdbcDao(DataSource dataSource) { | |||||
| this.template = new NamedParameterJdbcTemplate(dataSource); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public String queryForString(String sql) { | |||||
| return this.queryForString(sql, (Map<String, ?>) null); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public String queryForString(String sql, Map<String, ?> paramMap) { | |||||
| try { | |||||
| return this.template.queryForObject(sql, paramMap, String.class); | |||||
| } catch (EmptyResultDataAccessException e) { | |||||
| return StringUtils.EMPTY; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public String queryForString(String sql, Object paramObj) { | |||||
| try { | |||||
| return this.template.queryForObject(sql, new BeanPropertySqlParameterSource(paramObj), String.class); | |||||
| } catch (EmptyResultDataAccessException e) { | |||||
| return StringUtils.EMPTY; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @return {@code true} if non-zero, {@code false} if zero | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public boolean queryForBoolean(String sql) { | |||||
| return this.queryForBoolean(sql, (Map<String, ?>) null); | |||||
| } | |||||
| /** | |||||
| * @return {@code true} if non-zero, {@code false} if zero | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public boolean queryForBoolean(String sql, Map<String, ?> paramMap) { | |||||
| try { | |||||
| return this.template.queryForObject(sql, paramMap, Boolean.class); | |||||
| } catch (EmptyResultDataAccessException e) { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @return {@code true} if non-zero, {@code false} if zero | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public boolean queryForBoolean(String sql, Object paramObj) { | |||||
| try { | |||||
| return this.template.queryForObject(sql, new BeanPropertySqlParameterSource(paramObj), Boolean.class); | |||||
| } catch (EmptyResultDataAccessException e) { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public int queryForInt(String sql) { | |||||
| return this.queryForInt(sql, (Map<String, ?>) null); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public int queryForInt(String sql, Map<String, ?> paramMap) { | |||||
| try { | |||||
| return this.template.queryForObject(sql, paramMap, Integer.class); | |||||
| } catch (EmptyResultDataAccessException e) { | |||||
| return 0; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public int queryForInt(String sql, Object paramObj) { | |||||
| try { | |||||
| return this.template.queryForObject(sql, | |||||
| new BeanPropertySqlParameterSource(paramObj), Integer.class); | |||||
| } catch (EmptyResultDataAccessException e) { | |||||
| return 0; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public BigDecimal queryForDecimal(String sql) { | |||||
| return this.queryForDecimal(sql, (Map<String, ?>) null); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public BigDecimal queryForDecimal(String sql, Map<String, ?> paramMap) { | |||||
| try { | |||||
| return this.template.queryForObject(sql, paramMap, BigDecimal.class); | |||||
| } catch (EmptyResultDataAccessException e) { | |||||
| return BigDecimal.ZERO; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public BigDecimal queryForDecimal(String sql, Object paramObj) { | |||||
| try { | |||||
| return this.template.queryForObject(sql, | |||||
| new BeanPropertySqlParameterSource(paramObj), BigDecimal.class); | |||||
| } catch (EmptyResultDataAccessException e) { | |||||
| return BigDecimal.ZERO; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public <T> Optional<T> queryForEntity(String sql, Class<T> entity) { | |||||
| return this.queryForEntity(sql, (Map<String, ?>) null, entity); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public <T> Optional<T> queryForEntity(String sql, Map<String, ?> paramMap, Class<T> entity) { | |||||
| try { | |||||
| return Optional.of(this.template.queryForObject(sql, paramMap, | |||||
| new BeanPropertyRowMapper<T>(entity))); | |||||
| } catch (EmptyResultDataAccessException e) { | |||||
| return Optional.empty(); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSizeDataAccessException: Incorrect result size | |||||
| */ | |||||
| public <T> Optional<T> queryForEntity(String sql, Object paramObj, Class<T> entity) { | |||||
| try { | |||||
| return Optional.of(this.template.queryForObject(sql, | |||||
| new BeanPropertySqlParameterSource(paramObj), new BeanPropertyRowMapper<T>(entity))); | |||||
| } catch (EmptyResultDataAccessException e) { | |||||
| return Optional.empty(); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| */ | |||||
| public <T> List<T> queryForList(String sql, Class<T> entity) { | |||||
| return this.queryForList(sql, (Map<String, ?>) null, entity); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| */ | |||||
| public <T> List<T> queryForList(String sql, Map<String, ?> paramMap, Class<T> entity) { | |||||
| return this.template.query(sql, paramMap, new BeanPropertyRowMapper<T>(entity)); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| */ | |||||
| public <T> List<T> queryForList(String sql, Object paramObj, Class<T> entity) { | |||||
| return this.template.query(sql, new BeanPropertySqlParameterSource(paramObj), new BeanPropertyRowMapper<T>(entity)); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| */ | |||||
| public List<Integer> queryForInts(String sql) { | |||||
| return this.queryForInts(sql, (Map<String, ?>) null); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| */ | |||||
| public List<Integer> queryForInts(String sql, Map<String, ?> paramMap) { | |||||
| return this.template.queryForList(sql, paramMap, Integer.class); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| */ | |||||
| public List<Integer> queryForInts(String sql, Object paramObj) { | |||||
| return this.template.queryForList(sql, new BeanPropertySqlParameterSource(paramObj), Integer.class); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| */ | |||||
| public List<LocalDate> queryForDates(String sql) { | |||||
| return this.queryForDates(sql, (Map<String, ?>) null); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| */ | |||||
| public List<LocalDate> queryForDates(String sql, Map<String, ?> paramMap) { | |||||
| return this.template.queryForList(sql, paramMap, LocalDate.class); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| */ | |||||
| public List<LocalDate> queryForDates(String sql, Object paramObj) { | |||||
| return this.template.queryForList(sql, new BeanPropertySqlParameterSource(paramObj), LocalDate.class); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| */ | |||||
| public List<LocalDateTime> queryForDatetimes(String sql) { | |||||
| return this.queryForDatetimes(sql, (Map<String, ?>) null); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| */ | |||||
| public List<LocalDateTime> queryForDatetimes(String sql, Map<String, ?> paramMap) { | |||||
| return this.template.queryForList(sql, paramMap, LocalDateTime.class); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| */ | |||||
| public List<LocalDateTime> queryForDatetimes(String sql, Object paramObj) { | |||||
| return this.template.queryForList(sql, new BeanPropertySqlParameterSource(paramObj), LocalDateTime.class); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| */ | |||||
| public List<String> queryForStrings(String sql) { | |||||
| return this.queryForStrings(sql, (Map<String, ?>) null); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| */ | |||||
| public List<String> queryForStrings(String sql, Map<String, ?> paramMap) { | |||||
| return this.template.queryForList(sql, paramMap, String.class); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| * @throws IncorrectResultSetColumnCountException Incorrect column count | |||||
| */ | |||||
| public List<String> queryForStrings(String sql, Object paramObj) { | |||||
| return this.template.queryForList(sql, new BeanPropertySqlParameterSource(paramObj), String.class); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| */ | |||||
| public List<Map<String, Object>> queryForList(String sql) { | |||||
| return this.queryForList(sql, (Map<String, ?>) null); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| */ | |||||
| public List<Map<String, Object>> queryForList(String sql, Map<String, ?> paramMap) { | |||||
| return this.template.queryForList(sql, paramMap); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| */ | |||||
| public List<Map<String, Object>> queryForList(String sql, Object paramObj) { | |||||
| return this.template.queryForList(sql, new BeanPropertySqlParameterSource(paramObj)); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| */ | |||||
| public Optional<Map<String, Object>> queryForMap(String sql) { | |||||
| return this.queryForMap(sql, (Map<String, ?>) null); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| */ | |||||
| public Optional<Map<String, Object>> queryForMap(String sql, Map<String, ?> paramMap) { | |||||
| try { | |||||
| return Optional.of(this.template.queryForMap(sql, paramMap)); | |||||
| } catch (EmptyResultDataAccessException e) { | |||||
| return Optional.empty(); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| */ | |||||
| public Optional<Map<String, Object>> queryForMap(String sql, Object paramObj) { | |||||
| try { | |||||
| return Optional.of(this.template.queryForMap(sql, new BeanPropertySqlParameterSource(paramObj))); | |||||
| } catch (EmptyResultDataAccessException e) { | |||||
| return Optional.empty(); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| */ | |||||
| public int executeUpdate(String sql) { | |||||
| return this.executeUpdate(sql, (Map<String, ?>) null); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| */ | |||||
| public int executeUpdate(String sql, Map<String, ?> paramMap) { | |||||
| return this.template.update(sql, paramMap); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| */ | |||||
| public int executeUpdate(String sql, Object paramObj) { | |||||
| return this.template.update(sql, new BeanPropertySqlParameterSource(paramObj)); | |||||
| } | |||||
| /** | |||||
| * @throws BadSqlGrammarException sql error | |||||
| * @throws InvalidDataAccessApiUsageException params missing when needed | |||||
| */ | |||||
| public int[] batchUpdate(String sql, List<?> paramsMapOrObject) { | |||||
| return this.template.batchUpdate(sql, SqlParameterSourceUtils.createBatch(paramsMapOrObject)); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,7 @@ | |||||
| package com.ffii.core.user.dao; | |||||
| import com.ffii.core.support.AbstractDao; | |||||
| import com.ffii.core.user.entity.Group; | |||||
| public interface GroupDao extends AbstractDao<Group, Integer> { | |||||
| } | |||||
| @@ -0,0 +1,13 @@ | |||||
| package com.ffii.core.user.dao; | |||||
| import java.util.Optional; | |||||
| import com.ffii.core.support.AbstractDao; | |||||
| import com.ffii.core.user.entity.User; | |||||
| /** @author Fung */ | |||||
| public interface UserDao extends AbstractDao<User, Integer> { | |||||
| public Optional<User> findByIdAndDeletedFalse(int id); | |||||
| public Optional<User> findByUsernameAndDeletedFalse(String username); | |||||
| } | |||||
| @@ -0,0 +1,37 @@ | |||||
| package com.ffii.core.user.entity; | |||||
| import javax.persistence.Column; | |||||
| import javax.persistence.Entity; | |||||
| import javax.persistence.Table; | |||||
| import javax.validation.constraints.NotNull; | |||||
| import com.ffii.core.entity.BaseEntity; | |||||
| @Entity | |||||
| @Table(name = "`group`") | |||||
| public class Group extends BaseEntity<Integer> { | |||||
| @NotNull | |||||
| @Column | |||||
| private String name; | |||||
| @Column | |||||
| private String description; | |||||
| public String getName() { | |||||
| return name; | |||||
| } | |||||
| public void setName(String name) { | |||||
| this.name = name; | |||||
| } | |||||
| public String getDescription() { | |||||
| return description; | |||||
| } | |||||
| public void setDescription(String description) { | |||||
| this.description = description; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,287 @@ | |||||
| package com.ffii.core.user.entity; | |||||
| import java.time.LocalDate; | |||||
| import java.util.Collection; | |||||
| import javax.persistence.Column; | |||||
| import javax.persistence.Entity; | |||||
| import javax.persistence.Table; | |||||
| import javax.persistence.Transient; | |||||
| import javax.validation.constraints.NotBlank; | |||||
| import javax.validation.constraints.NotNull; | |||||
| import org.springframework.security.core.GrantedAuthority; | |||||
| import org.springframework.security.core.userdetails.UserDetails; | |||||
| import com.fasterxml.jackson.annotation.JsonIgnore; | |||||
| import com.ffii.core.entity.BaseEntity; | |||||
| /** @author Fung */ | |||||
| @Entity | |||||
| @Table(name = "user") | |||||
| public class User extends BaseEntity<Integer> implements UserDetails { | |||||
| @NotBlank | |||||
| @Column(unique = true) | |||||
| private String username; | |||||
| @JsonIgnore | |||||
| @NotBlank | |||||
| @Column | |||||
| private String password; | |||||
| @NotNull | |||||
| @Column | |||||
| private Boolean locked = Boolean.FALSE; | |||||
| @NotBlank | |||||
| @Column | |||||
| private String name; | |||||
| @Column | |||||
| private LocalDate expiryDate; | |||||
| @JsonIgnore | |||||
| @Transient | |||||
| private Collection<GrantedAuthority> authorities; | |||||
| // @NotNull | |||||
| @Column | |||||
| private Integer companyId; | |||||
| // @NotNull | |||||
| @Column | |||||
| private Integer customerId; | |||||
| @Column | |||||
| private String locale; | |||||
| @Column | |||||
| private String fullname; | |||||
| @Column | |||||
| private String firstname; | |||||
| @Column | |||||
| private String lastname; | |||||
| // @NotNull | |||||
| @Column | |||||
| private Integer deptId; | |||||
| @Column | |||||
| private String department; | |||||
| @Column | |||||
| private String title; | |||||
| @Column | |||||
| private String email; | |||||
| @Column | |||||
| private String phone1; | |||||
| @Column | |||||
| private String phone2; | |||||
| @Column | |||||
| private String landingPage; | |||||
| @Column | |||||
| private String remarks; | |||||
| public boolean isLocked() { | |||||
| return this.locked == null ? false : this.locked; | |||||
| } | |||||
| // getter & setter | |||||
| public void setUsername(String username) { | |||||
| this.username = username; | |||||
| } | |||||
| public void setPassword(String password) { | |||||
| this.password = password; | |||||
| } | |||||
| public Boolean getLocked() { | |||||
| return locked; | |||||
| } | |||||
| public void setLocked(Boolean locked) { | |||||
| this.locked = locked; | |||||
| } | |||||
| public String getName() { | |||||
| return name; | |||||
| } | |||||
| public void setName(String name) { | |||||
| this.name = name; | |||||
| } | |||||
| public LocalDate getExpiryDate() { | |||||
| return expiryDate; | |||||
| } | |||||
| public void setExpiryDate(LocalDate expiryDate) { | |||||
| this.expiryDate = expiryDate; | |||||
| } | |||||
| public void setAuthorities(Collection<GrantedAuthority> authorities) { | |||||
| this.authorities = authorities; | |||||
| } | |||||
| public Integer getCompanyId() { | |||||
| return companyId; | |||||
| } | |||||
| public void setCompanyId(Integer companyId) { | |||||
| this.companyId = companyId; | |||||
| } | |||||
| public Integer getCustomerId() { | |||||
| return customerId; | |||||
| } | |||||
| public void setCustomerId(Integer customerId) { | |||||
| this.customerId = customerId; | |||||
| } | |||||
| public String getLocale() { | |||||
| return locale; | |||||
| } | |||||
| public void setLocale(String locale) { | |||||
| this.locale = locale; | |||||
| } | |||||
| public String getFullname() { | |||||
| return fullname; | |||||
| } | |||||
| public void setFullname(String fullname) { | |||||
| this.fullname = fullname; | |||||
| } | |||||
| 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 Integer getDeptId() { | |||||
| return deptId; | |||||
| } | |||||
| public void setDeptId(Integer deptId) { | |||||
| this.deptId = deptId; | |||||
| } | |||||
| public String getTitle() { | |||||
| return title; | |||||
| } | |||||
| public void setTitle(String title) { | |||||
| this.title = title; | |||||
| } | |||||
| public String getEmail() { | |||||
| return email; | |||||
| } | |||||
| public void setEmail(String email) { | |||||
| this.email = email; | |||||
| } | |||||
| public String getPhone1() { | |||||
| return phone1; | |||||
| } | |||||
| public void setPhone1(String phone1) { | |||||
| this.phone1 = phone1; | |||||
| } | |||||
| public String getPhone2() { | |||||
| return phone2; | |||||
| } | |||||
| public void setPhone2(String phone2) { | |||||
| this.phone2 = phone2; | |||||
| } | |||||
| public String getRemarks() { | |||||
| return remarks; | |||||
| } | |||||
| public void setRemarks(String remarks) { | |||||
| this.remarks = remarks; | |||||
| } | |||||
| // override | |||||
| @Override | |||||
| public Collection<? extends GrantedAuthority> getAuthorities() { | |||||
| return this.authorities; | |||||
| } | |||||
| @Override | |||||
| public String getPassword() { | |||||
| return this.password; | |||||
| } | |||||
| @Override | |||||
| public String getUsername() { | |||||
| return this.username; | |||||
| } | |||||
| @Override | |||||
| public boolean isAccountNonExpired() { | |||||
| return this.getExpiryDate() == null || this.getExpiryDate().isAfter(LocalDate.now()); | |||||
| } | |||||
| @Override | |||||
| public boolean isAccountNonLocked() { | |||||
| return !this.isLocked(); | |||||
| } | |||||
| @JsonIgnore | |||||
| @Override | |||||
| public boolean isCredentialsNonExpired() { | |||||
| return true; | |||||
| } | |||||
| @JsonIgnore | |||||
| @Override | |||||
| public boolean isEnabled() { | |||||
| return true; | |||||
| } | |||||
| public String getDepartment() { | |||||
| return department; | |||||
| } | |||||
| public void setDepartment(String department) { | |||||
| this.department = department; | |||||
| } | |||||
| public String getLandingPage() { | |||||
| return landingPage; | |||||
| } | |||||
| public void setLandingPage(String landingPage) { | |||||
| this.landingPage = landingPage; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,20 @@ | |||||
| package com.ffii.core.user.req; | |||||
| import javax.validation.constraints.Pattern; | |||||
| import javax.validation.constraints.Size; | |||||
| public class ForgetPwReq { | |||||
| @Size(max = 30) | |||||
| @Pattern(regexp = "^[A-Za-z0-9]+$") | |||||
| private String username; | |||||
| public String getUsername() { | |||||
| return username; | |||||
| } | |||||
| public void setUsername(String username) { | |||||
| this.username = username; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,21 @@ | |||||
| package com.ffii.core.user.req; | |||||
| import javax.validation.constraints.Pattern; | |||||
| import javax.validation.constraints.Size; | |||||
| /** @author Fung */ | |||||
| public class NewUserReq extends UpdateUserReq { | |||||
| @Size(max = 30) | |||||
| @Pattern(regexp = "^[A-Za-z0-9]+$") | |||||
| private String username; | |||||
| public String getUsername() { | |||||
| return username; | |||||
| } | |||||
| public void setUsername(String username) { | |||||
| this.username = username; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,80 @@ | |||||
| package com.ffii.core.user.req; | |||||
| import java.util.List; | |||||
| import javax.validation.constraints.NotNull; | |||||
| public class SaveGroupReq { | |||||
| private Integer id; | |||||
| @NotNull | |||||
| private String name; | |||||
| private String description; | |||||
| @NotNull | |||||
| private List<Integer> addUserIds; | |||||
| @NotNull | |||||
| private List<Integer> removeUserIds; | |||||
| @NotNull | |||||
| private List<Integer> addAuthIds; | |||||
| @NotNull | |||||
| private List<Integer> removeAuthIds; | |||||
| public Integer getId() { | |||||
| return id; | |||||
| } | |||||
| public void setId(Integer id) { | |||||
| this.id = id; | |||||
| } | |||||
| public String getName() { | |||||
| return name; | |||||
| } | |||||
| public void setName(String name) { | |||||
| this.name = name; | |||||
| } | |||||
| public String getDescription() { | |||||
| return description; | |||||
| } | |||||
| public void setDescription(String description) { | |||||
| this.description = description; | |||||
| } | |||||
| public List<Integer> getAddUserIds() { | |||||
| return addUserIds; | |||||
| } | |||||
| public void setAddUserIds(List<Integer> addUserIds) { | |||||
| this.addUserIds = addUserIds; | |||||
| } | |||||
| public List<Integer> getRemoveUserIds() { | |||||
| return removeUserIds; | |||||
| } | |||||
| public void setRemoveUserIds(List<Integer> removeUserIds) { | |||||
| this.removeUserIds = removeUserIds; | |||||
| } | |||||
| public List<Integer> getAddAuthIds() { | |||||
| return addAuthIds; | |||||
| } | |||||
| public void setAddAuthIds(List<Integer> addAuthIds) { | |||||
| this.addAuthIds = addAuthIds; | |||||
| } | |||||
| public List<Integer> getRemoveAuthIds() { | |||||
| return removeAuthIds; | |||||
| } | |||||
| public void setRemoveAuthIds(List<Integer> removeAuthIds) { | |||||
| this.removeAuthIds = removeAuthIds; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,69 @@ | |||||
| package com.ffii.core.user.req; | |||||
| public class SearchUserReq { | |||||
| private Integer id; | |||||
| private Integer groupId; | |||||
| private String username; | |||||
| private String name; | |||||
| private Boolean locked; | |||||
| private Integer start; | |||||
| private Integer limit; | |||||
| public Integer getId() { | |||||
| return id; | |||||
| } | |||||
| public void setId(Integer id) { | |||||
| this.id = id; | |||||
| } | |||||
| public Integer getGroupId() { | |||||
| return groupId; | |||||
| } | |||||
| public void setGroupId(Integer groupId) { | |||||
| this.groupId = groupId; | |||||
| } | |||||
| public String getUsername() { | |||||
| return username; | |||||
| } | |||||
| public void setUsername(String username) { | |||||
| this.username = username; | |||||
| } | |||||
| public String getName() { | |||||
| return name; | |||||
| } | |||||
| public void setName(String name) { | |||||
| this.name = name; | |||||
| } | |||||
| public Integer getStart() { | |||||
| return start; | |||||
| } | |||||
| public void setStart(Integer start) { | |||||
| this.start = start; | |||||
| } | |||||
| public Integer getLimit() { | |||||
| return limit; | |||||
| } | |||||
| public void setLimit(Integer limit) { | |||||
| this.limit = limit; | |||||
| } | |||||
| public Boolean getLocked() { | |||||
| return locked; | |||||
| } | |||||
| public void setLocked(Boolean locked) { | |||||
| this.locked = locked; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,171 @@ | |||||
| package com.ffii.core.user.req; | |||||
| import java.time.LocalDate; | |||||
| import java.util.List; | |||||
| import javax.validation.constraints.Email; | |||||
| import javax.validation.constraints.NotBlank; | |||||
| import javax.validation.constraints.NotNull; | |||||
| import javax.validation.constraints.Size; | |||||
| /** @author Fung */ | |||||
| public class UpdateUserReq { | |||||
| @NotNull | |||||
| private Boolean locked; | |||||
| @Size(max = 90) | |||||
| @NotBlank | |||||
| private String name; | |||||
| private String firstname; | |||||
| private String lastname; | |||||
| private LocalDate expiryDate; | |||||
| private String locale; | |||||
| @NotBlank | |||||
| private String landingPage; | |||||
| private String remarks; | |||||
| @NotBlank | |||||
| private String email; | |||||
| // @NotBlank | |||||
| private String department; | |||||
| @NotNull | |||||
| private Integer deptId; | |||||
| @NotNull | |||||
| private List<Integer> addGroupIds; | |||||
| @NotNull | |||||
| private List<Integer> removeGroupIds; | |||||
| @NotNull | |||||
| private List<Integer> addAuthIds; | |||||
| @NotNull | |||||
| private List<Integer> removeAuthIds; | |||||
| public Boolean getLocked() { | |||||
| return locked; | |||||
| } | |||||
| public void setLocked(Boolean locked) { | |||||
| this.locked = locked; | |||||
| } | |||||
| public String getName() { | |||||
| return name; | |||||
| } | |||||
| public void setName(String name) { | |||||
| this.name = name; | |||||
| } | |||||
| public LocalDate getExpiryDate() { | |||||
| return expiryDate; | |||||
| } | |||||
| public void setExpiryDate(LocalDate expiryDate) { | |||||
| this.expiryDate = expiryDate; | |||||
| } | |||||
| 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 Integer getDeptId() { | |||||
| return deptId; | |||||
| } | |||||
| public void setDeptId(Integer deptId) { | |||||
| this.deptId = deptId; | |||||
| } | |||||
| public String getLocale() { | |||||
| return locale; | |||||
| } | |||||
| public void setLocale(String locale) { | |||||
| this.locale = locale; | |||||
| } | |||||
| public void setFirstname(String firstname) { | |||||
| this.firstname = firstname; | |||||
| } | |||||
| public List<Integer> getAddGroupIds() { | |||||
| return addGroupIds; | |||||
| } | |||||
| public void setAddGroupIds(List<Integer> addGroupIds) { | |||||
| this.addGroupIds = addGroupIds; | |||||
| } | |||||
| public List<Integer> getRemoveGroupIds() { | |||||
| return removeGroupIds; | |||||
| } | |||||
| public void setRemoveGroupIds(List<Integer> removeGroupIds) { | |||||
| this.removeGroupIds = removeGroupIds; | |||||
| } | |||||
| public List<Integer> getAddAuthIds() { | |||||
| return addAuthIds; | |||||
| } | |||||
| public void setAddAuthIds(List<Integer> addAuthIds) { | |||||
| this.addAuthIds = addAuthIds; | |||||
| } | |||||
| public List<Integer> getRemoveAuthIds() { | |||||
| return removeAuthIds; | |||||
| } | |||||
| public void setRemoveAuthIds(List<Integer> removeAuthIds) { | |||||
| this.removeAuthIds = removeAuthIds; | |||||
| } | |||||
| public String getRemarks() { | |||||
| return remarks; | |||||
| } | |||||
| public void setRemarks(String remarks) { | |||||
| this.remarks = remarks; | |||||
| } | |||||
| public String getEmail() { | |||||
| return email; | |||||
| } | |||||
| public void setEmail(String email) { | |||||
| this.email = email; | |||||
| } | |||||
| public String getDepartment() { | |||||
| return department; | |||||
| } | |||||
| public void setDepartment(String department) { | |||||
| this.department = department; | |||||
| } | |||||
| public String getLandingPage() { | |||||
| return landingPage; | |||||
| } | |||||
| public void setLandingPage(String landingPage) { | |||||
| this.landingPage = landingPage; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,45 @@ | |||||
| package com.ffii.core.user.res; | |||||
| import java.util.List; | |||||
| import com.ffii.core.user.entity.User; | |||||
| public class LoadUserRes { | |||||
| private User data; | |||||
| private List<Integer> authIds; | |||||
| private List<Integer> groupIds; | |||||
| public LoadUserRes() { | |||||
| } | |||||
| public LoadUserRes(User data, List<Integer> authIds, List<Integer> groupIds) { | |||||
| this.data = data; | |||||
| this.authIds = authIds; | |||||
| this.groupIds = groupIds; | |||||
| } | |||||
| public User getData() { | |||||
| return data; | |||||
| } | |||||
| public void setData(User data) { | |||||
| this.data = data; | |||||
| } | |||||
| public List<Integer> getAuthIds() { | |||||
| return authIds; | |||||
| } | |||||
| public void setAuthIds(List<Integer> authIds) { | |||||
| this.authIds = authIds; | |||||
| } | |||||
| public List<Integer> getGroupIds() { | |||||
| return groupIds; | |||||
| } | |||||
| public void setGroupIds(List<Integer> groupIds) { | |||||
| this.groupIds = groupIds; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,29 @@ | |||||
| package com.ffii.core.user.service; | |||||
| import java.util.List; | |||||
| import org.springframework.stereotype.Service; | |||||
| import com.ffii.core.support.AbstractService; | |||||
| import com.ffii.core.support.JdbcDao; | |||||
| import com.ffii.core.user.service.pojo.AuthRecord; | |||||
| @Service | |||||
| public class AuthService extends AbstractService { | |||||
| public AuthService(JdbcDao jdbcDao) { | |||||
| super(jdbcDao); | |||||
| } | |||||
| public List<AuthRecord> list() { | |||||
| return jdbcDao.queryForList("SELECT" | |||||
| + " a.id," | |||||
| + " a.module," | |||||
| + " a.authority," | |||||
| + " a.name," | |||||
| + " a.description" | |||||
| + " FROM authority a" | |||||
| + " ORDER BY a.authority", AuthRecord.class); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,173 @@ | |||||
| package com.ffii.core.user.service; | |||||
| import java.text.MessageFormat; | |||||
| import java.util.List; | |||||
| import java.util.Map; | |||||
| import java.util.stream.Collectors; | |||||
| import javax.validation.Valid; | |||||
| import org.springframework.beans.BeanUtils; | |||||
| import org.springframework.stereotype.Service; | |||||
| import org.springframework.transaction.annotation.Transactional; | |||||
| import com.ffii.core.exception.NotFoundException; | |||||
| import com.ffii.core.exception.UnprocessableEntityException; | |||||
| import com.ffii.core.support.AbstractBaseEntityService; | |||||
| import com.ffii.core.support.JdbcDao; | |||||
| import com.ffii.core.user.dao.GroupDao; | |||||
| import com.ffii.core.user.entity.Group; | |||||
| import com.ffii.core.user.req.SaveGroupReq; | |||||
| import com.ffii.core.utils.Params; | |||||
| @Service | |||||
| public class GroupService extends AbstractBaseEntityService<Group, Integer, GroupDao> { | |||||
| public GroupService(JdbcDao jdbcDao, GroupDao dao) { | |||||
| super(jdbcDao, dao); | |||||
| } | |||||
| public List<Map<String, Object>> search(Map<String, Object> args) { | |||||
| StringBuilder sql = new StringBuilder("SELECT" | |||||
| + " g.*" | |||||
| + " FROM `group` g" | |||||
| + " WHERE g.deleted = FALSE"); | |||||
| if (args != null) { | |||||
| if (args.containsKey(Params.QUERY)) | |||||
| sql.append(" AND (g.name LIKE :query)"); | |||||
| if (args.containsKey(Params.ID)) | |||||
| sql.append(" AND g.id = :id"); | |||||
| if (args.containsKey(Params.NAME)) | |||||
| sql.append(" AND g.name LIKE :name"); | |||||
| if (args.containsKey("checkNameIsDuplicate")) | |||||
| sql.append(" AND BINARY g.name = :checkNameIsDuplicate AND g.deleted = 0 "); | |||||
| } | |||||
| sql.append(" ORDER BY g.name"); | |||||
| return jdbcDao.queryForList(sql.toString(), args); | |||||
| } | |||||
| public List<Map<String, Object>> searchForCombo(Map<String, Object> args) { | |||||
| StringBuilder sql = new StringBuilder("SELECT" | |||||
| + " g.id," | |||||
| + " g.name" | |||||
| + " FROM `group` g" | |||||
| + " WHERE g.deleted = FALSE"); | |||||
| if (args != null) { | |||||
| if (args.containsKey(Params.QUERY)) | |||||
| sql.append(" AND (g.name LIKE :query)"); | |||||
| if (args.containsKey(Params.ID)) | |||||
| sql.append(" AND g.id = :id"); | |||||
| } | |||||
| sql.append(" ORDER BY g.name"); | |||||
| return jdbcDao.queryForList(sql.toString(), args); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void delete(Group instance) { | |||||
| Map<String, Object> args = Map.of("groupId", instance.getId()); | |||||
| jdbcDao.executeUpdate("DELETE FROM user_group WHERE groupId = :groupId;", args); | |||||
| jdbcDao.executeUpdate("DELETE FROM group_authority WHERE groupId = :groupId;", args); | |||||
| markDelete(instance); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public Group saveOrUpdate(@Valid SaveGroupReq req) { | |||||
| Group instance; | |||||
| if (req.getId() != null) { | |||||
| instance = find(req.getId()).orElseThrow(NotFoundException::new); | |||||
| } else { | |||||
| List<Map<String, Object>> list = search(Map.of("checkNameIsDuplicate", req.getName())); | |||||
| if (list != null && list.size() > 0) { | |||||
| throw new UnprocessableEntityException( | |||||
| MessageFormat.format("Duplicate entry {0} for key 'name'", req.getName())); | |||||
| } | |||||
| instance = new Group(); | |||||
| } | |||||
| BeanUtils.copyProperties(req, instance); | |||||
| instance = save(instance); | |||||
| // try { | |||||
| // } catch (DataIntegrityViolationException e) { | |||||
| // throw new BadRequestException(MessageFormat.format("Duplicate entry {0} for | |||||
| // key 'name'", req.getName())); | |||||
| // } | |||||
| int id = instance.getId(); | |||||
| List<Map<String, Integer>> userBatchInsertValues = req.getAddUserIds().stream() | |||||
| .map(userId -> Map.of("groupId", id, "userId", userId)) | |||||
| .collect(Collectors.toList()); | |||||
| List<Map<String, Integer>> userBatchDeleteValues = req.getRemoveUserIds().stream() | |||||
| .map(userId -> Map.of("groupId", id, "userId", userId)) | |||||
| .collect(Collectors.toList()); | |||||
| if (!userBatchInsertValues.isEmpty()) { | |||||
| jdbcDao.batchUpdate( | |||||
| "INSERT IGNORE INTO user_group (groupId,userId)" | |||||
| + " VALUES (:groupId, :userId)", | |||||
| userBatchInsertValues); | |||||
| } | |||||
| if (!userBatchDeleteValues.isEmpty()) { | |||||
| jdbcDao.batchUpdate( | |||||
| "DELETE FROM user_group" | |||||
| + " WHERE groupId = :groupId AND userId = :userId", | |||||
| userBatchDeleteValues); | |||||
| } | |||||
| List<Map<String, Integer>> authBatchInsertValues = req.getAddAuthIds().stream() | |||||
| .map(authId -> Map.of("groupId", id, "authId", authId)) | |||||
| .collect(Collectors.toList()); | |||||
| List<Map<String, Integer>> authBatchDeleteValues = req.getRemoveAuthIds().stream() | |||||
| .map(authId -> Map.of("groupId", id, "authId", authId)) | |||||
| .collect(Collectors.toList()); | |||||
| if (!authBatchInsertValues.isEmpty()) { | |||||
| jdbcDao.batchUpdate( | |||||
| "INSERT IGNORE INTO group_authority (groupId, authId)" | |||||
| + " VALUES (:groupId, :authId)", | |||||
| authBatchInsertValues); | |||||
| } | |||||
| if (!authBatchDeleteValues.isEmpty()) { | |||||
| jdbcDao.batchUpdate( | |||||
| "DELETE FROM group_authority" | |||||
| + " WHERE groupId = :groupId AND authId = :authId", | |||||
| authBatchDeleteValues); | |||||
| } | |||||
| return instance; | |||||
| } | |||||
| public List<Integer> listGroupAuthId(int id) { | |||||
| return jdbcDao.queryForInts( | |||||
| "SELECT" | |||||
| + " ga.authId" | |||||
| + " FROM group_authority ga" | |||||
| + " WHERE ga.groupId = :id", | |||||
| Map.of(Params.ID, id)); | |||||
| } | |||||
| public List<Integer> listGroupUserId(int id) { | |||||
| return jdbcDao.queryForInts( | |||||
| "SELECT" | |||||
| + " gu.userId" | |||||
| + " FROM user_group gu" | |||||
| + " INNER JOIN user u ON u.deleted = FALSE AND gu.userId = u.id" | |||||
| + " WHERE gu.groupId = :id", | |||||
| Map.of(Params.ID, id)); | |||||
| } | |||||
| // here | |||||
| public boolean existsActiveUser(int id) { | |||||
| return jdbcDao.queryForBoolean(" select exists" | |||||
| + " (select *" | |||||
| + " from user_group ug" | |||||
| + " inner join `user` u on u.id = ug.userId" | |||||
| + " where ug.groupId = :id" | |||||
| + " and u.deleted = 0)", Map.of("id", id)); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,45 @@ | |||||
| package com.ffii.core.user.service; | |||||
| import java.time.LocalDateTime; | |||||
| import java.util.Map; | |||||
| import org.springframework.stereotype.Service; | |||||
| import org.springframework.transaction.annotation.Transactional; | |||||
| import com.ffii.core.support.AbstractService; | |||||
| import com.ffii.core.support.JdbcDao; | |||||
| /** @author Fung */ | |||||
| @Service | |||||
| public class UserAttemptService extends AbstractService { | |||||
| public UserAttemptService(JdbcDao jdbcDao) { | |||||
| super(jdbcDao); | |||||
| } | |||||
| public int findAttempt(String username) { | |||||
| return jdbcDao.queryForInt("SELECT attempt FROM user_attempt WHERE username = :username;", | |||||
| Map.of("username", username)); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void loginSuccess(String username) { | |||||
| jdbcDao.executeUpdate( | |||||
| "INSERT INTO user_attempt (username,attempt,lastAttempted) VALUES (:username,0,:lastAttempted)" | |||||
| + " ON DUPLICATE KEY UPDATE attempt = 0, lastAttempted = :lastAttempted;", | |||||
| Map.of("username", username, "lastAttempted", LocalDateTime.now())); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void loginFailure(String username) { | |||||
| jdbcDao.executeUpdate( | |||||
| "INSERT INTO user_attempt (username,attempt,lastAttempted) VALUES (:username,1,:lastAttempted)" | |||||
| + " ON DUPLICATE KEY UPDATE attempt = attempt + 1, lastAttempted = :lastAttempted;", | |||||
| Map.of("username", username, "lastAttempted", LocalDateTime.now())); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void reset(String username) { | |||||
| loginSuccess(username); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,391 @@ | |||||
| package com.ffii.core.user.service; | |||||
| import java.io.UnsupportedEncodingException; | |||||
| import java.util.LinkedHashSet; | |||||
| import java.util.List; | |||||
| import java.util.Locale; | |||||
| import java.util.Map; | |||||
| import java.util.Optional; | |||||
| import java.util.Set; | |||||
| import java.util.stream.Collectors; | |||||
| import java.util.stream.Stream; | |||||
| import javax.mail.internet.InternetAddress; | |||||
| import org.apache.commons.lang3.StringUtils; | |||||
| import org.apache.commons.text.StringEscapeUtils; | |||||
| import org.springframework.beans.BeanUtils; | |||||
| import org.springframework.beans.factory.annotation.Value; | |||||
| import org.springframework.context.MessageSource; | |||||
| import org.springframework.security.core.GrantedAuthority; | |||||
| import org.springframework.security.core.authority.SimpleGrantedAuthority; | |||||
| import org.springframework.security.core.userdetails.UserDetailsService; | |||||
| import org.springframework.security.core.userdetails.UsernameNotFoundException; | |||||
| import org.springframework.security.crypto.password.PasswordEncoder; | |||||
| import org.springframework.stereotype.Service; | |||||
| import org.springframework.transaction.annotation.Transactional; | |||||
| import com.ffii.core.common.ErrorCodes; | |||||
| import com.ffii.core.common.PasswordRule; | |||||
| import com.ffii.core.common.mail.pojo.MailRequest; | |||||
| import com.ffii.core.common.mail.service.MailService; | |||||
| import com.ffii.core.department.service.DepartmentService; | |||||
| import com.ffii.core.exception.NotFoundException; | |||||
| import com.ffii.core.exception.UnprocessableEntityException; | |||||
| import com.ffii.core.settings.service.SettingsService; | |||||
| import com.ffii.core.support.AbstractBaseEntityService; | |||||
| import com.ffii.core.support.JdbcDao; | |||||
| import com.ffii.core.user.dao.UserDao; | |||||
| import com.ffii.core.user.entity.User; | |||||
| import com.ffii.core.user.req.NewUserReq; | |||||
| import com.ffii.core.user.req.SearchUserReq; | |||||
| import com.ffii.core.user.req.UpdateUserReq; | |||||
| import com.ffii.core.user.service.pojo.UserRecord; | |||||
| import com.ffii.core.utils.LocaleUtils; | |||||
| import com.ffii.core.utils.Params; | |||||
| import com.ffii.core.utils.PasswordUtils; | |||||
| /** @author Fung */ | |||||
| @Service | |||||
| public class UserService extends AbstractBaseEntityService<User, Integer, UserDao> implements UserDetailsService { | |||||
| private static final String USER_AUTH_SQL = "SELECT a.authority FROM user_authority ua" | |||||
| + " INNER JOIN authority a ON a.id = ua.authId WHERE ua.userId = :userId"; | |||||
| private static final String UNION_SQL = " UNION "; | |||||
| private static final String GROUP_AUTH_SQL = "SELECT a.authority FROM user_group ug" | |||||
| + " INNER JOIN group_authority ga ON ga.groupId = ug.groupId" | |||||
| + " INNER JOIN authority a ON a.id = ga.authId WHERE ug.userId = :userId"; | |||||
| private final String LOAD_AUTH_SQL; | |||||
| private PasswordEncoder passwordEncoder; | |||||
| private MailService mailService; | |||||
| private SettingsService settingsService; | |||||
| private UserAttemptService userAttemptService; | |||||
| private MessageSource messageSource; | |||||
| private DepartmentService departmentService; | |||||
| public UserService( | |||||
| JdbcDao jdbcDao, | |||||
| UserDao dao, | |||||
| @Value("${app.auth.user:true}") boolean userAuth, | |||||
| @Value("${app.auth.group:false}") boolean groupAuth, | |||||
| PasswordEncoder passwordEncoder, | |||||
| MailService mailService, | |||||
| SettingsService settingsService, | |||||
| UserAttemptService userAttemptService, | |||||
| MessageSource messageSource, | |||||
| DepartmentService departmentService) { | |||||
| super(jdbcDao, dao); | |||||
| if (userAuth && groupAuth) { | |||||
| LOAD_AUTH_SQL = USER_AUTH_SQL + UNION_SQL + GROUP_AUTH_SQL; | |||||
| } else if (userAuth) { | |||||
| LOAD_AUTH_SQL = USER_AUTH_SQL; | |||||
| } else if (groupAuth) { | |||||
| LOAD_AUTH_SQL = GROUP_AUTH_SQL; | |||||
| } else { | |||||
| LOAD_AUTH_SQL = null; | |||||
| } | |||||
| this.passwordEncoder = passwordEncoder; | |||||
| this.mailService = mailService; | |||||
| this.settingsService = settingsService; | |||||
| this.userAttemptService = userAttemptService; | |||||
| this.messageSource = messageSource; | |||||
| this.departmentService = departmentService; | |||||
| } | |||||
| @Override | |||||
| public User loadUserByUsername(String username) throws UsernameNotFoundException { | |||||
| User user = findByUsername(username).orElseThrow(() -> new UsernameNotFoundException(username)); | |||||
| Set<GrantedAuthority> auths = new LinkedHashSet<GrantedAuthority>(); | |||||
| auths.add(new SimpleGrantedAuthority("ROLE_USER")); | |||||
| if (LOAD_AUTH_SQL != null) { | |||||
| jdbcDao.queryForList(LOAD_AUTH_SQL, Map.of("userId", user.getId())) | |||||
| .forEach(item -> auths.add(new SimpleGrantedAuthority((String) item.get("authority")))); | |||||
| } | |||||
| user.setAuthorities(auths); | |||||
| return user; | |||||
| } | |||||
| public Optional<User> findByUsername(String username) { | |||||
| return dao.findByUsernameAndDeletedFalse(username); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public boolean lock(int id, boolean locked) { | |||||
| Optional<User> opt = find(id); | |||||
| if (!opt.isPresent()) { | |||||
| return false; | |||||
| } | |||||
| User user = opt.get(); | |||||
| user.setLocked(locked); | |||||
| save(user); | |||||
| if (!locked) { | |||||
| userAttemptService.reset(user.getUsername()); | |||||
| } | |||||
| return true; | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public boolean setUserLocale(int id, Locale locale) { | |||||
| return jdbcDao.executeUpdate( | |||||
| "UPDATE user SET locale = :locale WHERE id = :id", | |||||
| Map.of(Params.ID, id, "locale", LocaleUtils.toLocaleStr(locale))) > 0; | |||||
| } | |||||
| public Stream<Integer> findUserIdByAuth(String auth) { | |||||
| return jdbcDao.queryForList( | |||||
| "SELECT ua.userId" | |||||
| + " FROM user_authority ua" | |||||
| + " INNER JOIN authority a ON a.id = ua.authId" | |||||
| + " WHERE a.authority = :auth", | |||||
| Map.of("auth", auth)) | |||||
| .stream() | |||||
| .map(r -> (Integer) r.get("userId")); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public User newRecord(NewUserReq req) throws UnsupportedEncodingException { | |||||
| if (findByUsername(req.getUsername()).isPresent()) { | |||||
| throw new UnprocessableEntityException(ErrorCodes.USERNAME_NOT_AVAILABLE); | |||||
| } | |||||
| String dept = departmentService.getDeptNameById(req.getDeptId()); | |||||
| String randomPassword = PasswordUtils.genPwd(new PasswordRule(settingsService)); | |||||
| String pwdHash = passwordEncoder.encode(randomPassword); | |||||
| User instance = new User(); | |||||
| instance.setPassword(pwdHash); | |||||
| instance = saveOrUpdate(instance, req); | |||||
| Locale locale = instance.getLocale() != null ? LocaleUtils.from(instance.getLocale()) : Locale.ENGLISH; | |||||
| mailService.send( | |||||
| MailRequest.builder() | |||||
| .subject("[MSMS] " + messageSource.getMessage("USER.newAc.subject", null, locale) + " - " + dept) | |||||
| .template("mail/newUser") | |||||
| .args(Map.of("department", dept, "username", instance.getUsername(), "password", | |||||
| StringEscapeUtils.escapeHtml4(randomPassword))) | |||||
| .addTo(new InternetAddress(instance.getEmail(), instance.getName())) | |||||
| .build(), | |||||
| locale); | |||||
| return instance; | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void updateRecord(int id, UpdateUserReq req) { | |||||
| saveOrUpdate( | |||||
| find(id).orElseThrow(NotFoundException::new), | |||||
| req); | |||||
| } | |||||
| private User saveOrUpdate(User instance, UpdateUserReq req) { | |||||
| BeanUtils.copyProperties(req, instance); | |||||
| instance = save(instance); | |||||
| int id = instance.getId(); | |||||
| List<Map<String, Integer>> groupBatchInsertValues = req.getAddGroupIds().stream() | |||||
| .map(groupId -> Map.of("userId", id, "groupId", groupId)) | |||||
| .collect(Collectors.toList()); | |||||
| List<Map<String, Integer>> groupBatchDeleteValues = req.getRemoveGroupIds().stream() | |||||
| .map(groupId -> Map.of("userId", id, "groupId", groupId)) | |||||
| .collect(Collectors.toList()); | |||||
| if (!groupBatchInsertValues.isEmpty()) { | |||||
| jdbcDao.batchUpdate( | |||||
| "INSERT IGNORE INTO user_group (groupId,userId)" | |||||
| + " VALUES (:groupId, :userId)", | |||||
| groupBatchInsertValues); | |||||
| } | |||||
| if (!groupBatchDeleteValues.isEmpty()) { | |||||
| jdbcDao.batchUpdate( | |||||
| "DELETE FROM user_group" | |||||
| + " WHERE groupId = :groupId AND userId = :userId", | |||||
| groupBatchDeleteValues); | |||||
| } | |||||
| List<Map<String, Integer>> authBatchInsertValues = req.getAddAuthIds().stream() | |||||
| .map(authId -> Map.of("userId", id, "authId", authId)) | |||||
| .collect(Collectors.toList()); | |||||
| List<Map<String, Integer>> authBatchDeleteValues = req.getRemoveAuthIds().stream() | |||||
| .map(authId -> Map.of("userId", id, "authId", authId)) | |||||
| .collect(Collectors.toList()); | |||||
| if (!authBatchInsertValues.isEmpty()) { | |||||
| jdbcDao.batchUpdate( | |||||
| "INSERT IGNORE INTO user_authority (userId, authId)" | |||||
| + " VALUES (:userId, :authId)", | |||||
| authBatchInsertValues); | |||||
| } | |||||
| if (!authBatchDeleteValues.isEmpty()) { | |||||
| jdbcDao.batchUpdate( | |||||
| "DELETE FROM user_authority" | |||||
| + " WHERE userId = :userId AND authId = :authId", | |||||
| authBatchDeleteValues); | |||||
| } | |||||
| return instance; | |||||
| } | |||||
| public Optional<User> findById(int id) { | |||||
| return dao.findByIdAndDeletedFalse(id); | |||||
| } | |||||
| public List<Map<String, Object>> searchForCombo(Map<String, Object> args) { | |||||
| StringBuilder sql = new StringBuilder("SELECT" | |||||
| + " u.id," | |||||
| + " u.companyId," | |||||
| + " u.department," | |||||
| + " u.username," | |||||
| + " u.name" | |||||
| + " FROM user u" | |||||
| + " WHERE u.deleted = FALSE"); | |||||
| if (args != null) { | |||||
| if (args.containsKey(Params.QUERY)) | |||||
| sql.append(" AND (u.username LIKE :query OR u.name LIKE :query)"); | |||||
| if (args.containsKey(Params.ID)) | |||||
| sql.append(" AND u.id = :id"); | |||||
| if (args.containsKey("companyIds")) | |||||
| sql.append(" AND u.companyId IN (:companyIds)"); | |||||
| if (args.containsKey("authority")) | |||||
| sql.append( | |||||
| " AND (EXISTS(SELECT 1 FROM users_authorities WHERE userId = id AND authority = :authority) OR EXISTS(SELECT 1 FROM user_group gu INNER JOIN groups_authorities ga ON ga.groupId = gu.groupId AND ga.authority = :authority WHERE gu.userId = id))"); | |||||
| } | |||||
| sql.append(" ORDER BY u.name"); | |||||
| return jdbcDao.queryForList(sql.toString(), args); | |||||
| } | |||||
| public List<UserRecord> search(SearchUserReq req) { | |||||
| StringBuilder sql = new StringBuilder("SELECT" | |||||
| + " u.id," | |||||
| + " u.created," | |||||
| + " u.createdBy," | |||||
| + " u.version," | |||||
| + " u.modified," | |||||
| + " u.modifiedBy," | |||||
| + " u.username," | |||||
| + " u.locked," | |||||
| + " u.name," | |||||
| + " u.companyId," | |||||
| + " u.customerId," | |||||
| + " u.locale," | |||||
| + " u.firstname," | |||||
| + " u.lastname," | |||||
| + " u.title," | |||||
| // + " u.department," | |||||
| + "d.name as department," | |||||
| + " u.deptId," | |||||
| + " u.email," | |||||
| + " u.phone1," | |||||
| + " u.phone2," | |||||
| + " u.remarks " | |||||
| + " FROM `user` u"); | |||||
| if (req != null) { | |||||
| if (req.getGroupId() != null) | |||||
| sql.append(" left join user_group ug on u.id = ug.userId"); | |||||
| sql.append(" left join department d on u.deptId = d.id"); | |||||
| } | |||||
| sql.append(" where u.deleted = false"); | |||||
| if (req != null) { | |||||
| if (req.getId() != null) | |||||
| sql.append(" AND u.id = :id"); | |||||
| if (req.getGroupId() != null) | |||||
| sql.append(" AND ug.groupId = :groupId"); | |||||
| if (StringUtils.isNotBlank(req.getUsername())) { | |||||
| req.setUsername("%" + req.getUsername() + "%"); | |||||
| sql.append(" AND u.username LIKE :username"); | |||||
| } | |||||
| if (StringUtils.isNotBlank(req.getName())) { | |||||
| req.setName("%" + req.getName() + "%"); | |||||
| sql.append(" AND u.name LIKE :name"); | |||||
| } | |||||
| if (req.getLocked() != null) { | |||||
| sql.append(" AND u.locked = :locked"); | |||||
| } | |||||
| } | |||||
| sql.append(" ORDER BY u.name"); | |||||
| if (req != null) { | |||||
| if (req.getStart() != null && req.getLimit() != null) | |||||
| sql.append(" LIMIT :start, :limit"); | |||||
| } | |||||
| return jdbcDao.queryForList(sql.toString(), req, UserRecord.class); | |||||
| } | |||||
| public int searchTotalCount(SearchUserReq req) { | |||||
| StringBuilder sql = new StringBuilder("SELECT" | |||||
| + " COUNT(*) AS count" | |||||
| + " FROM user u"); | |||||
| if (req != null) { | |||||
| if (req.getGroupId() != null) | |||||
| sql.append(" LEFT JOIN user_group gu on u.id = gu.userId"); | |||||
| } | |||||
| sql.append(" WHERE u.deleted = 0"); | |||||
| if (req != null) { | |||||
| if (req.getId() != null) | |||||
| sql.append(" AND u.id = :id"); | |||||
| if (req.getGroupId() != null) | |||||
| sql.append(" AND gu.groupId = :groupId"); | |||||
| if (StringUtils.isNotBlank(req.getUsername())) | |||||
| sql.append(" AND u.username LIKE :username"); | |||||
| if (StringUtils.isNotBlank(req.getName())) | |||||
| sql.append(" AND u.name LIKE :name"); | |||||
| if (req.getLocked() != null) | |||||
| sql.append(" AND u.locked = :locked"); | |||||
| } | |||||
| return jdbcDao.queryForInt(sql.toString(), req); | |||||
| } | |||||
| public List<Integer> listUserAuthId(int id) { | |||||
| return jdbcDao.queryForInts( | |||||
| "SELECT" | |||||
| + " ua.authId" | |||||
| + " FROM user_authority ua" | |||||
| + " WHERE ua.userId = :id", | |||||
| Map.of(Params.ID, id)); | |||||
| } | |||||
| public List<Integer> listUserGroupId(int id) { | |||||
| return jdbcDao.queryForInts( | |||||
| "SELECT" | |||||
| + " gu.groupId" | |||||
| + " FROM user_group gu" | |||||
| + " INNER JOIN `group` g ON g.deleted = FALSE AND g.id = gu.groupId" | |||||
| + " WHERE gu.userId = :id", | |||||
| Map.of(Params.ID, id)); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void resetPassword(int id) throws UnsupportedEncodingException { | |||||
| User instance = find(id).orElseThrow(NotFoundException::new); | |||||
| String randomPassword = PasswordUtils.genPwd(new PasswordRule(settingsService)); | |||||
| String dept = departmentService.getDeptNameById(instance.getDeptId()); | |||||
| instance.setPassword(passwordEncoder.encode(randomPassword)); | |||||
| instance = save(instance); | |||||
| Locale locale = instance.getLocale() != null ? LocaleUtils.from(instance.getLocale()) : Locale.ENGLISH; | |||||
| mailService.send( | |||||
| MailRequest.builder() | |||||
| .subject("[MSMS] " + messageSource.getMessage("USER.resetPwd.subject", null, locale) + " - " + dept) | |||||
| .template("mail/resetPwd") | |||||
| .args(Map.of("department", dept, "username", instance.getUsername(), "password", | |||||
| StringEscapeUtils.escapeHtml4(randomPassword))) | |||||
| .addTo(new InternetAddress(instance.getEmail(), instance.getName())) | |||||
| .build(), | |||||
| locale); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,46 @@ | |||||
| package com.ffii.core.user.service; | |||||
| import java.io.IOException; | |||||
| import java.util.Map; | |||||
| import java.util.Optional; | |||||
| import org.springframework.stereotype.Service; | |||||
| import org.springframework.transaction.annotation.Transactional; | |||||
| import org.springframework.web.multipart.MultipartFile; | |||||
| import com.ffii.core.common.file.FileRefType; | |||||
| import com.ffii.core.common.file.service.FileService; | |||||
| import com.ffii.core.support.AbstractService; | |||||
| import com.ffii.core.support.JdbcDao; | |||||
| @Service | |||||
| public class UserSignService extends AbstractService { | |||||
| private FileService fileService; | |||||
| public UserSignService(JdbcDao jdbcDao,FileService fileService) { | |||||
| super(jdbcDao); | |||||
| this.fileService = fileService; | |||||
| } | |||||
| public Optional<Map<String, Object>> findUserSignatureMap(int userId) { | |||||
| return jdbcDao.queryForMap( | |||||
| "SELECT" + | |||||
| " fr.refId," + | |||||
| " fr.created," + | |||||
| " fb.bytes" + | |||||
| " FROM file_ref fr" + | |||||
| " LEFT JOIN files_blob fb ON fr.fileId = fb.fileId" + | |||||
| " WHERE fr.refId = :userId" + | |||||
| " AND fr.refType = :refType" + | |||||
| " order by fr.fileId desc" + | |||||
| " limit 1", | |||||
| Map.of("userId", userId, "refType", FileRefType.USER_SIGN)); | |||||
| } | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public void saveSign(int userId, MultipartFile userSign) throws IOException { | |||||
| String filename = String.format("%d_%s", System.currentTimeMillis(), FileRefType.USER_SIGN); | |||||
| fileService.upload(userId, FileRefType.USER_SIGN,null, userSign,filename + ".png",null); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,50 @@ | |||||
| package com.ffii.core.user.service.pojo; | |||||
| public class AuthRecord { | |||||
| private Integer id; | |||||
| private String module; | |||||
| private String authority; | |||||
| private String name; | |||||
| private String description; | |||||
| public Integer getId() { | |||||
| return id; | |||||
| } | |||||
| public void setId(Integer id) { | |||||
| this.id = id; | |||||
| } | |||||
| public String getModule() { | |||||
| return module; | |||||
| } | |||||
| public void setModule(String module) { | |||||
| this.module = module; | |||||
| } | |||||
| public String getDescription() { | |||||
| return description; | |||||
| } | |||||
| public void setDescription(String description) { | |||||
| this.description = description; | |||||
| } | |||||
| public String getAuthority() { | |||||
| return authority; | |||||
| } | |||||
| public void setAuthority(String authority) { | |||||
| this.authority = authority; | |||||
| } | |||||
| public String getName() { | |||||
| return name; | |||||
| } | |||||
| public void setName(String name) { | |||||
| this.name = name; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,196 @@ | |||||
| package com.ffii.core.user.service.pojo; | |||||
| import java.time.LocalDateTime; | |||||
| public class UserRecord { | |||||
| private Integer id; | |||||
| private LocalDateTime created; | |||||
| private String createdBy; | |||||
| private LocalDateTime modified; | |||||
| private String modifiedBy; | |||||
| private String username; | |||||
| private Boolean locked; | |||||
| private String name; | |||||
| private Integer companyId; | |||||
| private Integer customerId; | |||||
| private String locale; | |||||
| private String fullname; | |||||
| private String firstname; | |||||
| private String lastname; | |||||
| private String title; | |||||
| private String department; | |||||
| private String deptId; | |||||
| private String email; | |||||
| private String phone1; | |||||
| private String phone2; | |||||
| private String remarks; | |||||
| public Integer getId() { | |||||
| return id; | |||||
| } | |||||
| public void setId(Integer id) { | |||||
| this.id = id; | |||||
| } | |||||
| public LocalDateTime getCreated() { | |||||
| return created; | |||||
| } | |||||
| public void setCreated(LocalDateTime created) { | |||||
| this.created = created; | |||||
| } | |||||
| public String getCreatedBy() { | |||||
| return createdBy; | |||||
| } | |||||
| public void setCreatedBy(String createdBy) { | |||||
| this.createdBy = createdBy; | |||||
| } | |||||
| public LocalDateTime getModified() { | |||||
| return modified; | |||||
| } | |||||
| public void setModified(LocalDateTime modified) { | |||||
| this.modified = modified; | |||||
| } | |||||
| public String getModifiedBy() { | |||||
| return modifiedBy; | |||||
| } | |||||
| public void setModifiedBy(String modifiedBy) { | |||||
| this.modifiedBy = modifiedBy; | |||||
| } | |||||
| public String getUsername() { | |||||
| return username; | |||||
| } | |||||
| public void setUsername(String username) { | |||||
| this.username = username; | |||||
| } | |||||
| public Boolean getLocked() { | |||||
| return locked; | |||||
| } | |||||
| public void setLocked(Boolean locked) { | |||||
| this.locked = locked; | |||||
| } | |||||
| public String getName() { | |||||
| return name; | |||||
| } | |||||
| public void setName(String name) { | |||||
| this.name = name; | |||||
| } | |||||
| public Integer getCompanyId() { | |||||
| return companyId; | |||||
| } | |||||
| public void setCompanyId(Integer companyId) { | |||||
| this.companyId = companyId; | |||||
| } | |||||
| public Integer getCustomerId() { | |||||
| return customerId; | |||||
| } | |||||
| public void setCustomerId(Integer customerId) { | |||||
| this.customerId = customerId; | |||||
| } | |||||
| public String getLocale() { | |||||
| return locale; | |||||
| } | |||||
| public void setLocale(String locale) { | |||||
| this.locale = locale; | |||||
| } | |||||
| public String getFullname() { | |||||
| return fullname; | |||||
| } | |||||
| public void setFullname(String fullname) { | |||||
| this.fullname = fullname; | |||||
| } | |||||
| 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 getTitle() { | |||||
| return title; | |||||
| } | |||||
| public void setTitle(String title) { | |||||
| this.title = title; | |||||
| } | |||||
| public String getDepartment() { | |||||
| return department; | |||||
| } | |||||
| public void setDepartment(String department) { | |||||
| this.department = department; | |||||
| } | |||||
| public String getDeptId() { | |||||
| return deptId; | |||||
| } | |||||
| public void setDeptId(String deptId) { | |||||
| this.deptId = deptId; | |||||
| } | |||||
| public String getEmail() { | |||||
| return email; | |||||
| } | |||||
| public void setEmail(String email) { | |||||
| this.email = email; | |||||
| } | |||||
| public String getPhone1() { | |||||
| return phone1; | |||||
| } | |||||
| public void setPhone1(String phone1) { | |||||
| this.phone1 = phone1; | |||||
| } | |||||
| public String getPhone2() { | |||||
| return phone2; | |||||
| } | |||||
| public void setPhone2(String phone2) { | |||||
| this.phone2 = phone2; | |||||
| } | |||||
| public String getRemarks() { | |||||
| return remarks; | |||||
| } | |||||
| public void setRemarks(String remarks) { | |||||
| this.remarks = remarks; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,30 @@ | |||||
| package com.ffii.core.user.web; | |||||
| import org.springframework.web.bind.annotation.GetMapping; | |||||
| import org.springframework.web.bind.annotation.RequestMapping; | |||||
| import org.springframework.web.bind.annotation.RestController; | |||||
| import com.ffii.core.response.RecordsRes; | |||||
| import com.ffii.core.support.AbstractController; | |||||
| import com.ffii.core.user.service.AuthService; | |||||
| import com.ffii.core.user.service.pojo.AuthRecord; | |||||
| import io.swagger.v3.oas.annotations.Operation; | |||||
| @RestController | |||||
| @RequestMapping("/protected/auth") | |||||
| public class AuthController extends AbstractController { | |||||
| private AuthService authService; | |||||
| public AuthController(AuthService authService) { | |||||
| this.authService = authService; | |||||
| } | |||||
| @Operation(summary="list system authorities") | |||||
| @GetMapping | |||||
| public RecordsRes<AuthRecord> list() { | |||||
| return new RecordsRes<>(authService.list()); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,88 @@ | |||||
| package com.ffii.core.user.web; | |||||
| import java.io.UnsupportedEncodingException; | |||||
| import java.util.Locale; | |||||
| import java.util.Map; | |||||
| import java.util.Optional; | |||||
| import javax.mail.internet.InternetAddress; | |||||
| import org.apache.commons.text.StringEscapeUtils; | |||||
| import org.springframework.context.MessageSource; | |||||
| import org.springframework.context.NoSuchMessageException; | |||||
| import org.springframework.security.crypto.password.PasswordEncoder; | |||||
| import org.springframework.web.bind.annotation.PostMapping; | |||||
| import org.springframework.web.bind.annotation.RequestMapping; | |||||
| import org.springframework.web.bind.annotation.RestController; | |||||
| import com.ffii.core.common.ErrorCodes; | |||||
| import com.ffii.core.common.PasswordRule; | |||||
| import com.ffii.core.common.mail.pojo.MailRequest; | |||||
| import com.ffii.core.common.mail.service.MailService; | |||||
| import com.ffii.core.department.service.DepartmentService; | |||||
| import com.ffii.core.exception.UnprocessableEntityException; | |||||
| import com.ffii.core.settings.service.SettingsService; | |||||
| import com.ffii.core.support.AbstractController; | |||||
| import com.ffii.core.user.entity.User; | |||||
| import com.ffii.core.user.req.ForgetPwReq; | |||||
| import com.ffii.core.user.service.UserService; | |||||
| import com.ffii.core.utils.LocaleUtils; | |||||
| import com.ffii.core.utils.PasswordUtils; | |||||
| @RestController | |||||
| @RequestMapping("/public/forget-pw") | |||||
| public class ForgetPwController extends AbstractController { | |||||
| private UserService userService; | |||||
| private DepartmentService departmentService; | |||||
| private SettingsService settingsService; | |||||
| private PasswordEncoder passwordEncoder; | |||||
| private MailService mailService; | |||||
| private MessageSource messageSource; | |||||
| public ForgetPwController(UserService userService, DepartmentService departmentService, | |||||
| SettingsService settingsService, PasswordEncoder passwordEncoder, MailService mailService, | |||||
| MessageSource messageSource) { | |||||
| this.userService = userService; | |||||
| this.departmentService = departmentService; | |||||
| this.settingsService = settingsService; | |||||
| this.passwordEncoder = passwordEncoder; | |||||
| this.mailService = mailService; | |||||
| this.messageSource = messageSource; | |||||
| } | |||||
| @PostMapping | |||||
| public boolean forgetPassword(ForgetPwReq req) throws NoSuchMessageException, UnsupportedEncodingException { | |||||
| Optional<User> userOpt = userService.findByUsername(req.getUsername()); | |||||
| if (userOpt.isEmpty()) { | |||||
| throw new UnprocessableEntityException(ErrorCodes.USER_NOT_EXIST); | |||||
| } | |||||
| User instance = userOpt.get(); | |||||
| String dept = departmentService.getDeptNameById(instance.getDeptId()); | |||||
| String randomPassword = PasswordUtils.genPwd(new PasswordRule(settingsService)); | |||||
| String pwdHash = passwordEncoder.encode(randomPassword); | |||||
| instance.setPassword(pwdHash); | |||||
| instance = userService.save(instance); | |||||
| Locale locale = instance.getLocale() != null ? LocaleUtils.from(instance.getLocale()) : Locale.ENGLISH; | |||||
| try { | |||||
| mailService.send( | |||||
| MailRequest.builder() | |||||
| .subject("[MSMS] " + messageSource.getMessage("USER.resetPw.subject", null, locale) + " - " + dept) | |||||
| .template("mail/forgetPw") | |||||
| .args(Map.of("department", dept, "username", instance.getUsername(), "password", | |||||
| StringEscapeUtils.escapeHtml4(randomPassword))) | |||||
| .addTo(new InternetAddress(instance.getEmail(), instance.getName())) | |||||
| .build(), | |||||
| locale); | |||||
| return true; | |||||
| } catch (Exception ex) { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,85 @@ | |||||
| package com.ffii.core.user.web; | |||||
| import java.util.Map; | |||||
| import javax.servlet.http.HttpServletRequest; | |||||
| import javax.validation.Valid; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.security.access.prepost.PreAuthorize; | |||||
| import org.springframework.web.bind.ServletRequestBindingException; | |||||
| import org.springframework.web.bind.annotation.DeleteMapping; | |||||
| import org.springframework.web.bind.annotation.GetMapping; | |||||
| import org.springframework.web.bind.annotation.PathVariable; | |||||
| import org.springframework.web.bind.annotation.PostMapping; | |||||
| import org.springframework.web.bind.annotation.RequestBody; | |||||
| import org.springframework.web.bind.annotation.RequestMapping; | |||||
| import org.springframework.web.bind.annotation.ResponseStatus; | |||||
| import org.springframework.web.bind.annotation.RestController; | |||||
| import com.ffii.core.common.ErrorCodes; | |||||
| import com.ffii.core.exception.NotFoundException; | |||||
| import com.ffii.core.exception.UnprocessableEntityException; | |||||
| import com.ffii.core.response.IdRes; | |||||
| import com.ffii.core.response.RecordsRes; | |||||
| import com.ffii.core.support.AbstractController; | |||||
| import com.ffii.core.user.req.SaveGroupReq; | |||||
| import com.ffii.core.user.service.GroupService; | |||||
| import com.ffii.core.utils.CriteriaArgsBuilder; | |||||
| import com.ffii.core.utils.Params; | |||||
| @RestController | |||||
| @RequestMapping(value = "/protected/group") | |||||
| public class GroupController extends AbstractController { | |||||
| private GroupService groupService; | |||||
| public GroupController(GroupService groupService) { | |||||
| this.groupService = groupService; | |||||
| } | |||||
| @PostMapping("/save") | |||||
| @PreAuthorize("hasAuthority('EDIT_USER_GROUP')") | |||||
| public IdRes saveOrUpdate(@RequestBody @Valid SaveGroupReq req) { | |||||
| return new IdRes(groupService.saveOrUpdate(req).getId()); | |||||
| } | |||||
| @DeleteMapping("/{id}") | |||||
| @PreAuthorize("hasAuthority('EDIT_USER_GROUP')") | |||||
| @ResponseStatus(HttpStatus.NO_CONTENT) | |||||
| public void delete(@PathVariable int id) { | |||||
| if (!groupService.existsActiveUser(id)) { | |||||
| groupService.delete(groupService.find(id).orElseThrow(NotFoundException::new)); | |||||
| } else { | |||||
| throw new UnprocessableEntityException(ErrorCodes.DELETE_USER_GROUP_ERROR); | |||||
| } | |||||
| } | |||||
| @GetMapping("/{id}") | |||||
| public Map<String, Object> load(@PathVariable int id) { | |||||
| return Map.of( | |||||
| Params.DATA, groupService.find(id).orElseThrow(NotFoundException::new), | |||||
| "authIds", groupService.listGroupAuthId(id), | |||||
| "userIds", groupService.listGroupUserId(id)); | |||||
| } | |||||
| @GetMapping("/combo") | |||||
| public RecordsRes<Map<String, Object>> comboJson(HttpServletRequest request) throws ServletRequestBindingException { | |||||
| return new RecordsRes<>(groupService.searchForCombo( | |||||
| CriteriaArgsBuilder.withRequest(request) | |||||
| .addInteger(Params.ID) | |||||
| .addStringLike(Params.QUERY) | |||||
| .build())); | |||||
| } | |||||
| @GetMapping | |||||
| public RecordsRes<Map<String, Object>> listJson(HttpServletRequest request) throws ServletRequestBindingException { | |||||
| return new RecordsRes<>(groupService.search( | |||||
| CriteriaArgsBuilder.withRequest(request) | |||||
| .addInteger(Params.ID) | |||||
| .addStringLike(Params.NAME) | |||||
| .addInteger("userId") | |||||
| .build())); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,241 @@ | |||||
| package com.ffii.core.user.web; | |||||
| import java.io.UnsupportedEncodingException; | |||||
| import java.util.Map; | |||||
| import javax.servlet.http.HttpServletRequest; | |||||
| import javax.validation.Valid; | |||||
| import javax.validation.constraints.NotBlank; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.security.access.prepost.PreAuthorize; | |||||
| import org.springframework.security.crypto.password.PasswordEncoder; | |||||
| import org.springframework.web.bind.ServletRequestBindingException; | |||||
| import org.springframework.web.bind.annotation.DeleteMapping; | |||||
| import org.springframework.web.bind.annotation.GetMapping; | |||||
| import org.springframework.web.bind.annotation.ModelAttribute; | |||||
| import org.springframework.web.bind.annotation.PatchMapping; | |||||
| import org.springframework.web.bind.annotation.PathVariable; | |||||
| import org.springframework.web.bind.annotation.PostMapping; | |||||
| import org.springframework.web.bind.annotation.PutMapping; | |||||
| import org.springframework.web.bind.annotation.RequestBody; | |||||
| import org.springframework.web.bind.annotation.RequestMapping; | |||||
| import org.springframework.web.bind.annotation.ResponseStatus; | |||||
| import org.springframework.web.bind.annotation.RestController; | |||||
| import com.ffii.core.common.ErrorCodes; | |||||
| import com.ffii.core.common.PasswordRule; | |||||
| import com.ffii.core.common.SecurityUtils; | |||||
| import com.ffii.core.exception.BadRequestException; | |||||
| import com.ffii.core.exception.NotFoundException; | |||||
| import com.ffii.core.exception.UnprocessableEntityException; | |||||
| import com.ffii.core.response.FailureRes; | |||||
| import com.ffii.core.response.IdRes; | |||||
| import com.ffii.core.response.RecordsRes; | |||||
| import com.ffii.core.settings.service.SettingsService; | |||||
| import com.ffii.core.support.AbstractController; | |||||
| import com.ffii.core.user.entity.User; | |||||
| import com.ffii.core.user.req.NewUserReq; | |||||
| import com.ffii.core.user.req.SearchUserReq; | |||||
| import com.ffii.core.user.req.UpdateUserReq; | |||||
| import com.ffii.core.user.res.LoadUserRes; | |||||
| import com.ffii.core.user.service.UserService; | |||||
| import com.ffii.core.user.service.pojo.UserRecord; | |||||
| import com.ffii.core.utils.CriteriaArgsBuilder; | |||||
| import com.ffii.core.utils.Params; | |||||
| import com.ffii.core.utils.PasswordUtils; | |||||
| import io.swagger.v3.oas.annotations.Operation; | |||||
| import io.swagger.v3.oas.annotations.media.Content; | |||||
| import io.swagger.v3.oas.annotations.media.Schema; | |||||
| import io.swagger.v3.oas.annotations.responses.ApiResponse; | |||||
| /** @author Fung */ | |||||
| @RestController | |||||
| @RequestMapping("/protected/user") | |||||
| public class UserController extends AbstractController { | |||||
| private UserService userService; | |||||
| private PasswordEncoder passwordEncoder; | |||||
| private SettingsService settingsService; | |||||
| public UserController( | |||||
| UserService userService, | |||||
| PasswordEncoder passwordEncoder, | |||||
| SettingsService settingsService) { | |||||
| this.userService = userService; | |||||
| this.passwordEncoder = passwordEncoder; | |||||
| this.settingsService = settingsService; | |||||
| } | |||||
| @Operation(summary = "list user", responses = { @ApiResponse(responseCode = "200"), | |||||
| @ApiResponse(responseCode = "404", content = @Content) }) | |||||
| @GetMapping | |||||
| @PreAuthorize("hasAuthority('VIEW_USER')") | |||||
| public RecordsRes<UserRecord> list(@ModelAttribute @Valid SearchUserReq req) { | |||||
| return new RecordsRes<>(userService.search(req), userService.searchTotalCount(req)); | |||||
| } | |||||
| @Operation(summary = "load user data", responses = { @ApiResponse(responseCode = "200"), | |||||
| @ApiResponse(responseCode = "404", content = @Content) }) | |||||
| @GetMapping("/{id}") | |||||
| @PreAuthorize("hasAuthority('VIEW_USER')") | |||||
| public LoadUserRes load(@PathVariable int id) { | |||||
| return new LoadUserRes( | |||||
| userService.find(id).orElseThrow(NotFoundException::new), | |||||
| userService.listUserAuthId(id), | |||||
| userService.listUserGroupId(id)); | |||||
| } | |||||
| @Operation(summary = "delete user", responses = { @ApiResponse(responseCode = "204"), | |||||
| @ApiResponse(responseCode = "404", content = @Content) }) | |||||
| @DeleteMapping("/{id}") | |||||
| @ResponseStatus(HttpStatus.NO_CONTENT) | |||||
| @PreAuthorize("hasAuthority('EDIT_USER')") | |||||
| public void delete(@PathVariable int id) { | |||||
| userService.markDelete(userService.find(id).orElseThrow(NotFoundException::new)); | |||||
| } | |||||
| @Operation(summary = "lock user", responses = { | |||||
| @ApiResponse(responseCode = "204"), | |||||
| @ApiResponse(responseCode = "404", content = @Content) }) | |||||
| @PatchMapping("/{id}/lock") | |||||
| @ResponseStatus(HttpStatus.NO_CONTENT) | |||||
| @PreAuthorize("hasAuthority('EDIT_USER')") | |||||
| public void lock(@PathVariable int id) { | |||||
| if (!userService.lock(id, true)) { | |||||
| throw new NotFoundException(); | |||||
| } | |||||
| } | |||||
| @Operation(summary = "unlock user", responses = { | |||||
| @ApiResponse(responseCode = "204"), | |||||
| @ApiResponse(responseCode = "404", content = @Content) }) | |||||
| @PatchMapping("/{id}/unlock") | |||||
| @ResponseStatus(HttpStatus.NO_CONTENT) | |||||
| @PreAuthorize("hasAuthority('EDIT_USER')") | |||||
| public void unlock(@PathVariable int id) { | |||||
| if (!userService.lock(id, false)) { | |||||
| throw new NotFoundException(); | |||||
| } | |||||
| } | |||||
| @Operation(summary = "new user") | |||||
| @PostMapping | |||||
| @ResponseStatus(HttpStatus.CREATED) | |||||
| @PreAuthorize("hasAuthority('EDIT_USER')") | |||||
| public IdRes newRecord(@RequestBody @Valid NewUserReq req) throws UnsupportedEncodingException { | |||||
| return new IdRes(userService.newRecord(req).getId()); | |||||
| } | |||||
| @Operation(summary = "update user", responses = { | |||||
| @ApiResponse(responseCode = "204"), | |||||
| @ApiResponse(responseCode = "400", content = @Content), | |||||
| @ApiResponse(responseCode = "404", content = @Content), | |||||
| }) | |||||
| @PutMapping("/{id}") | |||||
| @ResponseStatus(HttpStatus.NO_CONTENT) | |||||
| @PreAuthorize("hasAuthority('EDIT_USER')") | |||||
| public void updateRecord(@PathVariable int id, @RequestBody @Valid UpdateUserReq req) { | |||||
| userService.updateRecord(id, req); | |||||
| } | |||||
| @Operation(summary = "login as some user") | |||||
| @PutMapping("/login-as/{username}") | |||||
| @PreAuthorize("hasAuthority('SUPERUSER')") | |||||
| @ResponseStatus(HttpStatus.NO_CONTENT) | |||||
| public void loginAs(@PathVariable String username) { | |||||
| SecurityUtils.loginUser(userService, username); | |||||
| } | |||||
| @Operation(summary = "current user change password", description = "error: USER_WRONG_NEW_PWD = new password not available", responses = { | |||||
| @ApiResponse(responseCode = "204"), | |||||
| @ApiResponse(responseCode = "400", content = @Content), | |||||
| @ApiResponse(responseCode = "404", content = @Content), | |||||
| @ApiResponse(responseCode = "422", content = @Content(schema = @Schema(implementation = FailureRes.class))), | |||||
| }) | |||||
| @PatchMapping("/change-password") | |||||
| @ResponseStatus(HttpStatus.NO_CONTENT) | |||||
| public void changePassword(@RequestBody @Valid ChangePwdReq req) { | |||||
| int id = SecurityUtils.getUser().get().getId(); | |||||
| User instance = userService.find(id).orElseThrow(NotFoundException::new); | |||||
| if (!passwordEncoder.matches(req.getPassword(), instance.getPassword())) { | |||||
| throw new BadRequestException(); | |||||
| } | |||||
| PasswordRule rule = new PasswordRule(settingsService); | |||||
| if (!PasswordUtils.checkPwd(req.getNewPassword(), rule)) { | |||||
| throw new UnprocessableEntityException(ErrorCodes.USER_WRONG_NEW_PWD); | |||||
| } | |||||
| instance.setPassword(passwordEncoder.encode(req.getNewPassword())); | |||||
| userService.save(instance); | |||||
| } | |||||
| @Operation(summary = "reset password", responses = { | |||||
| @ApiResponse(responseCode = "204"), | |||||
| @ApiResponse(responseCode = "404", content = @Content), | |||||
| }) | |||||
| @PostMapping("/{id}/reset-password") | |||||
| @PreAuthorize("hasAuthority('EDIT_USER')") | |||||
| @ResponseStatus(HttpStatus.NO_CONTENT) | |||||
| public void resetPassword(@PathVariable int id) throws UnsupportedEncodingException { | |||||
| userService.resetPassword(id); | |||||
| } | |||||
| @Operation(summary = "list for combo") | |||||
| @GetMapping("/combo") | |||||
| // @PreAuthorize("hasAuthority('VIEW_USER')") | |||||
| public RecordsRes<Map<String, Object>> comboJson(HttpServletRequest request) throws ServletRequestBindingException { | |||||
| Map<String, Object> args = CriteriaArgsBuilder.withRequest(request) | |||||
| .addInteger(Params.ID) | |||||
| .addStringLike(Params.QUERY) | |||||
| .addBoolean("send") | |||||
| .build(); | |||||
| User u = SecurityUtils.getUser().get(); | |||||
| if (args.containsKey("send") && (Boolean) args.containsKey("send")) { | |||||
| args.remove("send"); | |||||
| args.put("senderId", u.getId()); | |||||
| } | |||||
| // if current user is not from host compnay, then restrict access to users in | |||||
| // (self company + host company) | |||||
| // if (u.getCompanyId() > 1) { | |||||
| // args.put("companyIds", Arrays.asList(u.getCompanyId())); | |||||
| // } | |||||
| return new RecordsRes<>(userService.searchForCombo(args)); | |||||
| } | |||||
| @Operation(summary = "get password rules") | |||||
| @GetMapping("/password-rule") | |||||
| public PasswordRule passwordRule() { | |||||
| return new PasswordRule(settingsService); | |||||
| } | |||||
| public static class ChangePwdReq { | |||||
| @NotBlank | |||||
| private String password; | |||||
| @NotBlank | |||||
| private String newPassword; | |||||
| public String getPassword() { | |||||
| return password; | |||||
| } | |||||
| public void setPassword(String password) { | |||||
| this.password = password; | |||||
| } | |||||
| public String getNewPassword() { | |||||
| return newPassword; | |||||
| } | |||||
| public void setNewPassword(String newPassword) { | |||||
| this.newPassword = newPassword; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,51 @@ | |||||
| package com.ffii.core.user.web; | |||||
| import java.io.IOException; | |||||
| import java.util.Map; | |||||
| import javax.servlet.http.HttpServletRequest; | |||||
| import org.springframework.http.HttpStatus; | |||||
| import org.springframework.web.bind.annotation.GetMapping; | |||||
| import org.springframework.web.bind.annotation.PathVariable; | |||||
| import org.springframework.web.bind.annotation.PostMapping; | |||||
| import org.springframework.web.bind.annotation.RequestMapping; | |||||
| import org.springframework.web.bind.annotation.RequestParam; | |||||
| import org.springframework.web.bind.annotation.ResponseStatus; | |||||
| import org.springframework.web.bind.annotation.RestController; | |||||
| import org.springframework.web.multipart.MultipartFile; | |||||
| import com.ffii.core.exception.NotFoundException; | |||||
| import com.ffii.core.response.DataRes; | |||||
| import com.ffii.core.support.AbstractController; | |||||
| import com.ffii.core.user.service.UserService; | |||||
| import com.ffii.core.user.service.UserSignService; | |||||
| @RestController | |||||
| @RequestMapping("/protected/user/{userId}/sign") | |||||
| public class UserSignController extends AbstractController { | |||||
| private UserService userService; | |||||
| private UserSignService userSignService; | |||||
| public UserSignController(UserService userService, UserSignService userSignService) { | |||||
| this.userService = userService; | |||||
| this.userSignService = userSignService; | |||||
| } | |||||
| @PostMapping | |||||
| // @PreAuthorize("hasAuthority('USER_MAINTAIN')") | |||||
| @ResponseStatus(HttpStatus.CREATED) | |||||
| public void saveSign(HttpServletRequest request, @PathVariable int userId, @RequestParam MultipartFile userSign) throws IOException { | |||||
| if (userService.find(userId).isEmpty()) { | |||||
| throw new NotFoundException(); | |||||
| } | |||||
| userSignService.saveSign(userId, userSign); | |||||
| } | |||||
| @GetMapping | |||||
| // @PreAuthorize("hasAuthority('USER_MAINTAIN')") | |||||
| public DataRes<Map<String, Object>> findSign(@PathVariable int userId) { | |||||
| return new DataRes<>(userSignService.findUserSignatureMap(userId).orElseThrow(NotFoundException::new)); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,59 @@ | |||||
| package com.ffii.core.utils; | |||||
| import java.math.BigDecimal; | |||||
| import java.util.Collection; | |||||
| import java.util.Map; | |||||
| import java.util.function.Supplier; | |||||
| import org.apache.commons.lang3.StringUtils; | |||||
| public abstract class AssertUtils { | |||||
| public static void isTrue(boolean v, Supplier<RuntimeException> errSupplier) { | |||||
| if (!v) throw errSupplier.get(); | |||||
| } | |||||
| public static void isFalse(boolean v, Supplier<RuntimeException> errSupplier) { | |||||
| if (v) throw errSupplier.get(); | |||||
| } | |||||
| public static void isNotNull(Object v, Supplier<RuntimeException> errSupplier) { | |||||
| if (v == null) throw errSupplier.get(); | |||||
| } | |||||
| public static void isNull(Object v, Supplier<RuntimeException> errSupplier) { | |||||
| if (v != null) throw errSupplier.get(); | |||||
| } | |||||
| public static void isNotEmpty(Collection<?> v, Supplier<RuntimeException> errSupplier) { | |||||
| if (v.isEmpty()) throw errSupplier.get(); | |||||
| } | |||||
| public static void isEmpty(Collection<?> v, Supplier<RuntimeException> errSupplier) { | |||||
| if (!v.isEmpty()) throw errSupplier.get(); | |||||
| } | |||||
| public static void isNotEmpty(Map<?, ?> v, Supplier<RuntimeException> errSupplier) { | |||||
| if (v.isEmpty()) throw errSupplier.get(); | |||||
| } | |||||
| public static void isEmpty(Map<?, ?> v, Supplier<RuntimeException> errSupplier) { | |||||
| if (!v.isEmpty()) throw errSupplier.get(); | |||||
| } | |||||
| public static void isNotBlank(String s, Supplier<RuntimeException> errSupplier) { | |||||
| if (StringUtils.isBlank(s)) throw errSupplier.get(); | |||||
| } | |||||
| public static void isBlank(String s, Supplier<RuntimeException> errSupplier) { | |||||
| if (StringUtils.isNotBlank(s)) throw errSupplier.get(); | |||||
| } | |||||
| public static void isZero(BigDecimal bd, Supplier<RuntimeException> errSupplier) { | |||||
| if (BigDecimal.ZERO.equals(bd)) throw errSupplier.get(); | |||||
| } | |||||
| public static void isZero(Number i, Supplier<RuntimeException> errSupplier) { | |||||
| if (i.equals(0)) throw errSupplier.get(); | |||||
| } | |||||
| } | |||||