Sfoglia il codice sorgente

initial commit

create_edit_user
MSI\derek 6 mesi fa
parent
commit
d3ff6a1acd
94 ha cambiato i file con 6350 aggiunte e 1 eliminazioni
  1. +37
    -0
      .gitignore
  2. +48
    -1
      README.md
  3. +56
    -0
      build.gradle
  4. BIN
      gradle/wrapper/gradle-wrapper.jar
  5. +6
    -0
      gradle/wrapper/gradle-wrapper.properties
  6. +240
    -0
      gradlew
  7. +91
    -0
      gradlew.bat
  8. +1
    -0
      settings.gradle
  9. +121
    -0
      src/main/java/com/ffii/core/entity/BaseEntity.java
  10. +49
    -0
      src/main/java/com/ffii/core/entity/IdEntity.java
  11. +15
    -0
      src/main/java/com/ffii/core/exception/BadRequestException.java
  12. +16
    -0
      src/main/java/com/ffii/core/exception/ConflictException.java
  13. +19
    -0
      src/main/java/com/ffii/core/exception/InternalServerErrorException.java
  14. +13
    -0
      src/main/java/com/ffii/core/exception/NotFoundException.java
  15. +37
    -0
      src/main/java/com/ffii/core/exception/UnprocessableEntityException.java
  16. +21
    -0
      src/main/java/com/ffii/core/response/DataRes.java
  17. +36
    -0
      src/main/java/com/ffii/core/response/ErrorRes.java
  18. +39
    -0
      src/main/java/com/ffii/core/response/FailureRes.java
  19. +21
    -0
      src/main/java/com/ffii/core/response/IdRes.java
  20. +41
    -0
      src/main/java/com/ffii/core/response/RecordsRes.java
  21. +42
    -0
      src/main/java/com/ffii/core/support/AbstractBaseEntityService.java
  22. +65
    -0
      src/main/java/com/ffii/core/support/AbstractIdEntityService.java
  23. +16
    -0
      src/main/java/com/ffii/core/support/AbstractRepository.java
  24. +15
    -0
      src/main/java/com/ffii/core/support/AbstractService.java
  25. +44
    -0
      src/main/java/com/ffii/core/support/ErrorHandler.java
  26. +437
    -0
      src/main/java/com/ffii/core/support/JdbcDao.java
  27. +85
    -0
      src/main/java/com/ffii/core/utils/AES.java
  28. +242
    -0
      src/main/java/com/ffii/core/utils/CriteriaArgsBuilder.java
  29. +778
    -0
      src/main/java/com/ffii/core/utils/ExcelUtils.java
  30. +47
    -0
      src/main/java/com/ffii/core/utils/JsonUtils.java
  31. +114
    -0
      src/main/java/com/ffii/core/utils/JwtTokenUtil.java
  32. +35
    -0
      src/main/java/com/ffii/core/utils/MapUtils.java
  33. +42
    -0
      src/main/java/com/ffii/core/utils/Params.java
  34. +83
    -0
      src/main/java/com/ffii/core/utils/PasswordUtils.java
  35. +13
    -0
      src/main/java/com/ffii/tsms/TsmsApplication.java
  36. +36
    -0
      src/main/java/com/ffii/tsms/config/AppConfig.java
  37. +29
    -0
      src/main/java/com/ffii/tsms/config/WebConfig.java
  38. +80
    -0
      src/main/java/com/ffii/tsms/config/security/SecurityConfig.java
  39. +75
    -0
      src/main/java/com/ffii/tsms/config/security/jwt/JwtRequestFilter.java
  40. +31
    -0
      src/main/java/com/ffii/tsms/config/security/jwt/service/JwtUserDetailsService.java
  41. +151
    -0
      src/main/java/com/ffii/tsms/config/security/jwt/web/JwtAuthenticationController.java
  42. +43
    -0
      src/main/java/com/ffii/tsms/config/security/service/LoginLogService.java
  43. +13
    -0
      src/main/java/com/ffii/tsms/model/AbilityModel.java
  44. +28
    -0
      src/main/java/com/ffii/tsms/model/ExceptionResponse.java
  45. +40
    -0
      src/main/java/com/ffii/tsms/model/JwtRequest.java
  46. +56
    -0
      src/main/java/com/ffii/tsms/model/JwtResponse.java
  47. +39
    -0
      src/main/java/com/ffii/tsms/model/RefreshToken.java
  48. +16
    -0
      src/main/java/com/ffii/tsms/model/TokenRefreshRequest.java
  49. +37
    -0
      src/main/java/com/ffii/tsms/model/TokenRefreshResponse.java
  50. +17
    -0
      src/main/java/com/ffii/tsms/modules/common/ErrorCodes.java
  51. +116
    -0
      src/main/java/com/ffii/tsms/modules/common/PasswordRule.java
  52. +146
    -0
      src/main/java/com/ffii/tsms/modules/common/SecurityUtils.java
  53. +61
    -0
      src/main/java/com/ffii/tsms/modules/common/SettingNames.java
  54. +48
    -0
      src/main/java/com/ffii/tsms/modules/common/service/AuditLogService.java
  55. +74
    -0
      src/main/java/com/ffii/tsms/modules/settings/entity/Settings.java
  56. +12
    -0
      src/main/java/com/ffii/tsms/modules/settings/entity/SettingsRepository.java
  57. +208
    -0
      src/main/java/com/ffii/tsms/modules/settings/service/SettingsService.java
  58. +66
    -0
      src/main/java/com/ffii/tsms/modules/settings/web/SettingsController.java
  59. +37
    -0
      src/main/java/com/ffii/tsms/modules/user/entity/Group.java
  60. +6
    -0
      src/main/java/com/ffii/tsms/modules/user/entity/GroupRepository.java
  61. +254
    -0
      src/main/java/com/ffii/tsms/modules/user/entity/User.java
  62. +15
    -0
      src/main/java/com/ffii/tsms/modules/user/entity/UserRepository.java
  63. +29
    -0
      src/main/java/com/ffii/tsms/modules/user/req/NewPublicUserReq.java
  64. +21
    -0
      src/main/java/com/ffii/tsms/modules/user/req/NewUserReq.java
  65. +80
    -0
      src/main/java/com/ffii/tsms/modules/user/req/SaveGroupReq.java
  66. +69
    -0
      src/main/java/com/ffii/tsms/modules/user/req/SearchUserReq.java
  67. +151
    -0
      src/main/java/com/ffii/tsms/modules/user/req/UpdateUserReq.java
  68. +176
    -0
      src/main/java/com/ffii/tsms/modules/user/service/GroupService.java
  69. +48
    -0
      src/main/java/com/ffii/tsms/modules/user/service/UserAuthorityService.java
  70. +269
    -0
      src/main/java/com/ffii/tsms/modules/user/service/UserService.java
  71. +41
    -0
      src/main/java/com/ffii/tsms/modules/user/service/pojo/AuthRecord.java
  72. +155
    -0
      src/main/java/com/ffii/tsms/modules/user/service/pojo/UserRecord.java
  73. +45
    -0
      src/main/java/com/ffii/tsms/modules/user/service/res/LoadUserRes.java
  74. +80
    -0
      src/main/java/com/ffii/tsms/modules/user/web/GroupController.java
  75. +21
    -0
      src/main/java/com/ffii/tsms/modules/user/web/TestController.java
  76. +193
    -0
      src/main/java/com/ffii/tsms/modules/user/web/UserController.java
  77. +5
    -0
      src/main/resources/application-db-2fi.yml
  78. +5
    -0
      src/main/resources/application-db-local.yml
  79. +9
    -0
      src/main/resources/application-ldap-local.yml
  80. +2
    -0
      src/main/resources/application-prod-linux.yml
  81. +2
    -0
      src/main/resources/application-prod-win.yml
  82. +28
    -0
      src/main/resources/application.yml
  83. +77
    -0
      src/main/resources/db/changelog/changes/20230720_01_alex/01_base.sql
  84. +13
    -0
      src/main/resources/db/changelog/changes/20230720_01_alex/02_settings.sql
  85. +10
    -0
      src/main/resources/db/changelog/changes/20230720_01_alex/03_settings_data.sql
  86. +5
    -0
      src/main/resources/db/changelog/changes/20230720_01_alex/04_update_user_authority.sql
  87. +13
    -0
      src/main/resources/db/changelog/changes/20230725_01_alex/01_audit_log.sql
  88. +11
    -0
      src/main/resources/db/changelog/changes/20230725_01_alex/02_user_login_log.sql
  89. +3
    -0
      src/main/resources/db/changelog/db.changelog-master.yaml
  90. +14
    -0
      src/main/resources/ldap-test-users.ldif
  91. +23
    -0
      src/main/resources/log4j2-prod-linux.yml
  92. +23
    -0
      src/main/resources/log4j2-prod-win.yml
  93. +17
    -0
      src/main/resources/log4j2.yml
  94. +13
    -0
      src/test/java/com/ffii/tsms/ArsApplicationTests.java

+ 37
- 0
.gitignore Vedi File

@@ -0,0 +1,37 @@
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/

+ 48
- 1
README.md Vedi File

@@ -1,2 +1,49 @@
# FPSMS-backend
# TSMS Backend

## Getting started
1. Create a schema named `tsmsdb` in MySQL workbench
2. Create a `launch.json` file and put it into the `.vscode` folder
```json
{
"version": "0.2.0",
"configurations": [
{
"type": "java",
"name": "TsmsApplication",
"request": "launch",
"mainClass": "com.ffii.tsms.TsmsApplication",
"projectName": "TSMS-backend"
},
{
"type": "java",
"name": "Launch Local",
"request": "launch",
"mainClass": "com.ffii.tsms.TsmsApplication",
"console": "internalConsole",
"projectName": "TSMS-backend",
"args": "--spring.profiles.active=db-local,ldap-local"
}
]
}
```
3. Create a `settings.json` file and put it into the `.vscode` folder
*(You may need to change some settings depending on your development environment)*
```json
{
"java.configuration.updateBuildConfiguration": "interactive",
"java.jdt.ls.java.home": "C:\\java\\jdk-17.0.8",
"java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx2G -Xms100m -Xlog:disable"
}
```

4. Run and Debug "Launch Local"

## Using gradle

This project can also be run using gradle.

### Running the application
After creating the table in MySQL, run
```shell
./gradlew bootRun --args='--spring.profiles.active=db-local'
```

+ 56
- 0
build.gradle Vedi File

@@ -0,0 +1,56 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.1'
id 'io.spring.dependency-management' version '1.1.0'
}

group = 'com.ffii'
version = '0.0.1-SNAPSHOT'

java {
sourceCompatibility = '17'
}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-ldap'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-log4j2'
implementation 'org.springframework.security:spring-security-ldap'
implementation 'org.liquibase:liquibase-core'
implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0'
implementation group: 'org.apache.poi', name: 'poi', version: '5.2.3'
implementation group: 'org.apache.poi', name: 'poi-ooxml', version: '5.2.3'

implementation group: 'jakarta.persistence', name: 'jakarta.persistence-api', version: '3.1.0'
implementation group: 'jakarta.annotation', name: 'jakarta.annotation-api', version: '2.1.1'
implementation group: 'jakarta.validation', name: 'jakarta.validation-api', version: '3.0.2'
implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.15.2'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.15.2'

implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'

compileOnly group: 'jakarta.servlet', name: 'jakarta.servlet-api', version: '6.0.0'
runtimeOnly 'com.mysql:mysql-connector-j'
runtimeOnly 'com.unboundid:unboundid-ldapsdk:6.0.9'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}

configurations {
all {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}
}

BIN
gradle/wrapper/gradle-wrapper.jar Vedi File


+ 6
- 0
gradle/wrapper/gradle-wrapper.properties Vedi File

@@ -0,0 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

+ 240
- 0
gradlew Vedi File

@@ -0,0 +1,240 @@
#!/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 \
"$@"

# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi

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

+ 91
- 0
gradlew.bat Vedi File

@@ -0,0 +1,91 @@
@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% equ 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% equ 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!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%

:mainEnd
if "%OS%"=="Windows_NT" endlocal

:omega

+ 1
- 0
settings.gradle Vedi File

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

+ 121
- 0
src/main/java/com/ffii/core/entity/BaseEntity.java Vedi File

@@ -0,0 +1,121 @@
package com.ffii.core.entity;

import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Optional;

import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Version;
import jakarta.validation.constraints.NotNull;

import org.springframework.security.core.context.SecurityContextHolder;

/** @author Terence */
@MappedSuperclass
public abstract class BaseEntity<ID extends Serializable> extends IdEntity<ID> {

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

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 Vedi File

@@ -0,0 +1,49 @@
package com.ffii.core.entity;

import java.io.Serializable;

import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.PostLoad;
import jakarta.persistence.PrePersist;
import jakarta.persistence.Transient;

import org.springframework.data.domain.Persistable;

import com.fasterxml.jackson.annotation.JsonIgnore;

/** @author Terence */
@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 Vedi File

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

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

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

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

+ 37
- 0
src/main/java/com/ffii/core/exception/UnprocessableEntityException.java Vedi File

@@ -0,0 +1,37 @@
package com.ffii.core.exception;

import java.util.Map;

import jakarta.validation.constraints.NotNull;

import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

/* sub record not found (e.g. item_line record) */
public class UnprocessableEntityException extends ResponseStatusException {

public UnprocessableEntityException() {
super(HttpStatus.UNPROCESSABLE_ENTITY);
}

public UnprocessableEntityException(@NotNull Map<String, Object> map) {
super(HttpStatus.UNPROCESSABLE_ENTITY, map2Str(map));
}

public UnprocessableEntityException(String reason) {
super(HttpStatus.UNPROCESSABLE_ENTITY, reason);
}

private static String map2Str(@NotNull Map<String, Object> map) {
try {
return new ObjectMapper().writeValueAsString(map);
} catch (JsonProcessingException e) {
e.printStackTrace();
return "";
}
}

}

+ 21
- 0
src/main/java/com/ffii/core/response/DataRes.java Vedi File

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

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

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

@@ -0,0 +1,21 @@
package com.ffii.core.response;

public class IdRes {
private long id;

public IdRes() {
}

public IdRes(long id) {
this.id = id;
}

public long getId() {
return id;
}

public void setId(long id) {
this.id = id;
}

}

+ 41
- 0
src/main/java/com/ffii/core/response/RecordsRes.java Vedi File

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

}

