Alex Cheung 2 лет назад
Родитель
Сommit
86ab1f6506
100 измененных файлов: 7455 добавлений и 0 удалений
  1. +40
    -0
      .gitignore
  2. +46
    -0
      build.gradle
  3. +12
    -0
      example.launch.json
  4. Двоичные данные
      gradle/wrapper/gradle-wrapper.jar
  5. +5
    -0
      gradle/wrapper/gradle-wrapper.properties
  6. +234
    -0
      gradlew
  7. +89
    -0
      gradlew.bat
  8. +1
    -0
      settings.gradle
  9. +13
    -0
      src/main/java/com/ffii/BaseApplication.java
  10. +103
    -0
      src/main/java/com/ffii/baseapp/example/web/PublicExampleController.java
  11. +26
    -0
      src/main/java/com/ffii/core/common/ErrorCodes.java
  12. +55
    -0
      src/main/java/com/ffii/core/common/MailSMTP.java
  13. +114
    -0
      src/main/java/com/ffii/core/common/PasswordRule.java
  14. +146
    -0
      src/main/java/com/ffii/core/common/SecurityUtils.java
  15. +59
    -0
      src/main/java/com/ffii/core/common/SettingNames.java
  16. +46
    -0
      src/main/java/com/ffii/core/common/file/FileRefType.java
  17. +13
    -0
      src/main/java/com/ffii/core/common/file/dao/FileBlobDao.java
  18. +15
    -0
      src/main/java/com/ffii/core/common/file/dao/FileDao.java
  19. +17
    -0
      src/main/java/com/ffii/core/common/file/dao/FileRefDao.java
  20. +87
    -0
      src/main/java/com/ffii/core/common/file/entity/File.java
  21. +47
    -0
      src/main/java/com/ffii/core/common/file/entity/FileBlob.java
  22. +74
    -0
      src/main/java/com/ffii/core/common/file/entity/FileRef.java
  23. +169
    -0
      src/main/java/com/ffii/core/common/file/service/FileService.java
  24. +78
    -0
      src/main/java/com/ffii/core/common/file/web/FileController.java
  25. +125
    -0
      src/main/java/com/ffii/core/common/file/web/FileDownloadController.java
  26. +80
    -0
      src/main/java/com/ffii/core/common/file/web/FileUploadController.java
  27. +11
    -0
      src/main/java/com/ffii/core/common/id/dao/IdCounterDao.java
  28. +56
    -0
      src/main/java/com/ffii/core/common/id/entity/IdCounter.java
  29. +28
    -0
      src/main/java/com/ffii/core/common/id/service/IdCounterService.java
  30. +356
    -0
      src/main/java/com/ffii/core/common/mail/pojo/MailRequest.java
  31. +49
    -0
      src/main/java/com/ffii/core/common/mail/service/MailSenderService.java
  32. +143
    -0
      src/main/java/com/ffii/core/common/mail/service/MailService.java
  33. +15
    -0
      src/main/java/com/ffii/core/common/mobile/dao/AccessTokenDao.java
  34. +81
    -0
      src/main/java/com/ffii/core/common/mobile/entity/AccessToken.java
  35. +33
    -0
      src/main/java/com/ffii/core/common/mobile/service/AccessTokenService.java
  36. +59
    -0
      src/main/java/com/ffii/core/common/mobile/web/ProtectedMobileLogoutController.java
  37. +281
    -0
      src/main/java/com/ffii/core/common/mobile/web/PublicMobileLoginController.java
  38. +21
    -0
      src/main/java/com/ffii/core/common/res/LocaleRes.java
  39. +53
    -0
      src/main/java/com/ffii/core/common/res/MeRes.java
  40. +104
    -0
      src/main/java/com/ffii/core/common/web/CommonProtectedController.java
  41. +40
    -0
      src/main/java/com/ffii/core/common/web/CommonPublicController.java
  42. +15
    -0
      src/main/java/com/ffii/core/common/web/RootController.java
  43. +39
    -0
      src/main/java/com/ffii/core/config/AppConfig.java
  44. +8
    -0
      src/main/java/com/ffii/core/config/AppPasswordEncoder.java
  45. +115
    -0
      src/main/java/com/ffii/core/config/OpenApiConfig.java
  46. +88
    -0
      src/main/java/com/ffii/core/config/SecurityConfig.java
  47. +65
    -0
      src/main/java/com/ffii/core/config/auth/FailureHandler.java
  48. +40
    -0
      src/main/java/com/ffii/core/config/auth/SuccessHandler.java
  49. +106
    -0
      src/main/java/com/ffii/core/config/filter/TokenAuthFilter.java
  50. +15
    -0
      src/main/java/com/ffii/core/department/dao/DepartmentDao.java
  51. +67
    -0
      src/main/java/com/ffii/core/department/entity/Department.java
  52. +42
    -0
      src/main/java/com/ffii/core/department/req/SaveDepartmentReq.java
  53. +99
    -0
      src/main/java/com/ffii/core/department/service/DepartmentService.java
  54. +100
    -0
      src/main/java/com/ffii/core/department/web/DepartmentController.java
  55. +124
    -0
      src/main/java/com/ffii/core/entity/BaseEntity.java
  56. +49
    -0
      src/main/java/com/ffii/core/entity/IdEntity.java
  57. +15
    -0
      src/main/java/com/ffii/core/exception/BadRequestException.java
  58. +16
    -0
      src/main/java/com/ffii/core/exception/ConflictException.java
  59. +19
    -0
      src/main/java/com/ffii/core/exception/InternalServerErrorException.java
  60. +13
    -0
      src/main/java/com/ffii/core/exception/NotFoundException.java
  61. +17
    -0
      src/main/java/com/ffii/core/exception/UnprocessableEntityException.java
  62. +21
    -0
      src/main/java/com/ffii/core/response/DataRes.java
  63. +36
    -0
      src/main/java/com/ffii/core/response/ErrorRes.java
  64. +39
    -0
      src/main/java/com/ffii/core/response/FailureRes.java
  65. +21
    -0
      src/main/java/com/ffii/core/response/IdRes.java
  66. +41
    -0
      src/main/java/com/ffii/core/response/RecordsRes.java
  67. +10
    -0
      src/main/java/com/ffii/core/settings/dao/SettingsDao.java
  68. +74
    -0
      src/main/java/com/ffii/core/settings/entity/Settings.java
  69. +210
    -0
      src/main/java/com/ffii/core/settings/service/SettingsService.java
  70. +69
    -0
      src/main/java/com/ffii/core/settings/web/SettingsController.java
  71. +42
    -0
      src/main/java/com/ffii/core/support/AbstractBaseEntityService.java
  72. +10
    -0
      src/main/java/com/ffii/core/support/AbstractController.java
  73. +16
    -0
      src/main/java/com/ffii/core/support/AbstractDao.java
  74. +65
    -0
      src/main/java/com/ffii/core/support/AbstractIdEntityService.java
  75. +15
    -0
      src/main/java/com/ffii/core/support/AbstractService.java
  76. +44
    -0
      src/main/java/com/ffii/core/support/ErrorHandler.java
  77. +432
    -0
      src/main/java/com/ffii/core/support/JdbcDao.java
  78. +7
    -0
      src/main/java/com/ffii/core/user/dao/GroupDao.java
  79. +13
    -0
      src/main/java/com/ffii/core/user/dao/UserDao.java
  80. +37
    -0
      src/main/java/com/ffii/core/user/entity/Group.java
  81. +287
    -0
      src/main/java/com/ffii/core/user/entity/User.java
  82. +20
    -0
      src/main/java/com/ffii/core/user/req/ForgetPwReq.java
  83. +21
    -0
      src/main/java/com/ffii/core/user/req/NewUserReq.java
  84. +80
    -0
      src/main/java/com/ffii/core/user/req/SaveGroupReq.java
  85. +69
    -0
      src/main/java/com/ffii/core/user/req/SearchUserReq.java
  86. +171
    -0
      src/main/java/com/ffii/core/user/req/UpdateUserReq.java
  87. +45
    -0
      src/main/java/com/ffii/core/user/res/LoadUserRes.java
  88. +29
    -0
      src/main/java/com/ffii/core/user/service/AuthService.java
  89. +173
    -0
      src/main/java/com/ffii/core/user/service/GroupService.java
  90. +45
    -0
      src/main/java/com/ffii/core/user/service/UserAttemptService.java
  91. +391
    -0
      src/main/java/com/ffii/core/user/service/UserService.java
  92. +46
    -0
      src/main/java/com/ffii/core/user/service/UserSignService.java
  93. +50
    -0
      src/main/java/com/ffii/core/user/service/pojo/AuthRecord.java
  94. +196
    -0
      src/main/java/com/ffii/core/user/service/pojo/UserRecord.java
  95. +30
    -0
      src/main/java/com/ffii/core/user/web/AuthController.java
  96. +88
    -0
      src/main/java/com/ffii/core/user/web/ForgetPwController.java
  97. +85
    -0
      src/main/java/com/ffii/core/user/web/GroupController.java
  98. +241
    -0
      src/main/java/com/ffii/core/user/web/UserController.java
  99. +51
    -0
      src/main/java/com/ffii/core/user/web/UserSignController.java
  100. +59
    -0
      src/main/java/com/ffii/core/utils/AssertUtils.java