+ 42
- 0
src/main/java/com/ffii/core/support/AbstractBaseEntityService.java Vedi File

@@ -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 Alex */
public abstract class AbstractBaseEntityService<T extends BaseEntity<ID>, ID extends Serializable, R extends AbstractRepository<T, ID>>
extends AbstractIdEntityService<T, ID, R> {

public AbstractBaseEntityService(JdbcDao jdbcDao, R repository) {
super(jdbcDao, repository);
}

/** find and check versionId */
public Optional<T> find(ID id, int version) {
Assert.notNull(id, "id must not be null");
return repository.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);
}
}

+ 65
- 0
src/main/java/com/ffii/core/support/AbstractIdEntityService.java Vedi File

@@ -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 Alex */
public abstract class AbstractIdEntityService<T extends IdEntity<ID>, ID extends Serializable, R extends AbstractRepository<T, ID>>
extends AbstractService {

protected R repository;

public AbstractIdEntityService(JdbcDao jdbcDao, R repository) {
super(jdbcDao);
this.repository = repository;
}

@Transactional(rollbackFor = Exception.class)
public T save(T entity) {
Assert.notNull(entity, "entity must not be null");
return this.repository.save(entity);
}

@Transactional(rollbackFor = Exception.class)
public T saveAndFlush(T entity) {
Assert.notNull(entity, "entity must not be null");
return this.repository.saveAndFlush(entity);
}

public List<T> listAll() {
return this.repository.findAll();
}

public Optional<T> find(ID id) {
Assert.notNull(id, "id must not be null");
return this.repository.findById(id);
}

public boolean existsById(ID id) {
Assert.notNull(id, "id must not be null");
return this.repository.existsById(id);
}

public List<T> findAllByIds(List<ID> ids) {
Assert.notNull(ids, "ids must not be null");
return this.repository.findAllById(ids);
}

@Transactional(rollbackFor = Exception.class)
public void delete(ID id) {
Assert.notNull(id, "id must not be null");
this.repository.deleteById(id);
}

@Transactional(rollbackFor = Exception.class)
public void delete(T entity) {
Assert.notNull(entity, "entity must not be null");
this.repository.delete(entity);
}
}

+ 16
- 0
src/main/java/com/ffii/core/support/AbstractRepository.java Vedi File

@@ -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 Alex
* @see https://docs.spring.io/spring-data/jpa/docs/2.7.0/reference/html/#jpa.query-methods.query-creation
*/
@NoRepositoryBean
public interface AbstractRepository<T extends IdEntity<ID>, ID extends Serializable> extends JpaRepository<T, ID> {
}

+ 15
- 0
src/main/java/com/ffii/core/support/AbstractService.java Vedi File

@@ -0,0 +1,15 @@
package com.ffii.core.support;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/** @author Terence */
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 Vedi File

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

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

}

+ 437
- 0
src/main/java/com/ffii/core/support/JdbcDao.java Vedi File

@@ -0,0 +1,437 @@
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 Terence */
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 {
var rs = this.template.queryForObject(sql, paramMap, Boolean.class);
return rs == null ? false : rs;
} 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 {
var rs = this.template.queryForObject(sql, new BeanPropertySqlParameterSource(paramObj), Boolean.class);
return rs == null ? false : rs;
} 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 {
var rs = this.template.queryForObject(sql, paramMap, Integer.class);
return rs == null ? 0 : rs;
} 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 {
var rs = this.template.queryForObject(sql,
new BeanPropertySqlParameterSource(paramObj), Integer.class);
return rs == null ? 0 : rs;
} 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));
}
}

+ 85
- 0
src/main/java/com/ffii/core/utils/AES.java Vedi File

@@ -0,0 +1,85 @@
package com.ffii.core.utils;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class AES {

protected final Log logger = LogFactory.getLog(getClass());

private static SecretKeySpec secretKey;
private static byte[] key;

public static void setKey(String myKey) {
MessageDigest sha = null;
try {
key = myKey.getBytes("UTF-8");
sha = MessageDigest.getInstance("SHA-1");
key = sha.digest(key);
key = Arrays.copyOf(key, 16);
secretKey = new SecretKeySpec(key, "AES");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}

public static String encrypt(String strToEncrypt, String secret) {
try {
setKey(secret);
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return Base64.getEncoder().encodeToString(cipher.doFinal(strToEncrypt.getBytes("UTF-8")));
} catch (Exception e) {
System.out.println("Error while encrypting: " + e.toString());
}
return null;
}

public static String urlEncrypt(String strToEncrypt, String secret) {
try {
setKey(secret);
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return Base64.getUrlEncoder().encodeToString(cipher.doFinal(strToEncrypt.getBytes("UTF-8")));
} catch (Exception e) {
System.out.println("Error while encrypting: " + e.toString());
}
return null;
}

public static String decrypt(String strToDecrypt, String secret) {
try {
setKey(secret);
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5PADDING");
cipher.init(Cipher.DECRYPT_MODE, secretKey);
return new String(cipher.doFinal(Base64.getDecoder().decode(strToDecrypt)), "UTF-8");
} catch (Exception e) {
System.out.println("Error while decrypting: " + e.toString());
}
return null;
}

public static String urlDecrypt(String strToDecrypt, String secret) {
try {
setKey(secret);
System.out.println("strToDecrypt: " + strToDecrypt);
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5PADDING");
cipher.init(Cipher.DECRYPT_MODE, secretKey);
return new String(cipher.doFinal(Base64.getUrlDecoder().decode(strToDecrypt)), "UTF-8");
} catch (Exception e) {
System.out.println("Error while decrypting: " + e.toString());
}
return null;
}
}

+ 242
- 0
src/main/java/com/ffii/core/utils/CriteriaArgsBuilder.java Vedi File

@@ -0,0 +1,242 @@
package com.ffii.core.utils;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import jakarta.servlet.http.HttpServletRequest;

import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;

/** @author Alex */
public class CriteriaArgsBuilder {

private HttpServletRequest request;
private Map<String, Object> args;

private CriteriaArgsBuilder(HttpServletRequest request, Map<String, Object> args) {
this.args = args;
this.request = request;
}

public static CriteriaArgsBuilder withRequest(HttpServletRequest request) {
return new CriteriaArgsBuilder(request, new HashMap<String, Object>());
}

public static CriteriaArgsBuilder withRequestNMap(HttpServletRequest request, Map<String, Object> args) {
return new CriteriaArgsBuilder(request, args);
}

public CriteriaArgsBuilder addStringExact(String paramName) throws ServletRequestBindingException {
String value = StringUtils.trimToNull(ServletRequestUtils.getStringParameter(this.request, paramName));
if (value != null)
args.put(paramName, value);
return this;
}

public CriteriaArgsBuilder addStringLike(String paramName) throws ServletRequestBindingException {
String value = StringUtils.trimToNull(ServletRequestUtils.getStringParameter(this.request, paramName));
if (value != null)
args.put(paramName, "%" + value + "%");
return this;
}

public CriteriaArgsBuilder addString(String paramName) throws ServletRequestBindingException {
return this.addStringExact(paramName);
}

public CriteriaArgsBuilder addStringStartsWith(String paramName) throws ServletRequestBindingException {
String value = StringUtils.trimToNull(ServletRequestUtils.getStringParameter(this.request, paramName));
if (value != null)
args.put(paramName, value + "%");
return this;
}

public CriteriaArgsBuilder addStringEndsWith(String paramName) throws ServletRequestBindingException {
String value = StringUtils.trimToNull(ServletRequestUtils.getStringParameter(this.request, paramName));
if (value != null)
args.put(paramName, "%" + value);
return this;
}

public CriteriaArgsBuilder addStringList(String paramName) throws ServletRequestBindingException {
String[] params = ServletRequestUtils.getStringParameters(this.request, paramName);
if (params.length > 0) {
List<String> value = new ArrayList<String>(params.length);
for (String param : params)
if (StringUtils.isNotBlank(param))
value.add(param);
if (value.size() > 0)
args.put(paramName, value);
}
return this;
}

public CriteriaArgsBuilder addStringCsv(String paramName) throws ServletRequestBindingException {
String text = ServletRequestUtils.getStringParameter(this.request, paramName);
if (text != null && StringUtils.isNotEmpty(text))
args.put(paramName, Arrays.asList(text.split(",")));
return this;
}

public CriteriaArgsBuilder addInteger(String paramName) throws ServletRequestBindingException {
Integer value = StringUtils.isNotBlank(this.request.getParameter(paramName))
? ServletRequestUtils.getRequiredIntParameter(request, paramName)
: null;
if (value != null)
args.put(paramName, value);
return this;
}

public CriteriaArgsBuilder addNonZeroInteger(String paramName) throws ServletRequestBindingException {
Integer value = StringUtils.isNotBlank(this.request.getParameter(paramName))
? ServletRequestUtils.getRequiredIntParameter(request, paramName)
: null;
if (value != null && value.intValue() != 0)
args.put(paramName, value);
return this;
}

public CriteriaArgsBuilder addIntegerList(String paramName) throws ServletRequestBindingException {
int[] params = ServletRequestUtils.getIntParameters(request, paramName);
if (params.length > 0) {
List<Integer> values = new ArrayList<Integer>();
for (int param : params)
values.add(param);
args.put(paramName, values);
}
return this;
}

public CriteriaArgsBuilder addNonZeroIntegerList(String paramName) throws ServletRequestBindingException {
int[] params = ServletRequestUtils.getIntParameters(request, paramName);
if (params.length > 0) {
List<Integer> values = new ArrayList<Integer>();
for (int param : params)
if (param != 0)
values.add(param);
args.put(paramName, values);
}
return this;
}

public CriteriaArgsBuilder addLong(String paramName) throws ServletRequestBindingException {
Long value = StringUtils.isNotBlank(this.request.getParameter(paramName))
? ServletRequestUtils.getRequiredLongParameter(request, paramName)
: null;
if (value != null)
args.put(paramName, value);
return this;
}

public CriteriaArgsBuilder addNonZeroLong(String paramName) throws ServletRequestBindingException {
Long value = StringUtils.isNotBlank(this.request.getParameter(paramName))
? ServletRequestUtils.getRequiredLongParameter(request, paramName)
: null;
if (value != null && value.longValue() != 0L)
args.put(paramName, value);
return this;
}

public CriteriaArgsBuilder addDatetime(String paramName) throws ServletRequestBindingException {
String value = ServletRequestUtils.getStringParameter(request, paramName);
if (StringUtils.isNotBlank(value)) {
try {
args.put(paramName, LocalDateTime.parse(value));
} catch (DateTimeParseException e) {
throw new ServletRequestBindingException(paramName);
}
}
return this;
}

public CriteriaArgsBuilder addDatetime(String paramName, DateTimeFormatter formatter)
throws ServletRequestBindingException {
String value = ServletRequestUtils.getStringParameter(request, paramName);
if (StringUtils.isNotBlank(value)) {
try {
args.put(paramName, LocalDateTime.parse(value, formatter));
} catch (DateTimeParseException e) {
throw new ServletRequestBindingException(paramName);
}
}
return this;
}

public CriteriaArgsBuilder addDate(String paramName) throws ServletRequestBindingException {
String value = ServletRequestUtils.getStringParameter(request, paramName);
if (StringUtils.isNotBlank(value)) {
try {
args.put(paramName, LocalDate.parse(value));
} catch (DateTimeParseException e) {
throw new ServletRequestBindingException(paramName);
}
}
return this;
}

public CriteriaArgsBuilder addDate(String paramName, DateTimeFormatter formatter)
throws ServletRequestBindingException {
String value = ServletRequestUtils.getStringParameter(request, paramName);
if (StringUtils.isNotBlank(value)) {
try {
args.put(paramName, LocalDate.parse(value, formatter));
} catch (DateTimeParseException e) {
throw new ServletRequestBindingException(paramName);
}
}
return this;
}

public CriteriaArgsBuilder addDateTo(String paramName) throws ServletRequestBindingException {
String value = ServletRequestUtils.getStringParameter(request, paramName);
if (StringUtils.isNotBlank(value)) {
try {
args.put(paramName, LocalDate.parse(value).plusDays(1));
} catch (DateTimeParseException e) {
throw new ServletRequestBindingException(paramName);
}
}
return this;
}

public CriteriaArgsBuilder addDateTo(String paramName, DateTimeFormatter formatter)
throws ServletRequestBindingException {
String value = ServletRequestUtils.getStringParameter(request, paramName);
if (StringUtils.isNotBlank(value)) {
try {
args.put(paramName, LocalDate.parse(value, formatter).plusDays(1));
} catch (DateTimeParseException e) {
throw new ServletRequestBindingException(paramName);
}
}
return this;
}

public CriteriaArgsBuilder addBoolean(String paramName) throws ServletRequestBindingException {

if (request.getParameter(paramName) == null || request.getParameter(paramName).isEmpty()) {
return this;
}
Boolean value = ServletRequestUtils.getBooleanParameter(request, paramName);
args.put(paramName, value);
return this;
}

public CriteriaArgsBuilder put(String key, Object value) {
args.put(key, value);
return this;
}

public Map<String, Object> build() {
return this.args;
}
}

+ 778
- 0
src/main/java/com/ffii/core/utils/ExcelUtils.java Vedi File

@@ -0,0 +1,778 @@
package com.ffii.core.utils;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.poifs.crypt.EncryptionInfo;
import org.apache.poi.poifs.crypt.EncryptionMode;
import org.apache.poi.poifs.crypt.Encryptor;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.DateUtil;
import org.apache.poi.ss.usermodel.RichTextString;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;

import jakarta.servlet.http.HttpServletResponse;

public abstract class ExcelUtils {

/**
* static A to Z char array
*/
private static final char[] A2Z = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
'U', 'V', 'W', 'X', 'Y', 'Z' };

private static final DataFormatter DATA_FORMATTER = new DataFormatter();

/** max rows limit of .xls **/
public static final int MAX_ROWS = 65536;
/** max columns limit of .xls **/
public static final int MAX_COLS = 256;

/**
* Column reference to index (0-based) map, support up to 256 columns (compatible with .xls format)
*/
public static final Map<String, Integer> COL_IDX = new HashMap<String, Integer>(MAX_COLS, 1.0f);

static {
for (int columnIndex = 0; columnIndex < MAX_COLS; columnIndex++) {
int tempColumnCount = columnIndex;
StringBuilder sb = new StringBuilder(2);
do {
sb.insert(0, A2Z[tempColumnCount % 26]);
tempColumnCount = (tempColumnCount / 26) - 1;
} while (tempColumnCount >= 0);
COL_IDX.put(sb.toString(), Integer.valueOf(columnIndex));
}
}

/**
* Load XSSF workbook (xlsx file) from template source.
*
* @param url
* the relative path to the template source, e.g. "WEB-INF/excel/exampleReportTemplate.xlsx"
*
* @return the workbook, or null if the template file cannot be loaded
*/
public static final Workbook loadXSSFWorkbookFromTemplateSource(ResourceLoader resourceLoader, String url) {
Resource resource = resourceLoader.getResource(url);
try {
return new XSSFWorkbook(resource.getInputStream());
} catch (IOException e) {
return null;
}
}

/**
* Write the workbook to byte array.
*
* @param workbook
* The Excel workbook (cannot be null)
*
* @return the byte[], or null if IO exception occurred
*/
public static final byte[] toByteArray(Workbook workbook) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
workbook.write(baos);
} catch (IOException e) {
return null;
}
return baos.toByteArray();
}

/**
* Check if the cell exists in the given sheet, row and column.
*
* @param sheet
* the Sheet (cannot be null)
* @param rowIndex
* 0-based row index
* @param colIndex
* 0-based column index
*
* @return {@code true} if cell exists, else {@code false}
*/
public static final boolean isCellExists(Sheet sheet, int rowIndex, int colIndex) {
Row row = sheet.getRow(rowIndex);
if (row != null) {
Cell cell = row.getCell(colIndex);
return cell != null;
}
return false;
}

/**
* Convenient method to obtain the cell in the given sheet, row and column.
* <p>
* Creates the row and the cell if not already exist.
*
* @param sheet
* the Sheet (cannot be null)
* @param rowIndex
* 0-based row index
* @param colIndex
* 0-based column index
*
* @return the Cell (never null)
*/
public static final Cell getCell(Sheet sheet, int rowIndex, int colIndex) {
Row row = sheet.getRow(rowIndex);
if (row == null) {
row = sheet.createRow(rowIndex);
}
Cell cell = row.getCell(colIndex);
if (cell == null) {
cell = row.createCell(colIndex);
}
return cell;
}

/**
* Get column index by column reference (support up to 256 columns)
*
* @param columnRef
* column reference such as "A", "B", "AA", "AB"...
*
* @return the column index
*
* @throws NullPointerException
* if column reference is invalid or the index exceeds 256
*/
public static final int getColumnIndex(String columnRef) {
return COL_IDX.get(columnRef);
}

/**
* Get column reference by column index
*
* @param columnIndex
* 0-based column index
*
* @return the column reference such as "A", "B", "AA", "AB"...
*/
public static final String getColumnRef(int columnIndex) {
StringBuilder sb = new StringBuilder();
int tempColumnCount = columnIndex;
do {
sb.insert(0, A2Z[tempColumnCount % 26]);
tempColumnCount = (tempColumnCount / 26) - 1;
} while (tempColumnCount >= 0);
return sb.toString();
}

/**
* Get the Excel Cell Ref String by columnIndex and rowIndex
*
* @param columnIndex
* 0-based column index
* @param rowIndex
* 0-based row index
*/
public static final String getCellRefString(int columnIndex, int rowIndex) {
StringBuilder sb = new StringBuilder();
int tempColumnCount = columnIndex;
do {
sb.insert(0, A2Z[tempColumnCount % 26]);
tempColumnCount = (tempColumnCount / 26) - 1;
} while (tempColumnCount >= 0);
sb.append(rowIndex + 1);
return sb.toString();
}

/**
* Get Cell value as <code>String</code>
*/
public static String getStringValue(Cell cell) {
if (cell != null && cell.getCellType() == CellType.FORMULA) {
try {
return cell.getStringCellValue();
} catch (Exception e) {
return "";
}
}
return DATA_FORMATTER.formatCellValue(cell);
}

/**
* Get Cell value as <code>BigDecimal</code>, with a fallback value
* <p>
* Only support {@link CellType#NUMERIC} and {@link CellType#STRING}
*
* @return the <code>BigDecimal</code> value, or the default value if cell is <code>null</code> or cell type is {@link CellType#BLANK}
*/
public static BigDecimal getDecimalValue(Cell cell, BigDecimal defaultValue) {
if (cell == null || cell.getCellType() == CellType.BLANK) return defaultValue;
if (cell.getCellType() == CellType.STRING) {
return new BigDecimal(cell.getStringCellValue());
} else {
return BigDecimal.valueOf(cell.getNumericCellValue());
}
}

/**
* Get Cell value as <code>BigDecimal</code>
* <p>
* Only support {@link CellType#NUMERIC} and {@link CellType#STRING}
*
* @return the <code>BigDecimal</code> value, or <code>BigDecimal.ZERO</code> if cell is <code>null</code> or cell type is {@link CellType#BLANK}
*/
public static BigDecimal getDecimalValue(Cell cell) {
return getDecimalValue(cell, BigDecimal.ZERO);
}

/**
* Get Cell value as <code>double</code>
* <p>
* Only support {@link CellType#NUMERIC} and {@link CellType#STRING}
*/
public static double getDoubleValue(Cell cell) {
if (cell == null) return 0.0;
if (cell.getCellType() == CellType.STRING) {
return NumberUtils.toDouble(cell.getStringCellValue());
} else {
return cell.getNumericCellValue();
}
}

/**
* Get Cell value as <code>int</code> (rounded half-up to the nearest integer)
* <p>
* Only support {@link CellType#NUMERIC} and {@link CellType#STRING}
*/
public static int getIntValue(Cell cell) {
return BigDecimal.valueOf(getDoubleValue(cell)).setScale(0, RoundingMode.HALF_UP).intValue();
}

/**
* Get Cell Integer value (truncated)
*/
public static Integer getIntValue(Cell cell, Integer defaultValue) {
if (cell == null) return defaultValue;
if (cell.getCellType() == CellType.STRING) {
return NumberUtils.toInt(cell.getStringCellValue(), defaultValue);
} else {
return (int) cell.getNumericCellValue();
}
}

public static LocalDate getDateValue(Cell cell, DateTimeFormatter formatter) {
if (cell == null) return null;
if (cell.getCellType() == CellType.STRING) {
try {
return LocalDate.parse(cell.getStringCellValue(), formatter);
} catch (DateTimeParseException e) {
return null;
}
}
if (DateUtil.isCellDateFormatted(cell)) {
try {
return DateUtil.getJavaDate(cell.getNumericCellValue()).toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDate();
} catch (NumberFormatException e) {
return null;
}
} else {
return null;
}
}

public static LocalDateTime getDatetimeValue(Cell cell, DateTimeFormatter formatter) {
if (cell == null) return null;
if (cell.getCellType() == CellType.STRING) {
try {
return LocalDateTime.parse(cell.getStringCellValue(), formatter);
} catch (DateTimeParseException e) {
return null;
}
}
if (DateUtil.isCellDateFormatted(cell)) {
try {
return DateUtil.getJavaDate(cell.getNumericCellValue()).toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
} catch (NumberFormatException e) {
return null;
}
} else {
return null;
}
}

/**
* Convenient method to set Cell value
*
* @param cell
* the Cell (cannot be null)
* @param value
* the value to set
*/
public static void setCellValue(Cell cell, Object value) {
if (value instanceof String)
cell.setCellValue((String) value);
else if (value instanceof RichTextString)
cell.setCellValue((RichTextString) value);
else if (value instanceof Number)
cell.setCellValue(((Number) value).doubleValue());
else if (value instanceof Boolean)
cell.setCellValue(((Boolean) value).booleanValue());
else if (value instanceof Calendar)
cell.setCellValue((Calendar) value);
else if (value instanceof Date)
cell.setCellValue((Date) value);
else if (value instanceof LocalDate)
cell.setCellValue((LocalDate) value);
else if (value instanceof LocalTime)
cell.setCellValue(((LocalTime) value).toString());
else if (value instanceof LocalDateTime)
cell.setCellValue((LocalDateTime) value);
else if (value == null)
cell.setCellValue("");
else
throw new IllegalArgumentException(value.getClass().toString() + " is not supported");
}

/**
* Convenient method to set Cell value by Sheet, row index, and column index
*
* @param sheet
* the Sheet (cannot be null)
* @param rowIndex
* 0-based row index
* @param colIndex
* 0-based column index
* @param value
* the value to set
*/
public static void setCellValue(Sheet sheet, int rowIndex, int colIndex, Object value) {
setCellValue(getCell(sheet, rowIndex, colIndex), value);
}

/**
* Increase Row Height (if necessary, but never decrease it) by counting the no. of lines in a String value
*
* @param sheet
* The Excel worksheet
* @param row
* The row index (0-based)
* @param value
* The (multi-line) String value to count for the no. of lines
* @param heightInPoints
* The height (in points) for 1 line of text
*/
public static void increaseRowHeight(Sheet sheet, int row, String value, int heightInPoints) {
int lines = StringUtils.countMatches(value, "\n") + 1; // count no. of lines
float newHeight = heightInPoints * lines;

Row r = sheet.getRow(row);
if (r == null) r = sheet.createRow(row);

// increase the row height if necessary, but never decrease it
if (r.getHeightInPoints() < newHeight) {
r.setHeightInPoints(newHeight);
}
}

/**
* Add merged region (i.e. merge cells)
*
* @param sheet
* The Excel worksheet
* @param firstRowIdx
* The first row index (0-based)
* @param lastRowIdx
* The last row index (0-based)
* @param firstColIdx
* The first column index (0-based)
* @param lastColIdx
* The last column index (0-based)
*/
public static void addMergedRegion(Sheet sheet, int firstRowIdx, int lastRowIdx, int firstColIdx, int lastColIdx) {
CellRangeAddress cellRangeAddress = new CellRangeAddress(firstRowIdx, lastRowIdx, firstColIdx, lastColIdx);
sheet.addMergedRegion(cellRangeAddress);
}

/**
* Copy and Insert Row
*
* @param workbook
* The Excel workbook
* @param sourceSheet
* The source Excel worksheet
* @param destinationSheet
* The destination Excel worksheet
* @param sourceRowNum
* The source row index (0-based) to copy from
* @param destinationRowNum
* The destination row index (0-based) to insert into (from the copied row)
*/
public static void copyAndInsertRow(Workbook workbook, Sheet sourceSheet, Sheet destinationSheet, int sourceRowNum, int destinationRowNum) {
// get the source / destination row
Row sourceRow = sourceSheet.getRow(sourceRowNum);
Row destRow = destinationSheet.getRow(destinationRowNum);

// if the row exist in destination, push down all rows by 1
if (destRow != null) {
destinationSheet.shiftRows(destinationRowNum, destinationSheet.getLastRowNum(), 1, true, false);
}
// create a new row
destRow = destinationSheet.createRow(destinationRowNum);

// loop through source columns to add to new row
for (int i = 0; i < sourceRow.getLastCellNum(); i++) {
// grab a copy of the old cell
Cell oldCell = sourceRow.getCell(i);

// if the old cell is null jump to next cell
if (oldCell == null) continue;

// create a new cell in destination row
Cell newCell = destRow.createCell(i);

// apply cell style to new cell from old cell
newCell.setCellStyle(oldCell.getCellStyle());

// if there is a cell comment, copy
if (oldCell.getCellComment() != null) {
newCell.setCellComment(oldCell.getCellComment());
}

// if there is a cell hyperlink, copy
if (oldCell.getHyperlink() != null) {
newCell.setHyperlink(oldCell.getHyperlink());
}

// copy the cell data value
switch (oldCell.getCellType()) {
case NUMERIC:
newCell.setCellValue(oldCell.getNumericCellValue());
break;
case STRING:
newCell.setCellValue(oldCell.getRichStringCellValue());
break;
case FORMULA:
newCell.setCellFormula(oldCell.getCellFormula());
break;
case BLANK:
newCell.setCellValue(oldCell.getStringCellValue());
break;
case BOOLEAN:
newCell.setCellValue(oldCell.getBooleanCellValue());
break;
case ERROR:
newCell.setCellErrorValue(oldCell.getErrorCellValue());
break;
default:
break;
}
}

// if there are any merged regions in the source row, copy to new row
for (int i = 0; i < sourceSheet.getNumMergedRegions(); i++) {
CellRangeAddress cellRangeAddress = sourceSheet.getMergedRegion(i);
if (cellRangeAddress.getFirstRow() == sourceRow.getRowNum()) {
addMergedRegion(
destinationSheet,
destRow.getRowNum(),
(destRow.getRowNum() + (cellRangeAddress.getLastRow() - cellRangeAddress.getFirstRow())),
cellRangeAddress.getFirstColumn(),
cellRangeAddress.getLastColumn());
}
}

// copy row height
destRow.setHeight(sourceRow.getHeight());
}