+ 40
- 0
.gitignore Просмотреть файл

@@ -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/

+ 46
- 0
build.gradle Просмотреть файл

@@ -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()
}

+ 12
- 0
example.launch.json Просмотреть файл

@@ -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"
}
]
}

Двоичные данные
gradle/wrapper/gradle-wrapper.jar Просмотреть файл


+ 5
- 0
gradle/wrapper/gradle-wrapper.properties Просмотреть файл

@@ -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

+ 234
- 0
gradlew Просмотреть файл

@@ -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" "$@"

+ 89
- 0
gradlew.bat Просмотреть файл

@@ -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

+ 1
- 0
settings.gradle Просмотреть файл

@@ -0,0 +1 @@
rootProject.name = 'BASE'

+ 13
- 0
src/main/java/com/ffii/BaseApplication.java Просмотреть файл

@@ -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);
}

}

+ 103
- 0
src/main/java/com/ffii/baseapp/example/web/PublicExampleController.java Просмотреть файл

@@ -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());
}

}

+ 26
- 0
src/main/java/com/ffii/core/common/ErrorCodes.java Просмотреть файл

@@ -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";
}

+ 55
- 0
src/main/java/com/ffii/core/common/MailSMTP.java Просмотреть файл

@@ -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;
}

}

+ 114
- 0
src/main/java/com/ffii/core/common/PasswordRule.java Просмотреть файл