/**
* Copy and Insert Row
*
* @param workbook
* The Excel workbook
* @param sheet
* The Excel worksheet
* @param sourceRowNum
* The source row index (0-based) to copy from
* @param destinationRowNum
* The destination row index (0-based) to insert into (from the copied row)
*/
public static void copyAndInsertRow(Workbook workbook, Sheet sheet, int sourceRowNum, int destinationRowNum) {
copyAndInsertRow(workbook, sheet, sheet, sourceRowNum, destinationRowNum);
}

public static void copyAndInsertRow(Workbook workbook, Sheet sourceSheet, int sourceRowNum, int destinationRowNum, int times) {
// get the source / destination row
Row sourceRow = sourceSheet.getRow(sourceRowNum);

Row[] destRows = new Row[times];
for (int j = 0; j < times; j++) {
Row destRow = sourceSheet.getRow(destinationRowNum + j);
// if the row exist in destination, push down all rows by 1
if (destRow != null) {
sourceSheet.shiftRows(destinationRowNum + j, sourceSheet.getLastRowNum(), 1, true, false);
}
// create a new row
destRows[j] = sourceSheet.createRow(destinationRowNum + j);
// copy row height
destRows[j].setHeight(sourceRow.getHeight());
}

// loop through source columns to add to new row
for (int i = 0; i < sourceRow.getLastCellNum(); i++) {
// grab a copy of the old cell
Cell oldCell = sourceRow.getCell(i);

// if the old cell is null jump to next cell
if (oldCell == null) continue;

for (int k = 0; k < times; k++) {
// create a new cell in destination row
Cell newCell = destRows[k].createCell(i);

// apply cell style to new cell from old cell
newCell.setCellStyle(oldCell.getCellStyle());
}
}
}

/**
* Copy Column
*
* @param workbook
* The Excel workbook
* @param sourceSheet
* The source Excel worksheet
* @param destinationSheet
* The destination Excel worksheet
* @param rowStart
* The source row start index (0-based) to copy from
* @param rowEnd
* The source row end index (0-based) to copy from
* @param sourceColumnNum
* The source column index (0-based) to copy from
* @param destinationColumnNum
* The destination column index (0-based) to copy into (from the copied row)
*/
public static void copyColumn(Workbook workbook, Sheet sourceSheet, Sheet destinationSheet, int rowStart, int rowEnd, int sourceColumnNum,
int destinationColumnNum) {
for (int i = rowStart; i <= rowEnd; i++) {
Row sourceRow = sourceSheet.getRow(i);
if (sourceRow == null) continue;

Row destinationRow = destinationSheet.getRow(i);
if (destinationRow == null) destinationRow = destinationSheet.createRow(i);

Cell oldCell = sourceRow.getCell(sourceColumnNum);
if (oldCell == null) continue;

Cell newCell = destinationRow.createCell(destinationColumnNum);

newCell.setCellStyle(oldCell.getCellStyle());

if (oldCell.getCellComment() != null) {
newCell.setCellComment(oldCell.getCellComment());
}

if (oldCell.getHyperlink() != null) {
newCell.setHyperlink(oldCell.getHyperlink());
}

switch (oldCell.getCellType()) {
case NUMERIC:
newCell.setCellValue(oldCell.getNumericCellValue());
break;
case STRING:
newCell.setCellValue(oldCell.getRichStringCellValue());
break;
case FORMULA:
newCell.setCellFormula(oldCell.getCellFormula());
break;
case BLANK:
newCell.setCellValue(oldCell.getStringCellValue());
break;
case BOOLEAN:
newCell.setCellValue(oldCell.getBooleanCellValue());
break;
case ERROR:
newCell.setCellErrorValue(oldCell.getErrorCellValue());
break;
default:
break;
}

for (int ii = 0; ii < sourceSheet.getNumMergedRegions(); ii++) {
CellRangeAddress cellRangeAddress = sourceSheet.getMergedRegion(ii);
if (cellRangeAddress.getFirstRow() == sourceRow.getRowNum()) {
addMergedRegion(
destinationSheet,
cellRangeAddress.getFirstRow(),
cellRangeAddress.getLastRow(),
destinationColumnNum,
(destinationColumnNum + (cellRangeAddress.getLastColumn() - cellRangeAddress.getFirstColumn())));
}
}
}

destinationSheet.setColumnWidth(destinationColumnNum, sourceSheet.getColumnWidth(sourceColumnNum));
}

/**
* Copy Column
*
* @param workbook
* The Excel workbook
* @param sheet
* The Excel worksheet
* @param rowStart
* The source row start index (0-based) to copy from
* @param rowEnd
* The source row end index (0-based) to copy from
* @param sourceColumnNum
* The source column index (0-based) to copy from
* @param destinationColumnNum
* The destination column index (0-based) to copy into (from the copied row)
*/
public static void copyColumn(Workbook workbook, Sheet sheet, int rowStart, int rowEnd, int sourceColumnNum, int destinationColumnNum) {
copyColumn(workbook, sheet, sheet, rowStart, rowEnd, sourceColumnNum, destinationColumnNum);
}

public static void shiftColumns(Row row, int startingIndex, int shiftCount) {
for (int i = row.getPhysicalNumberOfCells() - 1; i >= startingIndex; i--) {
Cell oldCell = row.getCell(i);
Cell newCell = row.createCell(i + shiftCount);

// apply cell style to new cell from old cell
newCell.setCellStyle(oldCell.getCellStyle());

// if there is a cell comment, copy
if (oldCell.getCellComment() != null) {
newCell.setCellComment(oldCell.getCellComment());
}

// if there is a cell hyperlink, copy
if (oldCell.getHyperlink() != null) {
newCell.setHyperlink(oldCell.getHyperlink());
}

// copy the cell data value
switch (oldCell.getCellType()) {
case NUMERIC:
newCell.setCellValue(oldCell.getNumericCellValue());
break;
case STRING:
newCell.setCellValue(oldCell.getRichStringCellValue());
break;
case FORMULA:
newCell.setCellFormula(oldCell.getCellFormula());
break;
case BLANK:
newCell.setCellValue(oldCell.getStringCellValue());
break;
case BOOLEAN:
newCell.setCellValue(oldCell.getBooleanCellValue());
break;
case ERROR:
newCell.setCellErrorValue(oldCell.getErrorCellValue());
break;
default:
break;
}
}
}

/** handle some invalid char included ( /\*[]:? ) */
public static void setSheetName(Workbook workbook, Sheet sheet, String name) {
if (workbook != null && sheet != null && StringUtils.isNotBlank(name))
workbook.setSheetName(workbook.getSheetIndex(sheet), name.replaceAll("[/\\\\*\\[\\]:\\?]", "_"));
}

/** delete row */
public static void deleteRow(Sheet sheet, int rowIndex) {
if (sheet != null) {
sheet.removeRow(sheet.getRow(rowIndex));
if (rowIndex < sheet.getLastRowNum())
sheet.shiftRows(rowIndex, sheet.getLastRowNum(), -1);
}
}

public static byte[] encrypt(Workbook workbook, String password) {
return encrypt(toByteArray(workbook), password);
}

public static byte[] encrypt(byte[] bytes, String password) {
try {
POIFSFileSystem fs = new POIFSFileSystem();
EncryptionInfo info = new EncryptionInfo(EncryptionMode.agile);
// EncryptionInfo info = new EncryptionInfo(EncryptionMode.agile, CipherAlgorithm.aes192, HashAlgorithm.sha384, -1, -1, null);

Encryptor enc = info.getEncryptor();
enc.confirmPassword(password);

// Read in an existing OOXML file and write to encrypted output stream
// don't forget to close the output stream otherwise the padding bytes aren't added
OPCPackage opc = OPCPackage.open(new ByteArrayInputStream(bytes));
OutputStream os = enc.getDataStream(fs);
opc.save(os);

ByteArrayOutputStream bos = new ByteArrayOutputStream();
fs.writeFilesystem(bos);

return bos.toByteArray();
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public static Workbook loadTemplate(String templateClasspath) throws InvalidFormatException, IOException {
return loadTemplateFile(templateClasspath);
}

public static Workbook loadTemplateFile(String templateClasspath) throws InvalidFormatException, IOException {
ClassPathResource r = new ClassPathResource(templateClasspath + "_" + ".xlsx");
if (!r.exists()) r = new ClassPathResource(templateClasspath + ".xlsx");

try (InputStream in = r.getInputStream()) {
return new XSSFWorkbook(in);
}
}

public static void send(HttpServletResponse response, Workbook workbook, String filename) throws IOException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", String.format("attachment; filename=\"%s\"",
response.encodeURL(filename + ".xlsx")));
try (OutputStream out = response.getOutputStream()) {
workbook.write(out);
}
}
}

+ 47
- 0
src/main/java/com/ffii/core/utils/JsonUtils.java Vedi File

@@ -0,0 +1,47 @@
package com.ffii.core.utils;

import java.io.IOException;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

/**
* JSON Utils
*
* @author Patrick
*/
public abstract class JsonUtils {

// Default mapper instance
private static final ObjectMapper mapper = new ObjectMapper();
/**
* Method that can be used to serialize any Java value as a JSON String.
*/
public static String toJsonString(Object obj) {
try {
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
return null;
}
}

/**
* Read from JSON String.
*
* @param content
* JSON String content
* @param valueType
* the return type
*/
public static <T> T fromJsonString(String content, Class<T> valueType) throws JsonParseException, JsonMappingException, IOException {
return mapper.readValue(content, valueType);
}

}

+ 114
- 0
src/main/java/com/ffii/core/utils/JwtTokenUtil.java Vedi File

@@ -0,0 +1,114 @@
package com.ffii.core.utils;

import java.io.Serializable;
import java.security.Key;
import java.time.Instant;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import com.ffii.tsms.model.RefreshToken;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;

@Component
@Scope(value = ConfigurableBeanFactory. SCOPE_SINGLETON)
public class JwtTokenUtil implements Serializable {

Logger logger = LoggerFactory.getLogger(JwtTokenUtil.class);

private static final long serialVersionUID = -2550185165626007488L;

// * 60000 = 1 Min
public static final long JWT_TOKEN_EXPIRED_TIME = 60000 * 14400;
public static final String AES_SECRET = "ffii";
public static final String TOKEN_SEPARATOR = "@@";

// @Value("${jwt.secret}")
// private String secret;

private static final Key secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS512);

// retrieve username from jwt token
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}

// retrieve expiration date from jwt token
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}

public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}

// for retrieveing any information from token we will need the secret key
private Claims getAllClaimsFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody();
}

// check if the token has expired
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}

// generate token for user
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return doGenerateToken(claims, userDetails.getUsername());
}

// while creating the token -
// 1. Define claims of the token, like Issuer, Expiration, Subject, and the ID
// 2. Sign the JWT using the HS512 algorithm and secret key.
// 3. According to JWS Compact
// Serialization(https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1)
// compaction of the JWT to a URL-safe string
private String doGenerateToken(Map<String, Object> claims, String subject) {
logger.info((new Date(System.currentTimeMillis() + JWT_TOKEN_EXPIRED_TIME)).toString());
return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_EXPIRED_TIME))
.signWith(secretKey).compact();
}

// validate token
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}

public RefreshToken createRefreshToken(String username) {
RefreshToken refreshToken = new RefreshToken();
refreshToken.setUserName(username);
refreshToken.setExpiryDate(Instant.now().plusMillis(JWT_TOKEN_EXPIRED_TIME * 60 * 24));
long instantNum = Instant.now().plusMillis(JWT_TOKEN_EXPIRED_TIME * 60 * 24).toEpochMilli();
refreshToken.setToken(AES.encrypt(username + TOKEN_SEPARATOR + instantNum, AES_SECRET));
return refreshToken;
}

public boolean verifyExpiration(RefreshToken token) throws Exception {
if (token.getExpiryDate().compareTo(Instant.now()) < 0) {
return false;
}

return true;
}

public String getUsernameFromRefreshToken(String refreshToken) {
return AES.decrypt(refreshToken, AES_SECRET);
}
}

+ 35
- 0
src/main/java/com/ffii/core/utils/MapUtils.java Vedi File

@@ -0,0 +1,35 @@
package com.ffii.core.utils;

import java.util.HashMap;
import java.util.Map;

/**
* MapUtils
*
* @author Patrick
*/
public class MapUtils {

/**
* Convert key-value pairs to HashMap
*
* @param keyValuePairs
* Keys and values must be in pairs
*
* @return Map
*/
@SuppressWarnings("unchecked")
public static <K, V> Map<K, V> toHashMap(Object... keyValuePairs) {
if (keyValuePairs.length % 2 != 0)
throw new IllegalArgumentException("Keys and values must be in pairs");

Map<K, V> map = new HashMap<K, V>(keyValuePairs.length / 2);

for (int i = 0; i < keyValuePairs.length; i += 2) {
map.put((K) keyValuePairs[i], (V) keyValuePairs[i + 1]);
}

return map;
}

}

+ 42
- 0
src/main/java/com/ffii/core/utils/Params.java Vedi File

@@ -0,0 +1,42 @@
package com.ffii.core.utils;

/** @author Alex */
public abstract class Params {
public static final String ERROR = "error";

public static final String SUCCESS = "success";
public static final String DATA = "data";
public static final String RECORDS = "records";
public static final String TOTAL = "total";

public static final String ID = "id";
public static final String CODE = "code";
public static final String NAME = "name";
public static final String TYPE = "type";
public static final String MSG = "msg";
public static final String MSG_CODE = "msgCode";
public static final String MESSAGES = "messages";
public static final String FROM = "from";
public static final String TO = "to";

// sql
public static final String QUERY = "query";

// pagin
public static final String PAGE = "page";
public static final String START = "start";
public static final String LIMIT = "limit";

// filter
public static final String FILTER = "filter";
public static final String OPERATOR = "operator";
public static final String LIKE = "like";
public static final String PROPERTY = "property";

// sort
public static final String SORT = "sort";
public static final String DIRECTION = "direction";

public static final String VALUE = "value";

}

+ 83
- 0
src/main/java/com/ffii/core/utils/PasswordUtils.java Vedi File

@@ -0,0 +1,83 @@
/*******************************************************************************
* Copyright 2Fi Business Solutions Ltd.
*
* This code is copyrighted. Under no circumstances should any party, people,
* or organization should redistribute any portions of this code in any form,
* either verbatim or through electronic media, to any third parties, unless
* under explicit written permission by 2Fi Business Solutions Ltd.
******************************************************************************/
package com.ffii.core.utils;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.regex.Pattern;

public abstract class PasswordUtils {

private static final Pattern PATTERN_DIGITS = Pattern.compile("[0-9]");
private static final Pattern PATTERN_A2Z_LOWER = Pattern.compile("[a-z]");
private static final Pattern PATTERN_A2Z_UPPER = Pattern.compile("[A-Z]");

private static final String A2Z_LOWER = "abcdefghijklmnopqrstuvwxyz";
private static final String A2Z_UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final String DIGITS = "0123456789";

/*
* Ref: https://www.owasp.org/index.php/Password_special_characters
* without space character
*/
private static final String SPECIAL_CHARS = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
private static Pattern PATTERN_SPECIAL_CHARS = Pattern.compile("[!\"#$%&'()*+,-./:;<=>?@\\[\\\\\\]^_`{|}~]");

public static final boolean checkPwd(String pwd, IPasswordRule rule) {
if (pwd == null) return false;
if (pwd.length() < rule.getMin()) return false;
if (pwd.length() > rule.getMax()) return false;

if (rule.needNumberChar() && !PATTERN_DIGITS.matcher(pwd).find()) return false;
if (rule.needUpperEngChar() && !PATTERN_A2Z_UPPER.matcher(pwd).find()) return false;
if (rule.needLowerEngChar() && !PATTERN_A2Z_LOWER.matcher(pwd).find()) return false;
if (rule.needSpecialChar() && !PATTERN_SPECIAL_CHARS.matcher(pwd).find()) return false;

return true;
}

public static String genPwd(IPasswordRule rule) {
int length = rule.getMin();

StringBuilder password = new StringBuilder(length);
Random random = new Random(System.nanoTime());

List<String> charCategories = new ArrayList<>(4);
if (rule.needLowerEngChar()) charCategories.add(A2Z_LOWER);
if (rule.needUpperEngChar()) charCategories.add(A2Z_UPPER);
if (rule.needNumberChar()) charCategories.add(DIGITS);
if (rule.needSpecialChar()) charCategories.add(SPECIAL_CHARS);

for (int i = 0; i < length; i++) {
String charCategory = charCategories.get(i % charCategories.size());
char randomChar = charCategory.charAt(random.nextInt(charCategory.length()));
if (password.length() > 0)
password.insert(random.nextInt(password.length()), randomChar);
else
password.append(randomChar);
}

return password.toString();
}

public static interface IPasswordRule {
public int getMin();

public int getMax();

public boolean needNumberChar();

public boolean needUpperEngChar();

public boolean needLowerEngChar();

public boolean needSpecialChar();
}
}

+ 13
- 0
src/main/java/com/ffii/tsms/TsmsApplication.java Vedi File

@@ -0,0 +1,13 @@
package com.ffii.tsms;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TsmsApplication {

public static void main(String[] args) {
SpringApplication.run(TsmsApplication.class, args);
}

}

+ 36
- 0
src/main/java/com/ffii/tsms/config/AppConfig.java Vedi File

@@ -0,0 +1,36 @@
package com.ffii.tsms.config;

import javax.sql.DataSource;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

import com.ffii.core.support.JdbcDao;

/** @author Terence */
@Configuration
// @EnableJpaRepositories("com.ffii.ars.*")
// @ComponentScan(basePackages = { "com.ffii.core.*" })
@ComponentScan(basePackages = { "com.ffii.core.*","com.ffii.tsms.*"})
// @EntityScan("com.ffii.ars.*")
@EnableScheduling
@EnableAsync
public class AppConfig {

@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}

@Bean
public JdbcDao jdbcDao(DataSource dataSource) {
return new JdbcDao(dataSource);
}
}

+ 29
- 0
src/main/java/com/ffii/tsms/config/WebConfig.java Vedi File

@@ -0,0 +1,29 @@
package com.ffii.tsms.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedHeaders("*")
.allowedOrigins("*")
.exposedHeaders("filename")
.allowedMethods("GET", "POST", "PUT", "DELETE", "HEAD");

}

@Bean
public InternalResourceViewResolver defaultViewResolver() {
return new InternalResourceViewResolver();
}

}

+ 80
- 0
src/main/java/com/ffii/tsms/config/security/SecurityConfig.java Vedi File

@@ -0,0 +1,80 @@
package com.ffii.tsms.config.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.ldap.core.support.BaseLdapPathContextSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.config.ldap.LdapBindAuthenticationManagerFactory;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.ffii.tsms.config.security.jwt.JwtRequestFilter;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

public static final String INDEX_URL = "/";
public static final String LOGIN_URL = "/login";
public static final String LDAP_LOGIN_URL = "/ldap-login";

public static final String[] URL_WHITELIST = {
INDEX_URL,
LOGIN_URL,
LDAP_LOGIN_URL
};

@Lazy
@Autowired
private JwtRequestFilter jwtRequestFilter;

@Bean
@Qualifier("AuthenticationManager")
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}

@Bean
@Qualifier("LdapAuthenticationManager")
public AuthenticationManager ldapAuthenticationManager(BaseLdapPathContextSource contextSource) {
LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource);
factory.setUserSearchFilter("cn={0}");
return factory.createAuthenticationManager();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
@Order(1)
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.cors(Customizer.withDefaults()).csrf(csrf -> csrf.disable())
.requestCache(requestCache -> requestCache.disable())
.authorizeHttpRequests(
authRequest -> authRequest.requestMatchers(URL_WHITELIST).permitAll().anyRequest().authenticated())
.httpBasic(httpBasic -> httpBasic.authenticationEntryPoint(
(request, response, authException) -> response.sendError(HttpStatus.UNAUTHORIZED.value())))
.sessionManagement(
sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}

+ 75
- 0
src/main/java/com/ffii/tsms/config/security/jwt/JwtRequestFilter.java Vedi File

@@ -0,0 +1,75 @@
package com.ffii.tsms.config.security.jwt;

import java.io.IOException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import com.ffii.core.utils.JwtTokenUtil;
import com.ffii.tsms.config.security.jwt.service.JwtUserDetailsService;

import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

@Autowired
private JwtUserDetailsService jwtUserDetailsService;

@Autowired
private JwtTokenUtil jwtTokenUtil;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {

final String requestTokenHeader = request.getHeader("Authorization");

String username = null;
String jwtToken = null;
// JWT Token is in the form "Bearer token". Remove Bearer word and get
// only the Token
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7).replaceAll("\"", "");
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
logger.error("Unable to get JWT Token");
} catch (ExpiredJwtException e) {
logger.error("JWT Token has expired");
}
} else {
logger.warn("JWT Token does not begin with Bearer String");
}

// Once we get the token validate it.
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

UserDetails userDetails = jwtUserDetailsService.loadUserByUsername(username);

// if token is valid configure Spring Security to manually set
// authentication
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {

UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// After setting the Authentication in the context, we specify
// that the current user is authenticated. So it passes the
// Spring Security Configurations successfully.
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}

}

+ 31
- 0
src/main/java/com/ffii/tsms/config/security/jwt/service/JwtUserDetailsService.java Vedi File

@@ -0,0 +1,31 @@
package com.ffii.tsms.config.security.jwt.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.ffii.tsms.modules.user.entity.User;
import com.ffii.tsms.modules.user.entity.UserRepository;
import com.ffii.tsms.modules.user.service.UserAuthorityService;
import com.ffii.tsms.modules.user.service.UserService;

@Service
public class JwtUserDetailsService implements UserDetailsService {

@Autowired
UserRepository userRepository;

@Autowired
UserAuthorityService userAuthService;

@Autowired
UserService userService;


@Override
public User loadUserByUsername(String username) throws UsernameNotFoundException {
return userService.loadUserOptByUsername(username).orElseThrow(() -> new UsernameNotFoundException(username));
}

}

+ 151
- 0
src/main/java/com/ffii/tsms/config/security/jwt/web/JwtAuthenticationController.java Vedi File

@@ -0,0 +1,151 @@
package com.ffii.tsms.config.security.jwt.web;

import java.time.Instant;
import java.util.HashSet;
import java.util.Set;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

import com.ffii.core.utils.AES;
import com.ffii.core.utils.JwtTokenUtil;
import com.ffii.tsms.config.security.jwt.service.JwtUserDetailsService;
import com.ffii.tsms.config.security.service.LoginLogService;
import com.ffii.tsms.model.AbilityModel;
import com.ffii.tsms.model.ExceptionResponse;
import com.ffii.tsms.model.JwtRequest;
import com.ffii.tsms.model.JwtResponse;
import com.ffii.tsms.model.RefreshToken;
import com.ffii.tsms.model.TokenRefreshRequest;
import com.ffii.tsms.model.TokenRefreshResponse;
import com.ffii.tsms.modules.common.SecurityUtils;
import com.ffii.tsms.modules.user.entity.User;
import com.ffii.tsms.modules.user.entity.UserRepository;
import com.ffii.tsms.modules.user.service.UserAuthorityService;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;

@RestController
@CrossOrigin(origins = "*", allowedHeaders = "*")
public class JwtAuthenticationController {

@Autowired
@Qualifier("AuthenticationManager")
private AuthenticationManager authenticationManager;

@Autowired
@Qualifier("LdapAuthenticationManager")
private AuthenticationManager ldapAuthenticationManager;

@Autowired
private JwtTokenUtil jwtTokenUtil;

@Autowired
private JwtUserDetailsService userDetailsService;

@Autowired
private UserRepository userRepository;

@Autowired
UserAuthorityService userAuthorityService;

@Autowired
LoginLogService loginLogService;

@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody JwtRequest authenticationRequest, HttpServletRequest request) throws Exception {
String username = authenticationRequest.getUsername();
try {
boolean success = authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
loginLogService.createLoginLog(username, request.getRemoteAddr(), success);
} catch (Exception e) {
if (username != null) {
loginLogService.createLoginLog(username, request.getRemoteAddr(), false);
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ExceptionResponse("Unauthorized", ExceptionUtils.getStackTrace(e)));
}
return createAuthTokenResponse(authenticationRequest);
}

@PostMapping("/ldap-login")
public ResponseEntity<?> ldapLogin(@RequestBody JwtRequest authenticationRequest, HttpServletRequest request) throws Exception {
String username = authenticationRequest.getUsername();
try {
boolean success = ldapAuthenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
loginLogService.createLoginLog(username, request.getRemoteAddr(), success);
} catch (Exception e) {
loginLogService.createLoginLog(username, request.getRemoteAddr(), false);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ExceptionResponse("Unauthorized", ExceptionUtils.getStackTrace(e)));
}
return createAuthTokenResponse(authenticationRequest);
}

private boolean authenticate(String username, String password) throws Exception {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
return true;
}

private boolean ldapAuthenticate(String username, String password) throws Exception {
ldapAuthenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
return true;
}

private ResponseEntity<?> createAuthTokenResponse(JwtRequest authenticationRequest) {
final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
if (userDetails == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ExceptionResponse(authenticationRequest.getUsername() + " not yet register in the system.", null));
}

final String accessToken = jwtTokenUtil.generateToken(userDetails);
final String refreshToken = jwtTokenUtil.createRefreshToken(userDetails.getUsername()).getToken();

User user = userRepository.findByName(authenticationRequest.getUsername()).get(0);

Set<AbilityModel> abilities = new HashSet<>();
userAuthorityService.getUserAuthority(user).forEach(auth -> abilities.add(new AbilityModel(auth.getAuthority())));

return ResponseEntity.ok(new JwtResponse(accessToken, refreshToken, null, user, abilities));
}

@PostMapping("/refresh-token")
public ResponseEntity<TokenRefreshResponse> refreshtoken(@Valid @RequestBody TokenRefreshRequest request)
throws Exception {
String requestRefreshToken = request.getRefreshToken();

requestRefreshToken = requestRefreshToken.replaceAll("\"", "");
String[] decryptStringList = AES.decrypt(requestRefreshToken, JwtTokenUtil.AES_SECRET)
.split(JwtTokenUtil.TOKEN_SEPARATOR);
RefreshToken instance = new RefreshToken();
String username = decryptStringList[0];
instance.setExpiryDate(Instant.ofEpochMilli(Long.valueOf(decryptStringList[1])));
instance.setToken(requestRefreshToken);
instance.setUserName(decryptStringList[0]);

if (!jwtTokenUtil.verifyExpiration(instance)) {
throw new ResponseStatusException(HttpStatus.EXPECTATION_FAILED,
"Refresh token was expired. Please make a new signin request");
}

final UserDetails userDetails = userDetailsService.loadUserByUsername(username);

String accessToken = jwtTokenUtil.generateToken(userDetails);
String refreshToken = jwtTokenUtil.createRefreshToken(username).getToken();
return ResponseEntity.ok(new TokenRefreshResponse(accessToken, refreshToken));
}

}

+ 43
- 0
src/main/java/com/ffii/tsms/config/security/service/LoginLogService.java Vedi File

@@ -0,0 +1,43 @@
package com.ffii.tsms.config.security.service;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