@@ -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();
}

}

+ 146
- 0
src/main/java/com/ffii/core/common/SecurityUtils.java Просмотреть файл

@@ -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);
}

}

+ 59
- 0
src/main/java/com/ffii/core/common/SettingNames.java Просмотреть файл

@@ -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";

}

+ 46
- 0
src/main/java/com/ffii/core/common/file/FileRefType.java Просмотреть файл

@@ -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 };

}

+ 13
- 0
src/main/java/com/ffii/core/common/file/dao/FileBlobDao.java Просмотреть файл

@@ -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);
}

+ 15
- 0
src/main/java/com/ffii/core/common/file/dao/FileDao.java Просмотреть файл

@@ -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);
}

+ 17
- 0
src/main/java/com/ffii/core/common/file/dao/FileRefDao.java Просмотреть файл

@@ -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);
}

+ 87
- 0
src/main/java/com/ffii/core/common/file/entity/File.java Просмотреть файл

@@ -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;
}

}

+ 47
- 0
src/main/java/com/ffii/core/common/file/entity/FileBlob.java Просмотреть файл

@@ -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;
}

}

+ 74
- 0
src/main/java/com/ffii/core/common/file/entity/FileRef.java Просмотреть файл

@@ -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;
}

}

+ 169
- 0
src/main/java/com/ffii/core/common/file/service/FileService.java Просмотреть файл

@@ -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);
}

}

+ 78
- 0
src/main/java/com/ffii/core/common/file/web/FileController.java Просмотреть файл

@@ -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;
}

}
}

+ 125
- 0
src/main/java/com/ffii/core/common/file/web/FileDownloadController.java Просмотреть файл

@@ -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;
}
}

+ 80
- 0
src/main/java/com/ffii/core/common/file/web/FileUploadController.java Просмотреть файл

@@ -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);
}
}

}

+ 11
- 0
src/main/java/com/ffii/core/common/id/dao/IdCounterDao.java Просмотреть файл

@@ -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);
}

+ 56
- 0
src/main/java/com/ffii/core/common/id/entity/IdCounter.java Просмотреть файл

@@ -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;
}

}

+ 28
- 0
src/main/java/com/ffii/core/common/id/service/IdCounterService.java Просмотреть файл

@@ -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());
}
}

+ 356
- 0
src/main/java/com/ffii/core/common/mail/pojo/MailRequest.java Просмотреть файл

@@ -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;
}
}
}

+ 49
- 0
src/main/java/com/ffii/core/common/mail/service/MailSenderService.java Просмотреть файл

@@ -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;
}
}

+ 143
- 0
src/main/java/com/ffii/core/common/mail/service/MailService.java Просмотреть файл

@@ -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);
}

}

+ 15
- 0
src/main/java/com/ffii/core/common/mobile/dao/AccessTokenDao.java Просмотреть файл

@@ -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);
}

+ 81
- 0
src/main/java/com/ffii/core/common/mobile/entity/AccessToken.java Просмотреть файл

@@ -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;
}

}