import com.ffii.core.support.AbstractService;
import com.ffii.core.support.JdbcDao;
import com.ffii.core.utils.MapUtils;

@Service
public class LoginLogService extends AbstractService {

public LoginLogService(JdbcDao jdbcDao) {
super(jdbcDao);
}

@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = false)
public boolean createLoginLog(String username, String remoteAddr, boolean success) {
String sql = "INSERT INTO user_login_log (`username`, `loginTime`, `ipAddr`, `success`) "
+"VALUES (:username, :loginTime, :ipAddr, :success)";
Map<String, Object> args = new HashMap<>(4);
args.put("username", username);
args.put("loginTime", new Date());
args.put("ipAddr", remoteAddr);
args.put("success", success);

return (jdbcDao.executeUpdate(sql, args) == 1);
}

@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = true)
public List<Map<String, Object>> listLastLog(String username, int limit) {
return jdbcDao.queryForList("SELECT success FROM user_login_log where username = :username ORDER BY loginTime DESC LIMIT " + limit,
MapUtils.toHashMap("username", username));
}

}

+ 13
- 0
src/main/java/com/ffii/tsms/model/AbilityModel.java Vedi File

@@ -0,0 +1,13 @@
package com.ffii.tsms.model;

public class AbilityModel {

private final String actionSubjectCombo;
public AbilityModel(String actionSubjectCombo) {
this.actionSubjectCombo = actionSubjectCombo;
}

public String getActionSubjectCombo() {
return actionSubjectCombo;
}
}

+ 28
- 0
src/main/java/com/ffii/tsms/model/ExceptionResponse.java Vedi File

@@ -0,0 +1,28 @@
package com.ffii.tsms.model;

public class ExceptionResponse {
private String message;
private String exception;

public ExceptionResponse(String message, String exception) {
this.message = message;
this.exception = exception;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}

public String getException() {
return exception;
}

public void setException(String exception) {
this.exception = exception;
}

}

+ 40
- 0
src/main/java/com/ffii/tsms/model/JwtRequest.java Vedi File

@@ -0,0 +1,40 @@
package com.ffii.tsms.model;


import java.io.Serializable;

public class JwtRequest implements Serializable {

private static final long serialVersionUID = 5926468583005150707L;
private String username;
private String password;
//need default constructor for JSON Parsing
public JwtRequest()
{
}

public JwtRequest(String username, String password) {
this.setUsername(username);
this.setPassword(password);
}

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

}

+ 56
- 0
src/main/java/com/ffii/tsms/model/JwtResponse.java Vedi File

@@ -0,0 +1,56 @@
package com.ffii.tsms.model;

import java.io.Serializable;
import java.util.Set;

import com.ffii.tsms.modules.user.entity.User;

public class JwtResponse implements Serializable {

private static final long serialVersionUID = -8091879091924046844L;
private final Long id;
private final String name;
private final String email;
private final String accessToken;
private final String refreshToken;
private final String role;
private final Set<AbilityModel> abilities;

public JwtResponse(String accessToken, String refreshToken, String role, User user, Set<AbilityModel> abilities) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.role = role;
this.id = user.getId();
this.name = user.getName();
this.email = user.getEmail();
this.abilities = abilities;
}

public String getAccessToken() {
return this.accessToken;
}

public String getRole() {
return role;
}

public String getRefreshToken() {
return refreshToken;
}

public Long getId() {
return id;
}

public String getName() {
return name;
}

public String getEmail() {
return email;
}

public Set<AbilityModel> getAbilities() {
return abilities;
}
}

+ 39
- 0
src/main/java/com/ffii/tsms/model/RefreshToken.java Vedi File

@@ -0,0 +1,39 @@
package com.ffii.tsms.model;

import java.time.Instant;

public class RefreshToken {
private String userName;

private String token;

private Instant expiryDate;


public String getUserName() {
return userName;
}

public void setUserName(String userName) {
this.userName = userName;
}

public String getToken() {
return token;
}

public void setToken(String token) {
this.token = token;
}

public Instant getExpiryDate() {
return expiryDate;
}

public void setExpiryDate(Instant expiryDate) {
this.expiryDate = expiryDate;
}

}

+ 16
- 0
src/main/java/com/ffii/tsms/model/TokenRefreshRequest.java Vedi File

@@ -0,0 +1,16 @@
package com.ffii.tsms.model;
import jakarta.validation.constraints.NotBlank;

public class TokenRefreshRequest {
@NotBlank
private String refreshToken;

public String getRefreshToken() {
return refreshToken;
}

public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}

}

+ 37
- 0
src/main/java/com/ffii/tsms/model/TokenRefreshResponse.java Vedi File

@@ -0,0 +1,37 @@
package com.ffii.tsms.model;

public class TokenRefreshResponse {
private String accessToken;
private String refreshToken;
private String tokenType = "Bearer";

public TokenRefreshResponse(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}

public String getAccessToken() {
return accessToken;
}

public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}

public String getRefreshToken() {
return refreshToken;
}

public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}

public String getTokenType() {
return tokenType;
}

public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}

}

+ 17
- 0
src/main/java/com/ffii/tsms/modules/common/ErrorCodes.java Vedi File

@@ -0,0 +1,17 @@
package com.ffii.tsms.modules.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 CHANGE_MAIN_CUSTOMER_ERROR = "CHANGE_MAIN_CUSTOMER_ERROR";
}

+ 116
- 0
src/main/java/com/ffii/tsms/modules/common/PasswordRule.java Vedi File

@@ -0,0 +1,116 @@
package com.ffii.tsms.modules.common;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.ffii.core.utils.PasswordUtils.IPasswordRule;
import com.ffii.tsms.modules.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/tsms/modules/common/SecurityUtils.java Vedi File

@@ -0,0 +1,146 @@
package com.ffii.tsms.modules.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.tsms.modules.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);
}

}

+ 61
- 0
src/main/java/com/ffii/tsms/modules/common/SettingNames.java Vedi File

@@ -0,0 +1,61 @@
package com.ffii.tsms.modules.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";

public static final String LCTS_FLOOR = "LCTS.floor";

}

+ 48
- 0
src/main/java/com/ffii/tsms/modules/common/service/AuditLogService.java Vedi File

@@ -0,0 +1,48 @@
package com.ffii.tsms.modules.common.service;

import java.util.Date;
import java.util.List;
import java.util.Map;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

import com.ffii.core.support.AbstractService;
import com.ffii.core.support.JdbcDao;
import com.ffii.core.utils.MapUtils;
import jakarta.annotation.Nullable;

@Service
public class AuditLogService extends AbstractService {

public AuditLogService(JdbcDao jdbcDao) {
super(jdbcDao);
}

private static final String SQL_INSERT_AUDIT_LOG = "INSERT INTO audit_log (`tableName`, `recordId`, `modifiedBy`, `modified`, `oldData`, `newData`) "
+ "VALUES (:tableName, :recordId, :modifiedBy, :modified, :oldData, :newData)";

@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class, readOnly = false)
public int save(String tableName, Long recordId, Long modifiedBy, Date modified, @Nullable String oldData, String newData) {
return jdbcDao.executeUpdate(SQL_INSERT_AUDIT_LOG,MapUtils.toHashMap("tableName", tableName, "recordId", recordId,
"modifiedBy", modifiedBy, "modified", modified,
"oldData", oldData, "newData", newData));
}

@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = true)
public List<Map<String, Object>> search(String tableName, Integer recordId) {

String sql = "SELECT * FROM audit_log WHERE tableName = :tableName AND recordId = :recordId ORDER BY modified";

return jdbcDao.queryForList(sql, Map.of("tableName", tableName, "recordId", recordId));
}

@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = true)
public List<Map<String, Object>> getTables() {
String sql = "SELECT DISTINCT tableName FROM audit_log";

return jdbcDao.queryForList(sql, "");
}

}

+ 74
- 0
src/main/java/com/ffii/tsms/modules/settings/entity/Settings.java Vedi File

@@ -0,0 +1,74 @@
package com.ffii.tsms.modules.settings.entity;

import com.ffii.core.entity.IdEntity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;

@Entity
@Table(name = "settings")
public class Settings extends IdEntity<Long> {
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;
}

}

+ 12
- 0
src/main/java/com/ffii/tsms/modules/settings/entity/SettingsRepository.java Vedi File

@@ -0,0 +1,12 @@
package com.ffii.tsms.modules.settings.entity;

import java.util.Optional;

import org.springframework.data.repository.query.Param;

import com.ffii.core.support.AbstractRepository;

public interface SettingsRepository extends AbstractRepository<Settings, Long> {

Optional<Settings> findByName(@Param("name") String name);
}

+ 208
- 0
src/main/java/com/ffii/tsms/modules/settings/service/SettingsService.java Vedi File

@@ -0,0 +1,208 @@
package com.ffii.tsms.modules.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.tsms.modules.settings.entity.Settings;
import com.ffii.tsms.modules.settings.entity.SettingsRepository;


@Service
public class SettingsService extends AbstractIdEntityService<Settings, Long, SettingsRepository> {

public SettingsService(JdbcDao jdbcDao, SettingsRepository repository) {
super(jdbcDao, repository);
}

public Optional<Settings> findByName(String name) {
return this.repository.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);
}

}

+ 66
- 0
src/main/java/com/ffii/tsms/modules/settings/web/SettingsController.java Vedi File

@@ -0,0 +1,66 @@
package com.ffii.tsms.modules.settings.web;

import java.util.List;

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.tsms.modules.settings.entity.Settings;
import com.ffii.tsms.modules.settings.service.SettingsService;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;

@RestController
@RequestMapping("/settings")
public class SettingsController{

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

+ 37
- 0
src/main/java/com/ffii/tsms/modules/user/entity/Group.java Vedi File

@@ -0,0 +1,37 @@
package com.ffii.tsms.modules.user.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;

import com.ffii.core.entity.BaseEntity;

@Entity
@Table(name = "`group`")
public class Group extends BaseEntity<Long> {

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

}

+ 6
- 0
src/main/java/com/ffii/tsms/modules/user/entity/GroupRepository.java Vedi File

@@ -0,0 +1,6 @@
package com.ffii.tsms.modules.user.entity;

import com.ffii.core.support.AbstractRepository;

public interface GroupRepository extends AbstractRepository<Group, Long> {
}

+ 254
- 0
src/main/java/com/ffii/tsms/modules/user/entity/User.java Vedi File

@@ -0,0 +1,254 @@
package com.ffii.tsms.modules.user.entity;

import java.time.LocalDate;
import java.util.Collection;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import jakarta.validation.constraints.NotBlank;
import jakarta.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 Terence */
@Entity
@Table(name = "user")
public class User extends BaseEntity<Long> 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;

@Column
private String locale;

@Column
private String fullname;

@Column
private String firstname;

@Column
private String lastname;

@Column
private String department;

@Column
private String title;

@Column
private String email;

@Column
private String phone1;

@Column
private String phone2;

@Column
private String remarks;

@Column
private boolean lotusNotesUser = false;

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 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 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 boolean isLotusNotesUser() {
return this.lotusNotesUser;
}

public boolean getLotusNotesUser() {
return this.lotusNotesUser;
}

public void setLotusNotesUser(boolean lotusNotesUser) {
this.lotusNotesUser = lotusNotesUser;
}

}

+ 15
- 0
src/main/java/com/ffii/tsms/modules/user/entity/UserRepository.java Vedi File

@@ -0,0 +1,15 @@
package com.ffii.tsms.modules.user.entity;

import java.util.List;
import java.util.Optional;

import org.springframework.data.repository.query.Param;

import com.ffii.core.support.AbstractRepository;

public interface UserRepository extends AbstractRepository<User, Long> {

List<User> findByName(@Param("name") String name);

Optional<User> findByUsernameAndDeletedFalse(String username);
}

+ 29
- 0
src/main/java/com/ffii/tsms/modules/user/req/NewPublicUserReq.java Vedi File

@@ -0,0 +1,29 @@
package com.ffii.tsms.modules.user.req;

import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;

/** @author Alex */
public class NewPublicUserReq extends UpdateUserReq {

@Size(max = 30)
@Pattern(regexp = "^[A-Za-z0-9]+$")
private String username;
private String password;

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return this.password;
}

public void setPassword(String password) {
this.password = password;
}
}

+ 21
- 0
src/main/java/com/ffii/tsms/modules/user/req/NewUserReq.java Vedi File

@@ -0,0 +1,21 @@
package com.ffii.tsms.modules.user.req;

import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;

/** @author Alex */
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/tsms/modules/user/req/SaveGroupReq.java Vedi File

@@ -0,0 +1,80 @@
package com.ffii.tsms.modules.user.req;

import java.util.List;

import jakarta.validation.constraints.NotNull;

public class SaveGroupReq {
private Long id;

@NotNull
private String name;
private String description;

@NotNull
private List<Long> addUserIds;
@NotNull
private List<Long> removeUserIds;

@NotNull
private List<Long> addAuthIds;
@NotNull
private List<Long> removeAuthIds;

public Long getId() {
return id;
}

public void setId(Long 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<Long> getAddUserIds() {
return addUserIds;
}

public void setAddUserIds(List<Long> addUserIds) {
this.addUserIds = addUserIds;
}

public List<Long> getRemoveUserIds() {
return removeUserIds;
}

public void setRemoveUserIds(List<Long> removeUserIds) {
this.removeUserIds = removeUserIds;
}

public List<Long> getAddAuthIds() {
return addAuthIds;
}

public void setAddAuthIds(List<Long> addAuthIds) {
this.addAuthIds = addAuthIds;
}

public List<Long> getRemoveAuthIds() {
return removeAuthIds;
}

public void setRemoveAuthIds(List<Long> removeAuthIds) {
this.removeAuthIds = removeAuthIds;
}

}

+ 69
- 0
src/main/java/com/ffii/tsms/modules/user/req/SearchUserReq.java Vedi File

@@ -0,0 +1,69 @@
package com.ffii.tsms.modules.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;
}

}