+ 33
- 0
src/main/java/com/ffii/core/common/mobile/service/AccessTokenService.java Просмотреть файл

@@ -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);
}
}

+ 59
- 0
src/main/java/com/ffii/core/common/mobile/web/ProtectedMobileLogoutController.java Просмотреть файл

@@ -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());
}

}

+ 281
- 0
src/main/java/com/ffii/core/common/mobile/web/PublicMobileLoginController.java Просмотреть файл

@@ -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;
}

}
}

}

+ 21
- 0
src/main/java/com/ffii/core/common/res/LocaleRes.java Просмотреть файл

@@ -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;
}

}

+ 53
- 0
src/main/java/com/ffii/core/common/res/MeRes.java Просмотреть файл

@@ -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;
}

}

+ 104
- 0
src/main/java/com/ffii/core/common/web/CommonProtectedController.java Просмотреть файл

@@ -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;
}

}
}

+ 40
- 0
src/main/java/com/ffii/core/common/web/CommonPublicController.java Просмотреть файл

@@ -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;
}

}
}

+ 15
- 0
src/main/java/com/ffii/core/common/web/RootController.java Просмотреть файл

@@ -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";
}
}

+ 39
- 0
src/main/java/com/ffii/core/config/AppConfig.java Просмотреть файл

@@ -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();
}
}

+ 8
- 0
src/main/java/com/ffii/core/config/AppPasswordEncoder.java Просмотреть файл

@@ -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{
}

+ 115
- 0
src/main/java/com/ffii/core/config/OpenApiConfig.java Просмотреть файл

@@ -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"));
}

}

+ 88
- 0
src/main/java/com/ffii/core/config/SecurityConfig.java Просмотреть файл

@@ -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();
}

}

+ 65
- 0
src/main/java/com/ffii/core/config/auth/FailureHandler.java Просмотреть файл

@@ -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());
}
}

}

+ 40
- 0
src/main/java/com/ffii/core/config/auth/SuccessHandler.java Просмотреть файл

@@ -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());
}

}

+ 106
- 0
src/main/java/com/ffii/core/config/filter/TokenAuthFilter.java Просмотреть файл

@@ -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);
}
}
}

+ 15
- 0
src/main/java/com/ffii/core/department/dao/DepartmentDao.java Просмотреть файл

@@ -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);
}

+ 67
- 0
src/main/java/com/ffii/core/department/entity/Department.java Просмотреть файл

@@ -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;
}

}

+ 42
- 0
src/main/java/com/ffii/core/department/req/SaveDepartmentReq.java Просмотреть файл

@@ -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;
}

}

+ 99
- 0
src/main/java/com/ffii/core/department/service/DepartmentService.java Просмотреть файл

@@ -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));
}
}

+ 100
- 0
src/main/java/com/ffii/core/department/web/DepartmentController.java Просмотреть файл

@@ -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()));
}
}

+ 124
- 0
src/main/java/com/ffii/core/entity/BaseEntity.java Просмотреть файл

@@ -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;
}

}

+ 49
- 0
src/main/java/com/ffii/core/entity/IdEntity.java Просмотреть файл

@@ -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;
}
}

+ 15
- 0
src/main/java/com/ffii/core/exception/BadRequestException.java Просмотреть файл

@@ -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);
}
}

+ 16
- 0
src/main/java/com/ffii/core/exception/ConflictException.java Просмотреть файл

@@ -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);
}
}

+ 19
- 0
src/main/java/com/ffii/core/exception/InternalServerErrorException.java Просмотреть файл

@@ -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);
}
}

+ 13
- 0
src/main/java/com/ffii/core/exception/NotFoundException.java Просмотреть файл

@@ -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);
}
}

+ 17
- 0
src/main/java/com/ffii/core/exception/UnprocessableEntityException.java Просмотреть файл

@@ -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);
}

}

+ 21
- 0
src/main/java/com/ffii/core/response/DataRes.java Просмотреть файл

@@ -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;
}

}

+ 36
- 0
src/main/java/com/ffii/core/response/ErrorRes.java Просмотреть файл

@@ -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;
}

}

+ 39
- 0
src/main/java/com/ffii/core/response/FailureRes.java Просмотреть файл

@@ -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;
}

}

+ 21
- 0
src/main/java/com/ffii/core/response/IdRes.java Просмотреть файл

@@ -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;
}

}

+ 41
- 0
src/main/java/com/ffii/core/response/RecordsRes.java Просмотреть файл

@@ -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;
}

}

+ 10
- 0
src/main/java/com/ffii/core/settings/dao/SettingsDao.java Просмотреть файл

@@ -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);
}

+ 74
- 0
src/main/java/com/ffii/core/settings/entity/Settings.java Просмотреть файл

@@ -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;
}

}

+ 210
- 0
src/main/java/com/ffii/core/settings/service/SettingsService.java Просмотреть файл

@@ -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);
}

}

+ 69
- 0
src/main/java/com/ffii/core/settings/web/SettingsController.java Просмотреть файл

@@ -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;
}
}
}

+ 42
- 0
src/main/java/com/ffii/core/support/AbstractBaseEntityService.java Просмотреть файл

@@ -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);
}
}

+ 10
- 0
src/main/java/com/ffii/core/support/AbstractController.java Просмотреть файл

@@ -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());

}

+ 16
- 0
src/main/java/com/ffii/core/support/AbstractDao.java Просмотреть файл

@@ -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> {
}

+ 65
- 0
src/main/java/com/ffii/core/support/AbstractIdEntityService.java Просмотреть файл

@@ -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);
}
}

+ 15
- 0
src/main/java/com/ffii/core/support/AbstractService.java Просмотреть файл

@@ -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;
}
}

+ 44
- 0
src/main/java/com/ffii/core/support/ErrorHandler.java Просмотреть файл

@@ -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);
}

}

+ 432
- 0
src/main/java/com/ffii/core/support/JdbcDao.java Просмотреть файл

@@ -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));
}
}

+ 7
- 0
src/main/java/com/ffii/core/user/dao/GroupDao.java Просмотреть файл

@@ -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> {
}

+ 13
- 0
src/main/java/com/ffii/core/user/dao/UserDao.java Просмотреть файл

@@ -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);
}

+ 37
- 0
src/main/java/com/ffii/core/user/entity/Group.java Просмотреть файл

@@ -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;
}

}

+ 287
- 0
src/main/java/com/ffii/core/user/entity/User.java Просмотреть файл

@@ -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;
}

}

+ 20
- 0
src/main/java/com/ffii/core/user/req/ForgetPwReq.java Просмотреть файл

@@ -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;
}

}

+ 21
- 0
src/main/java/com/ffii/core/user/req/NewUserReq.java Просмотреть файл

@@ -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;
}

}

+ 80
- 0
src/main/java/com/ffii/core/user/req/SaveGroupReq.java Просмотреть файл

@@ -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;
}

}

+ 69
- 0
src/main/java/com/ffii/core/user/req/SearchUserReq.java Просмотреть файл

@@ -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;
}

}

+ 171
- 0
src/main/java/com/ffii/core/user/req/UpdateUserReq.java Просмотреть файл

@@ -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
@Email
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;
}

}

+ 45
- 0
src/main/java/com/ffii/core/user/res/LoadUserRes.java Просмотреть файл

@@ -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;
}

}

+ 29
- 0
src/main/java/com/ffii/core/user/service/AuthService.java Просмотреть файл

@@ -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);
}

}

+ 173
- 0
src/main/java/com/ffii/core/user/service/GroupService.java Просмотреть файл

@@ -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));
}
}

+ 45
- 0
src/main/java/com/ffii/core/user/service/UserAttemptService.java Просмотреть файл

@@ -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);
}
}

+ 391
- 0
src/main/java/com/ffii/core/user/service/UserService.java Просмотреть файл

@@ -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);
}
}

+ 46
- 0
src/main/java/com/ffii/core/user/service/UserSignService.java Просмотреть файл

@@ -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);
}
}

+ 50
- 0
src/main/java/com/ffii/core/user/service/pojo/AuthRecord.java Просмотреть файл

@@ -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;
}

}

+ 196
- 0
src/main/java/com/ffii/core/user/service/pojo/UserRecord.java Просмотреть файл

@@ -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;
}

}

+ 30
- 0
src/main/java/com/ffii/core/user/web/AuthController.java Просмотреть файл

@@ -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());
}

}

+ 88
- 0
src/main/java/com/ffii/core/user/web/ForgetPwController.java Просмотреть файл

@@ -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;
}

}

}

+ 85
- 0
src/main/java/com/ffii/core/user/web/GroupController.java Просмотреть файл

@@ -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()));
}

}

+ 241
- 0
src/main/java/com/ffii/core/user/web/UserController.java Просмотреть файл

@@ -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;
}

}
}

+ 51
- 0
src/main/java/com/ffii/core/user/web/UserSignController.java Просмотреть файл

@@ -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));
}
}

+ 59
- 0
src/main/java/com/ffii/core/utils/AssertUtils.java Просмотреть файл

@@ -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();
}

}

Некоторые файлы не были показаны из-за большого количества измененных файлов

Загрузка…
Отмена
Сохранить