+ 151
- 0
src/main/java/com/ffii/tsms/modules/user/req/UpdateUserReq.java Vedi File

@@ -0,0 +1,151 @@
package com.ffii.tsms.modules.user.req;

import java.time.LocalDate;
import java.util.List;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

/** @author Alex */
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;
private String remarks;

@NotBlank
@Email
private String email;
@NotBlank
private String department;

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

}

+ 176
- 0
src/main/java/com/ffii/tsms/modules/user/service/GroupService.java Vedi File

@@ -0,0 +1,176 @@
package com.ffii.tsms.modules.user.service;

import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.ffii.core.exception.NotFoundException;
import com.ffii.core.support.AbstractBaseEntityService;
import com.ffii.core.support.JdbcDao;
import com.ffii.core.utils.JsonUtils;
import com.ffii.core.utils.Params;
import com.ffii.tsms.modules.common.SecurityUtils;
import com.ffii.tsms.modules.common.service.AuditLogService;
import com.ffii.tsms.modules.user.entity.Group;
import com.ffii.tsms.modules.user.entity.GroupRepository;
import com.ffii.tsms.modules.user.req.SaveGroupReq;

import jakarta.persistence.Table;
import jakarta.validation.Valid;


@Service
public class GroupService extends AbstractBaseEntityService<Group, Long, GroupRepository> {

@Autowired
private AuditLogService auditLogService;

public GroupService(JdbcDao jdbcDao, GroupRepository repository) {
super(jdbcDao, repository);
}

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

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 {
instance = new Group();
}
BeanUtils.copyProperties(req, instance);

String tableName = instance.getClass().getAnnotation(Table.class).name();
StringBuilder sql = new StringBuilder("SELECT * FROM " + tableName + " WHERE id = :id");
String oldValueJson = null;
String newValueJson = null;

if (instance != null && instance.getId() != null && instance.getId() > 0) {
oldValueJson = JsonUtils.toJsonString(jdbcDao.queryForMap(sql.toString(), Map.of("id", instance.getId())).orElseThrow(NotFoundException::new));
}
instance = saveAndFlush(instance);
Long id = instance.getId();

List<Map<String, Long>> userBatchInsertValues = req.getAddUserIds().stream()
.map(userId -> Map.of("groupId", id, "userId", userId))
.collect(Collectors.toList());
List<Map<String, Long>> 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, Long>> authBatchInsertValues = req.getAddAuthIds().stream()
.map(authId -> Map.of("groupId", id, "authId", authId))
.collect(Collectors.toList());
List<Map<String, Long>> 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);
}

if (instance != null && instance.getId() != null && instance.getId() > 0) {
newValueJson = JsonUtils.toJsonString(jdbcDao.queryForMap(sql.toString(), Map.of("id", instance.getId())).orElseThrow(NotFoundException::new));
}

auditLogService.save(
tableName,
id,
SecurityUtils.getUser() != null ? SecurityUtils.getUser().get().getId() : null,
new Date(),
oldValueJson,
newValueJson);
return instance;
}

public List<Integer> listGroupAuthId(Long id) {
return jdbcDao.queryForInts(
"SELECT"
+ " ga.authId"
+ " FROM group_authority ga"
+ " WHERE ga.groupId = :id",
Map.of(Params.ID, id));
}

public List<Integer> listGroupUserId(Long 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));
}

}

+ 48
- 0
src/main/java/com/ffii/tsms/modules/user/service/UserAuthorityService.java Vedi File

@@ -0,0 +1,48 @@
package com.ffii.tsms.modules.user.service;

import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.ffii.core.support.AbstractService;
import com.ffii.core.support.JdbcDao;
import com.ffii.tsms.modules.user.entity.User;

@Service
public class UserAuthorityService extends AbstractService {
private static final String USER_AUTH_SQL = "SELECT a.authority"
+ " FROM `user` u"
+ " JOIN user_authority ua ON ua.userId = u.id"
+ " JOIN authority a ON a.id = ua.authId"
+ " WHERE u.deleted = 0"
+ " AND u.id = :userId";
private static final String UNION_SQL = " UNION ";
private static final String GROUP_AUTH_SQL = "SELECT a.authority"
+ " FROM `user` u"
+ " JOIN user_group ug ON ug.userId = u.id"
+ " JOIN `group` g ON g.deleted = 0 AND g.id = ug.groupId"
+ " JOIN group_authority ga ON ga.groupId = g.id"
+ " JOIN authority a ON a.id = ga.authId"
+ " WHERE u.deleted = 0"
+ " AND u.id = :userId";

public UserAuthorityService(JdbcDao jdbcDao) {
super(jdbcDao);
}

@Transactional(rollbackFor = Exception.class)
public Set<SimpleGrantedAuthority> getUserAuthority(User user) {
Set<SimpleGrantedAuthority> auths = new HashSet<>();
List<Map<String, Object>> records = jdbcDao.queryForList(USER_AUTH_SQL + UNION_SQL + GROUP_AUTH_SQL,
Map.of("userId", user.getId()));

records.forEach(item -> auths.add(new SimpleGrantedAuthority((String) item.get("authority"))));
return auths;
}

}

+ 269
- 0
src/main/java/com/ffii/tsms/modules/user/service/UserService.java Vedi File

@@ -0,0 +1,269 @@
package com.ffii.tsms.modules.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 org.apache.commons.lang3.LocaleUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.password.PasswordEncoder;
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.utils.Params;
import com.ffii.core.utils.PasswordUtils;
import com.ffii.tsms.modules.common.ErrorCodes;
import com.ffii.tsms.modules.common.PasswordRule;
import com.ffii.tsms.modules.settings.service.SettingsService;
import com.ffii.tsms.modules.user.entity.User;
import com.ffii.tsms.modules.user.entity.UserRepository;
import com.ffii.tsms.modules.user.req.NewPublicUserReq;
import com.ffii.tsms.modules.user.req.NewUserReq;
import com.ffii.tsms.modules.user.req.SearchUserReq;
import com.ffii.tsms.modules.user.req.UpdateUserReq;
import com.ffii.tsms.modules.user.service.pojo.UserRecord;

import jakarta.mail.internet.InternetAddress;

@Service
public class UserService extends AbstractBaseEntityService<User, Long, UserRepository> {
private static final String USER_AUTH_SQL = "SELECT a.authority"
+ " FROM `user` u"
+ " JOIN user_authority ua ON ua.userId = u.id"
+ " JOIN authority a ON a.id = ua.authId"
+ " WHERE u.deleted = 0"
+ " AND u.id = :userId";
private static final String UNION_SQL = " UNION ";
private static final String GROUP_AUTH_SQL = "SELECT a.authority"
+ " FROM `user` u"
+ " JOIN user_group ug ON ug.userId = u.id"
+ " JOIN `group` g ON g.deleted = 0 AND g.id = ug.groupId"
+ " JOIN group_authority ga ON ga.groupId = g.id"
+ " JOIN authority a ON a.id = ga.authId"
+ " WHERE u.deleted = 0"
+ " AND u.id = :userId";

@Autowired
private SettingsService settingsService;
@Autowired
private PasswordEncoder passwordEncoder;

@Autowired
UserRepository userRepository;

public UserService(JdbcDao jdbcDao, UserRepository userRepository) {
super(jdbcDao, userRepository);
}

public Optional<User> loadUserOptByUsername(String username) {
return findByUsername(username)
.map(user -> {
Set<GrantedAuthority> auths = new LinkedHashSet<GrantedAuthority>();
auths.add(new SimpleGrantedAuthority("ROLE_USER"));
jdbcDao.queryForList(USER_AUTH_SQL + UNION_SQL + GROUP_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 userRepository.findByUsernameAndDeletedFalse(username);
}

// @Transactional(rollbackFor = Exception.class)
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.locale,"
+ " u.firstname,"
+ " u.lastname,"
+ " u.title,"
+ " u.department,"
+ " u.email,"
+ " u.phone1,"
+ " u.phone2,"
+ " u.remarks "
+ " FROM `user` u"
+ " left join user_group ug on u.id = ug.userId"
+ " 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 List<Integer> listUserAuthId(long id) {
return jdbcDao.queryForInts(
"SELECT"
+ " ua.authId"
+ " FROM user_authority ua"
+ " WHERE ua.userId = :id",
Map.of(Params.ID, id));
}

public List<Integer> listUserGroupId(long 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));
}

private User saveOrUpdate(User instance, UpdateUserReq req) {
if (instance.getId() == null){
req.setLocked(false);
}
BeanUtils.copyProperties(req,instance);
instance = save(instance);
// long id = instance.getId();

// List<Map<String, Integer>> groupBatchInsertValues = req.getAddGroupIds().stream()
// .map(groupId -> Map.of("userId", (int) id, "groupId", groupId))
// .collect(Collectors.toList());
// List<Map<String, Integer>> groupBatchDeleteValues = req.getRemoveGroupIds().stream()
// .map(groupId -> Map.of("userId", (int) 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", (int)id, "authId", authId))
// .collect(Collectors.toList());
// List<Map<String, Integer>> authBatchDeleteValues = req.getRemoveAuthIds().stream()
// .map(authId -> Map.of("userId", (int)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;
}

@Transactional(rollbackFor = Exception.class)
public User newRecord(NewUserReq req) throws UnsupportedEncodingException {
if (findByUsername(req.getUsername()).isPresent()) {
throw new UnprocessableEntityException(ErrorCodes.USERNAME_NOT_AVAILABLE);
}

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(messageSource.getMessage("USER.newAc.subject", null, locale))
// .template("mail/newUser")
// .args(Map.of("username", instance.getUsername(), "password", StringEscapeUtils.escapeHtml4(randomPassword)))
// .addTo(new InternetAddress(instance.getEmail(), instance.getName()))
// .build(),
// locale);
return instance;
}

@Transactional(rollbackFor = Exception.class)
public User newPublicUserRecord(NewPublicUserReq req) throws UnsupportedEncodingException {
if (findByUsername(req.getUsername()).isPresent()) {
throw new UnprocessableEntityException(ErrorCodes.USERNAME_NOT_AVAILABLE);
}

String submitedPassword = req.getPassword();
String pwdHash = passwordEncoder.encode(submitedPassword);
req.setPassword(pwdHash);
User instance = new User();

instance = saveOrUpdate(instance, req);
return instance;
}

@Transactional(rollbackFor = Exception.class)
public void updateRecord(long id, UpdateUserReq req) {
saveOrUpdate(
find(id).orElseThrow(NotFoundException::new),
req);
}

@Transactional(rollbackFor = Exception.class)
public String resetPassword(long id) throws UnsupportedEncodingException {
User instance = find(id).orElseThrow(NotFoundException::new);
String randomPassword = PasswordUtils.genPwd(new PasswordRule(settingsService));

instance.setPassword(passwordEncoder.encode(randomPassword));
instance = save(instance);
return randomPassword;
}


}

+ 41
- 0
src/main/java/com/ffii/tsms/modules/user/service/pojo/AuthRecord.java Vedi File

@@ -0,0 +1,41 @@
package com.ffii.tsms.modules.user.service.pojo;

public class AuthRecord {
private Integer id;
private String module;
private String authority;
private String name;

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 getAuthority() {
return authority;
}

public void setAuthority(String authority) {
this.authority = authority;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

}

+ 155
- 0
src/main/java/com/ffii/tsms/modules/user/service/pojo/UserRecord.java Vedi File

@@ -0,0 +1,155 @@
package com.ffii.tsms.modules.user.service.pojo;

import java.time.LocalDateTime;

public class UserRecord {
private Integer id;
private LocalDateTime created;
private String createdBy;
private String 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 String getModified() {
return modified;
}
public void setModified(String 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;
}

}

+ 45
- 0
src/main/java/com/ffii/tsms/modules/user/service/res/LoadUserRes.java Vedi File

@@ -0,0 +1,45 @@
package com.ffii.tsms.modules.user.service.res;

import java.util.List;

import com.ffii.tsms.modules.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;
}

}

+ 80
- 0
src/main/java/com/ffii/tsms/modules/user/web/GroupController.java Vedi File

@@ -0,0 +1,80 @@
package com.ffii.tsms.modules.user.web;

import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.HttpStatus;
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.exception.NotFoundException;
import com.ffii.core.response.IdRes;
import com.ffii.core.response.RecordsRes;
import com.ffii.core.utils.CriteriaArgsBuilder;
import com.ffii.core.utils.Params;
import com.ffii.tsms.modules.user.req.SaveGroupReq;
import com.ffii.tsms.modules.user.service.GroupService;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;

@RestController
@RequestMapping("/group")
public class GroupController{

private final Log logger = LogFactory.getLog(getClass());
private GroupService groupService;
public GroupController(
GroupService groupService
) {
this.groupService = groupService;
}
@PostMapping("/save")
public IdRes saveOrUpdate(@RequestBody @Valid SaveGroupReq req) {
return new IdRes(groupService.saveOrUpdate(req).getId());
}

@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
groupService.delete(groupService.find(id).orElseThrow(NotFoundException::new));
}

@GetMapping("/{id}")
public Map<String, Object> load(@PathVariable Long 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()));
}

}

+ 21
- 0
src/main/java/com/ffii/tsms/modules/user/web/TestController.java Vedi File

@@ -0,0 +1,21 @@
package com.ffii.tsms.modules.user.web;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

private final Log logger = LogFactory.getLog(getClass());
@GetMapping("/test")
@Secured("ROLE_USER")
public ResponseEntity<?> test() throws Exception {
logger.info("hihihihihii");
return ResponseEntity.ok("hihi");
}
}

+ 193
- 0
src/main/java/com/ffii/tsms/modules/user/web/UserController.java Vedi File

@@ -0,0 +1,193 @@
package com.ffii.tsms.modules.user.web;

import java.io.UnsupportedEncodingException;

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.prepost.PreAuthorize;
import org.springframework.security.crypto.password.PasswordEncoder;
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.exception.BadRequestException;
import com.ffii.core.exception.NotFoundException;
import com.ffii.core.exception.UnprocessableEntityException;
import com.ffii.core.response.IdRes;
import com.ffii.core.utils.PasswordUtils;
import com.ffii.tsms.modules.common.ErrorCodes;
import com.ffii.tsms.modules.common.PasswordRule;
import com.ffii.tsms.modules.common.SecurityUtils;
import com.ffii.tsms.modules.settings.service.SettingsService;
import com.ffii.tsms.modules.user.entity.User;
import com.ffii.tsms.modules.user.req.NewPublicUserReq;
import com.ffii.tsms.modules.user.req.NewUserReq;
import com.ffii.tsms.modules.user.req.SearchUserReq;
import com.ffii.tsms.modules.user.req.UpdateUserReq;
import com.ffii.tsms.modules.user.service.UserService;
import com.ffii.tsms.modules.user.service.res.LoadUserRes;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;

@RestController
@RequestMapping("/user")
public class UserController{

private final Log logger = LogFactory.getLog(getClass());
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 ResponseEntity<?> list(@ModelAttribute @Valid SearchUserReq req) {
logger.info("Test List user");
return ResponseEntity.ok(userService.search(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 long id) {
LoadUserRes test = new LoadUserRes(
userService.find(id).orElseThrow(NotFoundException::new),
userService.listUserAuthId(id),
userService.listUserGroupId(id));
logger.info("Test List user2");
logger.info(test);
return test;
}

// @Operation(summary = "delete user", responses = { @ApiResponse(responseCode = "204"),
// @ApiResponse(responseCode = "404", content = @Content) })
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@PreAuthorize("hasAuthority('MAINTAIN_USER')")
public void delete(@PathVariable long id) {
userService.markDelete(userService.find(id).orElseThrow(NotFoundException::new));
}

// @Operation(summary = "new user")
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@PreAuthorize("hasAuthority('MAINTAIN_USER')")
public IdRes newRecord(@RequestBody @Valid NewUserReq req) throws UnsupportedEncodingException {
return new IdRes(userService.newRecord(req).getId());
}

// @Operation(summary = "new user by public user")
@PostMapping("/registry")
@ResponseStatus(HttpStatus.CREATED)
// @PreAuthorize("hasAuthority('MAINTAIN_USER')")
public ResponseEntity<?> createPublicUserRecord(@RequestBody NewPublicUserReq req) throws UnsupportedEncodingException {
logger.info("Create user request:");
return ResponseEntity.ok(new IdRes(userService.newPublicUserRecord(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('MAINTAIN_USER')")
public void updateRecord(@PathVariable int id, @RequestBody @Valid UpdateUserReq req) {
userService.updateRecord(id, req);
}

// @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)
// @PreAuthorize("hasAuthority('MAINTAIN_USER')")
public void changePassword(@RequestBody @Valid ChangePwdReq req) {
long id = SecurityUtils.getUser().get().getId();
User instance = userService.find(id).orElseThrow(NotFoundException::new);

logger.info("TEST req: "+req.getPassword());
logger.info("TEST instance: "+instance.getPassword());
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('MAINTAIN_USER')")
@ResponseStatus(HttpStatus.NO_CONTENT)
public ResponseEntity<?> resetPassword(@PathVariable long id) throws UnsupportedEncodingException {
String password = userService.resetPassword(id);
return ResponseEntity.ok(password);
}

// @Operation(summary = "get password rules")
@GetMapping("/password-rule")
public PasswordRule passwordRlue() {
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;
}

}

}

+ 5
- 0
src/main/resources/application-db-2fi.yml Vedi File

@@ -0,0 +1,5 @@
spring:
datasource:
jdbc-url: jdbc:mysql://192.168.1.81:3306/arsdb?useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT%2B8
username: root
password: secret

+ 5
- 0
src/main/resources/application-db-local.yml Vedi File

@@ -0,0 +1,5 @@
spring:
datasource:
jdbc-url: jdbc:mysql://127.0.0.1:3306/tsmsdb?useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT%2B8
username: root
password: secret

+ 9
- 0
src/main/resources/application-ldap-local.yml Vedi File

@@ -0,0 +1,9 @@
spring:
ldap:
embedded:
port: 8389
base-dn: dc=springframework,dc=org
ldif: classpath:ldap-test-users.ldif
validation:
enabled: false
urls: ldap://localhost:8389

+ 2
- 0
src/main/resources/application-prod-linux.yml Vedi File

@@ -0,0 +1,2 @@
logging:
config: 'classpath:log4j2-prod-linux.yml'

+ 2
- 0
src/main/resources/application-prod-win.yml Vedi File

@@ -0,0 +1,2 @@
logging:
config: 'classpath:log4j2-prod-win.yml'

+ 28
- 0
src/main/resources/application.yml Vedi File

@@ -0,0 +1,28 @@
server:
servlet:
contextPath: /api
encoding:
charset: UTF-8
enabled: true
force: true
port: 8090
error:
include-message: always

spring:
servlet:
multipart:
max-file-size: 500MB
max-request-size: 600MB
jpa:
hibernate:
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
database-platform: org.hibernate.dialect.MySQL8Dialect
properties:
hibernate:
dialect:
storage_engine: innodb

logging:
config: 'classpath:log4j2.yml'

+ 77
- 0
src/main/resources/db/changelog/changes/20230720_01_alex/01_base.sql Vedi File

@@ -0,0 +1,77 @@
--liquibase formatted sql

--changeset alex:user
--comment: core table
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`createdBy` varchar(30) DEFAULT NULL,
`version` int NOT NULL DEFAULT '0',
`modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`modifiedBy` varchar(30) DEFAULT NULL,
`deleted` tinyint(1) NOT NULL DEFAULT '0',
`username` varchar(30) NOT NULL,
`password` varchar(60) DEFAULT NULL,
`locked` tinyint(1) NOT NULL DEFAULT '0',
`expiryDate` date DEFAULT NULL,
`name` varchar(50) NOT NULL,
`locale` varchar(5) DEFAULT NULL,
`fullname` varchar(90) DEFAULT NULL,
`firstname` varchar(45) DEFAULT NULL,
`lastname` varchar(30) DEFAULT NULL,
`title` varchar(60) DEFAULT NULL,
`department` varchar(60) DEFAULT NULL,
`email` varchar(120) DEFAULT NULL,
`phone1` varchar(30) DEFAULT NULL,
`phone2` varchar(30) DEFAULT NULL,
`remarks` varchar(600) DEFAULT NULL,
`lotusNotesUser` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

INSERT INTO `user`(`name`,`username`, `password`)VALUES ('2fi','2fi','$2a$10$65S7/AhKn8MldlYmvFN5JOfr1yaULwFNDIhTskLTuUCKgbbs8sFAi');
INSERT INTO `user`(`name`,`username`, `password`,`lotusNotesUser`)VALUES ('user1','user1',null,1);

CREATE TABLE `authority` (
`id` int NOT NULL AUTO_INCREMENT,
`authority` varchar(255) NOT NULL,
`name` varchar(100) NOT NULL,
`module` varchar(50) DEFAULT NULL,
`description` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `authority` (`authority`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO `authority` VALUES (1,'MAINTAIN_USER','Maintain User',NULL,NULL),(2,'MAINTAIN_GROUP','Maintain group',NULL,NULL),(3,'VIEW_USER','view user',NULL,NULL),(4,'VIEW_GROUP','view group',NULL,NULL);
CREATE TABLE `user_authority` (
`userId` int NOT NULL,
`authId` int NOT NULL,
PRIMARY KEY (`userId`,`authId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO `user_authority` VALUES (1,3);

--changeset alex:group
--comment: group table
CREATE TABLE `group` (
`id` int NOT NULL AUTO_INCREMENT,
`created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`createdBy` varchar(30) DEFAULT NULL,
`version` int NOT NULL DEFAULT '0',
`modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`modifiedBy` varchar(30) DEFAULT NULL,
`deleted` tinyint(1) NOT NULL DEFAULT '0',
`name` varchar(50) NOT NULL,
`description` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_bin DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE `user_group` (
`groupId` int NOT NULL,
`userId` int NOT NULL,
PRIMARY KEY (`groupId`,`userId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE `group_authority` (
`groupId` int NOT NULL,
`authId` int NOT NULL,
PRIMARY KEY (`groupId`,`authId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

+ 13
- 0
src/main/resources/db/changelog/changes/20230720_01_alex/02_settings.sql Vedi File

@@ -0,0 +1,13 @@
--liquibase formatted sql

--changeset alex:settings
--comment: settings table
CREATE TABLE `settings` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`value` varchar(1000) NOT NULL,
`category` varchar(50),
`type` varchar(10),

INDEX `name_idx` (`name`)
);

+ 10
- 0
src/main/resources/db/changelog/changes/20230720_01_alex/03_settings_data.sql Vedi File

@@ -0,0 +1,10 @@
--liquibase formatted sql

--changeset alex:settings_data
INSERT INTO `settings` (`name`, `value`,`type`) VALUES
('SYS.password.rule.length.max', '20', 'integer'),
('SYS.password.rule.length.min', '8', 'integer'),
('SYS.password.rule.lower.eng', 'true', 'boolean'),
('SYS.password.rule.number', 'true', 'boolean'),
('SYS.password.rule.special', 'true', 'boolean'),
('SYS.password.rule.upper.eng', 'true', 'boolean');

+ 5
- 0
src/main/resources/db/changelog/changes/20230720_01_alex/04_update_user_authority.sql Vedi File

@@ -0,0 +1,5 @@
--liquibase formatted sql

--changeset alex:update_user_authority
INSERT INTO `tsmsdb`.`user_authority` (`userId`, `authId`) VALUES ('1', '1');
INSERT INTO `tsmsdb`.`user_authority` (`userId`, `authId`) VALUES ('1', '2');

+ 13
- 0
src/main/resources/db/changelog/changes/20230725_01_alex/01_audit_log.sql Vedi File

@@ -0,0 +1,13 @@
--liquibase formatted sql

--changeset alex:audit_log
--comment: audit log
CREATE TABLE `audit_log` (
`tableName` varchar(30) NOT NULL,
`recordId` int(11) NOT NULL,
`modifiedBy` int(11) DEFAULT NULL,
`modified` datetime DEFAULT NULL,
`oldData` json DEFAULT NULL,
`newData` json DEFAULT NULL,
KEY `idx_tableName_recordId` (`tableName`,`recordId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

+ 11
- 0
src/main/resources/db/changelog/changes/20230725_01_alex/02_user_login_log.sql Vedi File

@@ -0,0 +1,11 @@
--liquibase formatted sql

--changeset alex:user_login_log
--comment: user login log
CREATE TABLE `user_login_log` (
`username` varchar(32) NOT NULL,
`loginTime` datetime NOT NULL,
`ipAddr` varchar(45) NOT NULL,
`success` tinyint(1) NOT NULL,
PRIMARY KEY (`username`,`loginTime`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

+ 3
- 0
src/main/resources/db/changelog/db.changelog-master.yaml Vedi File

@@ -0,0 +1,3 @@
databaseChangeLog:
- includeAll:
path: classpath:/db/changelog/changes

+ 14
- 0
src/main/resources/ldap-test-users.ldif Vedi File

@@ -0,0 +1,14 @@
dn: dc=springframework,dc=org
objectClass: top
objectClass: domain
dc: springframework

dn: uid=user1,dc=springframework,dc=org
objectClass: top
cn: user1
userPassword: userPass1

dn: uid=user2,dc=springframework,dc=org
objectClass: top
cn: user2
userPassword: userPass2

+ 23
- 0
src/main/resources/log4j2-prod-linux.yml Vedi File

@@ -0,0 +1,23 @@
Configutation:
name: Prod-Default
Properties:
Property:
name: log_location
value: /usr/springboot/logs/
Appenders:
RollingFile:
name: RollingFile_Appender
fileName: ${log_location}tsms-all.log
filePattern: ${log_location}tsms-all.log.%i.gz
PatternLayout:
Pattern: "%d %p [%l] - %m%n"
Policies:
SizeBasedTriggeringPolicy:
size: 4096KB
DefaultRollOverStrategy:
max: 99
Loggers:
Root:
level: info
AppenderRef:
- ref: RollingFile_Appender

+ 23
- 0
src/main/resources/log4j2-prod-win.yml Vedi File

@@ -0,0 +1,23 @@
Configutation:
name: Prod-Default
Properties:
Property:
name: log_location
value: C:/workspace/
Appenders:
RollingFile:
name: RollingFile_Appender
fileName: ${log_location}tsms-all.log
filePattern: ${log_location}tsms-all.log.%i.gz
PatternLayout:
Pattern: "%d %p [%l] - %m%n"
Policies:
SizeBasedTriggeringPolicy:
size: 4096KB
DefaultRollOverStrategy:
max: 99
Loggers:
Root:
level: info
AppenderRef:
- ref: RollingFile_Appender

+ 17
- 0
src/main/resources/log4j2.yml Vedi File

@@ -0,0 +1,17 @@
Configutation:
name: Default
Properties:
Property:
name: log_pattern
value: "%d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${hostName} --- [%15.15t] %-40.40c{1.} : %m%n%ex"
Appenders:
Console:
name: Console_Appender
target: SYSTEM_OUT
PatternLayout:
pattern: ${log_pattern}
Loggers:
Root:
level: info
AppenderRef:
- ref: Console_Appender

+ 13
- 0
src/test/java/com/ffii/tsms/ArsApplicationTests.java Vedi File

@@ -0,0 +1,13 @@
package com.ffii.tsms;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class TsmsApplicationTests {

@Test
void contextLoads() {
}

}

Caricamento…
Annulla
Salva