aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWilliam Harrington <kb0iic@berzerkula.org>2025-02-11 21:30:34 -0600
committerWilliam Harrington <kb0iic@berzerkula.org>2025-02-11 21:30:34 -0600
commit5db196eebb04155491630b5396d1b7a7f2eab4e6 (patch)
treefc53afeb77808ca6c8ed39a055ac79241c756540
parent0adf14e0466bc153f4924cef62b18352aa987827 (diff)
Push initial release 1.0.0
-rw-r--r--.gitattributes2
-rw-r--r--.mvn/wrapper/maven-wrapper.properties19
-rw-r--r--mvnw259
-rw-r--r--mvnw.cmd149
-rw-r--r--pom.xml109
-rw-r--r--src/main/java/org/berzerkula/builddb/BuilddbApplication.java19
-rw-r--r--src/main/java/org/berzerkula/builddb/config/SecurityConfig.java79
-rw-r--r--src/main/java/org/berzerkula/builddb/controllers/AccountController.java141
-rw-r--r--src/main/java/org/berzerkula/builddb/controllers/DashboardController.java27
-rw-r--r--src/main/java/org/berzerkula/builddb/controllers/HomeController.java47
-rw-r--r--src/main/java/org/berzerkula/builddb/controllers/PkgController.java169
-rw-r--r--src/main/java/org/berzerkula/builddb/models/AppUser.java107
-rw-r--r--src/main/java/org/berzerkula/builddb/models/Pkg.java118
-rw-r--r--src/main/java/org/berzerkula/builddb/models/PkgDto.java104
-rw-r--r--src/main/java/org/berzerkula/builddb/models/RegisterDto.java84
-rw-r--r--src/main/java/org/berzerkula/builddb/repositories/AppUserRepository.java10
-rw-r--r--src/main/java/org/berzerkula/builddb/repositories/PkgRepository.java8
-rw-r--r--src/main/java/org/berzerkula/builddb/services/AppUserService.java36
-rw-r--r--src/main/resources/application.properties15
-rw-r--r--src/main/resources/templates/actuatorDashboard.html39
-rw-r--r--src/main/resources/templates/admin.html53
-rw-r--r--src/main/resources/templates/client.html53
-rw-r--r--src/main/resources/templates/contact.html53
-rw-r--r--src/main/resources/templates/index.html37
-rw-r--r--src/main/resources/templates/login.html87
-rw-r--r--src/main/resources/templates/navbar.html77
-rw-r--r--src/main/resources/templates/pkgs/add.html109
-rw-r--r--src/main/resources/templates/pkgs/edit.html109
-rw-r--r--src/main/resources/templates/pkgs/index.html62
-rw-r--r--src/main/resources/templates/pkgs/sorting.html6
-rw-r--r--src/main/resources/templates/profile.html84
-rw-r--r--src/main/resources/templates/register.html155
-rw-r--r--src/main/resources/templates/user.html53
-rw-r--r--src/test/java/org/berzerkula/builddb/BuilddbApplicationAppUserTests.java111
-rw-r--r--src/test/java/org/berzerkula/builddb/BuilddbApplicationPkgTests.java113
-rw-r--r--src/test/java/org/berzerkula/builddb/BuilddbApplicationTest.java24
-rw-r--r--src/test/java/org/berzerkula/builddb/controllers/BuilddbTestAccountController.java137
-rw-r--r--src/test/java/org/berzerkula/builddb/controllers/BuilddbTestDashboardController.java62
-rw-r--r--src/test/java/org/berzerkula/builddb/controllers/BuilddbTestHomeController.java33
-rw-r--r--src/test/java/org/berzerkula/builddb/controllers/BuilddbTestPkgController.java89
-rw-r--r--src/test/java/org/berzerkula/builddb/repositories/AppUserRepositoryTest.java31
-rw-r--r--src/test/java/org/berzerkula/builddb/repositories/PkgRepositoryTest.java32
-rw-r--r--src/test/java/org/berzerkula/builddb/repositories/TestH2AppUserRepository.java7
-rw-r--r--src/test/java/org/berzerkula/builddb/repositories/TestH2PkgRepository.java7
-rw-r--r--src/test/java/org/berzerkula/builddb/services/AppUserServiceTest.java54
-rw-r--r--src/test/resources/application.properties9
46 files changed, 3188 insertions, 0 deletions
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..3b41682
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+/mvnw text eol=lf
+*.cmd text eol=crlf
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000..d58dfb7
--- /dev/null
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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
+#
+# http://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.
+wrapperVersion=3.3.2
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
diff --git a/mvnw b/mvnw
new file mode 100644
index 0000000..19529dd
--- /dev/null
+++ b/mvnw
@@ -0,0 +1,259 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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
+#
+# http://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.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version 3.3.2
+#
+# Optional ENV vars
+# -----------------
+# JAVA_HOME - location of a JDK home dir, required when download maven via java source
+# MVNW_REPOURL - repo url base for downloading maven distribution
+# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
+# ----------------------------------------------------------------------------
+
+set -euf
+[ "${MVNW_VERBOSE-}" != debug ] || set -x
+
+# OS specific support.
+native_path() { printf %s\\n "$1"; }
+case "$(uname)" in
+CYGWIN* | MINGW*)
+ [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
+ native_path() { cygpath --path --windows "$1"; }
+ ;;
+esac
+
+# set JAVACMD and JAVACCMD
+set_java_home() {
+ # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
+ 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"
+ JAVACCMD="$JAVA_HOME/jre/sh/javac"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ JAVACCMD="$JAVA_HOME/bin/javac"
+
+ if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
+ echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
+ echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
+ return 1
+ fi
+ fi
+ else
+ JAVACMD="$(
+ 'set' +e
+ 'unset' -f command 2>/dev/null
+ 'command' -v java
+ )" || :
+ JAVACCMD="$(
+ 'set' +e
+ 'unset' -f command 2>/dev/null
+ 'command' -v javac
+ )" || :
+
+ if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
+ echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
+ return 1
+ fi
+ fi
+}
+
+# hash string like Java String::hashCode
+hash_string() {
+ str="${1:-}" h=0
+ while [ -n "$str" ]; do
+ char="${str%"${str#?}"}"
+ h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
+ str="${str#?}"
+ done
+ printf %x\\n $h
+}
+
+verbose() { :; }
+[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
+
+die() {
+ printf %s\\n "$1" >&2
+ exit 1
+}
+
+trim() {
+ # MWRAPPER-139:
+ # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
+ # Needed for removing poorly interpreted newline sequences when running in more
+ # exotic environments such as mingw bash on Windows.
+ printf "%s" "${1}" | tr -d '[:space:]'
+}
+
+# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
+while IFS="=" read -r key value; do
+ case "${key-}" in
+ distributionUrl) distributionUrl=$(trim "${value-}") ;;
+ distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
+ esac
+done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
+[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
+
+case "${distributionUrl##*/}" in
+maven-mvnd-*bin.*)
+ MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
+ case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
+ *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
+ :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
+ :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
+ :Linux*x86_64*) distributionPlatform=linux-amd64 ;;
+ *)
+ echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
+ distributionPlatform=linux-amd64
+ ;;
+ esac
+ distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
+ ;;
+maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
+*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
+esac
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
+[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
+distributionUrlName="${distributionUrl##*/}"
+distributionUrlNameMain="${distributionUrlName%.*}"
+distributionUrlNameMain="${distributionUrlNameMain%-bin}"
+MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
+MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
+
+exec_maven() {
+ unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
+ exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
+}
+
+if [ -d "$MAVEN_HOME" ]; then
+ verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ exec_maven "$@"
+fi
+
+case "${distributionUrl-}" in
+*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
+*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
+esac
+
+# prepare tmp dir
+if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
+ clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
+ trap clean HUP INT TERM EXIT
+else
+ die "cannot create temp dir"
+fi
+
+mkdir -p -- "${MAVEN_HOME%/*}"
+
+# Download and Install Apache Maven
+verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+verbose "Downloading from: $distributionUrl"
+verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+# select .zip or .tar.gz
+if ! command -v unzip >/dev/null; then
+ distributionUrl="${distributionUrl%.zip}.tar.gz"
+ distributionUrlName="${distributionUrl##*/}"
+fi
+
+# verbose opt
+__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
+[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
+
+# normalize http auth
+case "${MVNW_PASSWORD:+has-password}" in
+'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+esac
+
+if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
+ verbose "Found wget ... using wget"
+ wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
+elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
+ verbose "Found curl ... using curl"
+ curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
+elif set_java_home; then
+ verbose "Falling back to use Java to download"
+ javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
+ targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
+ cat >"$javaSource" <<-END
+ public class Downloader extends java.net.Authenticator
+ {
+ protected java.net.PasswordAuthentication getPasswordAuthentication()
+ {
+ return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
+ }
+ public static void main( String[] args ) throws Exception
+ {
+ setDefault( new Downloader() );
+ java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
+ }
+ }
+ END
+ # For Cygwin/MinGW, switch paths to Windows format before running javac and java
+ verbose " - Compiling Downloader.java ..."
+ "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
+ verbose " - Running Downloader.java ..."
+ "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
+fi
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+if [ -n "${distributionSha256Sum-}" ]; then
+ distributionSha256Result=false
+ if [ "$MVN_CMD" = mvnd.sh ]; then
+ echo "Checksum validation is not supported for maven-mvnd." >&2
+ echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ elif command -v sha256sum >/dev/null; then
+ if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
+ distributionSha256Result=true
+ fi
+ elif command -v shasum >/dev/null; then
+ if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
+ distributionSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
+ echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ fi
+ if [ $distributionSha256Result = false ]; then
+ echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
+ echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
+ exit 1
+ fi
+fi
+
+# unzip and move
+if command -v unzip >/dev/null; then
+ unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
+else
+ tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
+fi
+printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
+mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
+
+clean || :
+exec_maven "$@"
diff --git a/mvnw.cmd b/mvnw.cmd
new file mode 100644
index 0000000..249bdf3
--- /dev/null
+++ b/mvnw.cmd
@@ -0,0 +1,149 @@
+<# : batch portion
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.3.2
+@REM
+@REM Optional ENV vars
+@REM MVNW_REPOURL - repo url base for downloading maven distribution
+@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
+@REM ----------------------------------------------------------------------------
+
+@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
+@SET __MVNW_CMD__=
+@SET __MVNW_ERROR__=
+@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
+@SET PSModulePath=
+@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
+ IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
+)
+@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
+@SET __MVNW_PSMODULEP_SAVE=
+@SET __MVNW_ARG0_NAME__=
+@SET MVNW_USERNAME=
+@SET MVNW_PASSWORD=
+@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
+@echo Cannot start maven from wrapper >&2 && exit /b 1
+@GOTO :EOF
+: end batch / begin powershell #>
+
+$ErrorActionPreference = "Stop"
+if ($env:MVNW_VERBOSE -eq "true") {
+ $VerbosePreference = "Continue"
+}
+
+# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
+$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
+if (!$distributionUrl) {
+ Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
+}
+
+switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
+ "maven-mvnd-*" {
+ $USE_MVND = $true
+ $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
+ $MVN_CMD = "mvnd.cmd"
+ break
+ }
+ default {
+ $USE_MVND = $false
+ $MVN_CMD = $script -replace '^mvnw','mvn'
+ break
+ }
+}
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
+if ($env:MVNW_REPOURL) {
+ $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
+ $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
+}
+$distributionUrlName = $distributionUrl -replace '^.*/',''
+$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
+$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
+if ($env:MAVEN_USER_HOME) {
+ $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
+}
+$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
+$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
+
+if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
+ Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
+ exit $?
+}
+
+if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
+ Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
+}
+
+# prepare tmp dir
+$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
+$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
+$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
+trap {
+ if ($TMP_DOWNLOAD_DIR.Exists) {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+ }
+}
+
+New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
+
+# Download and Install Apache Maven
+Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+Write-Verbose "Downloading from: $distributionUrl"
+Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+$webclient = New-Object System.Net.WebClient
+if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
+ $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
+}
+[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
+if ($distributionSha256Sum) {
+ if ($USE_MVND) {
+ Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
+ }
+ Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
+ if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
+ Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
+ }
+}
+
+# unzip and move
+Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
+Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
+try {
+ Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
+} catch {
+ if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
+ Write-Error "fail to move MAVEN_HOME"
+ }
+} finally {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+}
+
+Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..c9ab951
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-parent</artifactId>
+ <version>3.4.2</version>
+ <relativePath/> <!-- lookup parent from repository -->
+ </parent>
+ <groupId>org.berzerkula</groupId>
+ <artifactId>builddb</artifactId>
+ <version>0.0.1-SNAPSHOT</version>
+ <name>builddb</name>
+ <description>builddb</description>
+ <url/>
+ <licenses>
+ <license/>
+ </licenses>
+ <developers>
+ <developer/>
+ </developers>
+ <scm>
+ <connection/>
+ <developerConnection/>
+ <tag/>
+ <url/>
+ </scm>
+ <properties>
+ <java.version>17</java.version>
+ </properties>
+ <dependencies>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-data-jpa</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-security</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-thymeleaf</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-validation</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-actuator</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.thymeleaf.extras</groupId>
+ <artifactId>thymeleaf-extras-springsecurity6</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.h2database</groupId>
+ <artifactId>h2</artifactId>
+ <scope>runtime</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.mysql</groupId>
+ <artifactId>mysql-connector-j</artifactId>
+ <scope>runtime</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.security</groupId>
+ <artifactId>spring-security-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>com.github.ulisesbocchio</groupId>
+ <artifactId>jasypt-spring-boot-starter</artifactId>
+ <version>3.0.5</version>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-maven-plugin</artifactId>
+ <configuration>
+ <executable>true</executable>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>com.github.ulisesbocchio</groupId>
+ <artifactId>jasypt-maven-plugin</artifactId>
+ <version>3.0.5</version>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/src/main/java/org/berzerkula/builddb/BuilddbApplication.java b/src/main/java/org/berzerkula/builddb/BuilddbApplication.java
new file mode 100644
index 0000000..7ab9042
--- /dev/null
+++ b/src/main/java/org/berzerkula/builddb/BuilddbApplication.java
@@ -0,0 +1,19 @@
+package org.berzerkula.builddb;
+
+import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@EnableEncryptableProperties
+@SpringBootApplication
+public class BuilddbApplication {
+
+ private static final Logger logger = LoggerFactory.getLogger(BuilddbApplication.class);
+
+ public static void main(String[] args) {
+ SpringApplication.run(BuilddbApplication.class, args);
+ }
+
+}
diff --git a/src/main/java/org/berzerkula/builddb/config/SecurityConfig.java b/src/main/java/org/berzerkula/builddb/config/SecurityConfig.java
new file mode 100644
index 0000000..dbaacd5
--- /dev/null
+++ b/src/main/java/org/berzerkula/builddb/config/SecurityConfig.java
@@ -0,0 +1,79 @@
+package org.berzerkula.builddb.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+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.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+
+@Configuration
+@EnableWebSecurity
+@EnableMethodSecurity
+public class SecurityConfig {
+
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ return http
+ .authorizeHttpRequests( auth -> auth
+ .requestMatchers("/").permitAll()
+ .requestMatchers("/actuator/**").hasRole("admin")
+ .requestMatchers("/env/**").hasRole("admin")
+ .requestMatchers("/health/**").hasRole("admin")
+ .requestMatchers("/info/**").hasRole("admin")
+ .requestMatchers("/contact").permitAll()
+ .requestMatchers("/pkgs/**").hasRole("client")
+ .requestMatchers("/register").permitAll()
+ .requestMatchers("/login").permitAll()
+ .requestMatchers("/logout").permitAll()
+ .anyRequest().authenticated()
+ )
+ .formLogin(form -> form
+ .loginPage("/login")
+ .usernameParameter("email")
+ .passwordParameter("password")
+ .defaultSuccessUrl("/", true)
+ )
+ .logout(config -> config.logoutSuccessUrl("/"))
+ .build();
+ }
+
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/org/berzerkula/builddb/controllers/AccountController.java b/src/main/java/org/berzerkula/builddb/controllers/AccountController.java
new file mode 100644
index 0000000..6cec175
--- /dev/null
+++ b/src/main/java/org/berzerkula/builddb/controllers/AccountController.java
@@ -0,0 +1,141 @@
+package org.berzerkula.builddb.controllers;
+
+import java.util.Date;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.PostMapping;
+
+import org.berzerkula.builddb.models.AppUser;
+import org.berzerkula.builddb.models.RegisterDto;
+import org.berzerkula.builddb.repositories.AppUserRepository;
+
+import jakarta.validation.Valid;
+
+@Controller
+public class AccountController {
+
+ @Autowired
+ private AppUserRepository repo;
+
+ @GetMapping("/profile")
+ public String profile(Authentication auth, Model model) {
+ AppUser user = repo.findByEmail(auth.getName());
+ model.addAttribute("appUser", user);
+
+ return "profile";
+ }
+
+ @GetMapping("/login")
+ public String login() {
+ return "login";
+ }
+
+ @GetMapping("/register")
+ public String register(Model model) {
+ RegisterDto registerDto = new RegisterDto();
+ model.addAttribute(registerDto);
+ model.addAttribute("success", false);
+ return "register";
+ }
+
+ @PostMapping("/register")
+ public String register(
+ Model model,
+ @Valid @ModelAttribute RegisterDto registerDto,
+ BindingResult result
+ ) {
+
+ if (!registerDto.getPassword().equals(registerDto.getConfirmPassword())) {
+ result.addError(
+ new FieldError("registerDto", "confirmPassword"
+ , "Password and Confirm Password do not match")
+ );
+ }
+
+
+ AppUser appUser = repo.findByEmail(registerDto.getEmail());
+ if (appUser != null) {
+ result.addError(
+ new FieldError("registerDto", "email"
+ , "Email address is already used")
+ );
+ }
+
+
+ if (result.hasErrors()) {
+ return "register";
+ }
+
+
+ try {
+ // create new account
+ var bCryptEncoder = new BCryptPasswordEncoder();
+
+
+ AppUser newUser = new AppUser();
+ newUser.setFirstName(registerDto.getFirstName());
+ newUser.setLastName(registerDto.getLastName());
+ newUser.setEmail(registerDto.getEmail());
+ newUser.setPhone(registerDto.getPhone());
+ newUser.setAddress(registerDto.getAddress());
+ newUser.setRole("client");
+ newUser.setCreatedAt(new Date());
+ newUser.setPassword(bCryptEncoder.encode(registerDto.getPassword()));
+
+ repo.save(newUser);
+
+
+ model.addAttribute("registerDto", new RegisterDto());
+ model.addAttribute("success", true);
+ }
+ catch(Exception ex) {
+ result.addError(
+ new FieldError("registerDto", "firstName"
+ , ex.getMessage())
+ );
+ }
+
+ return "register";
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/org/berzerkula/builddb/controllers/DashboardController.java b/src/main/java/org/berzerkula/builddb/controllers/DashboardController.java
new file mode 100644
index 0000000..e928312
--- /dev/null
+++ b/src/main/java/org/berzerkula/builddb/controllers/DashboardController.java
@@ -0,0 +1,27 @@
+package org.berzerkula.builddb.controllers;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+
+@Controller
+public class DashboardController {
+
+ @GetMapping("/user")
+ public String userDashboard() {
+ return "user";
+ }
+
+ @GetMapping("/client")
+ public String clientDashboard() {
+ return "client";
+ }
+
+ @GetMapping("/admin")
+ public String adminDashboard() {
+ return "admin";
+ }
+
+ @GetMapping("/actuatorDashboard")
+ public String actuatorDashboard() { return "actuatorDashboard"; }
+
+}
diff --git a/src/main/java/org/berzerkula/builddb/controllers/HomeController.java b/src/main/java/org/berzerkula/builddb/controllers/HomeController.java
new file mode 100644
index 0000000..ad171c8
--- /dev/null
+++ b/src/main/java/org/berzerkula/builddb/controllers/HomeController.java
@@ -0,0 +1,47 @@
+package org.berzerkula.builddb.controllers;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+
+@Controller
+public class HomeController {
+
+ @GetMapping("/contact")
+ public String contact() {
+ return "contact";
+ }
+
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/org/berzerkula/builddb/controllers/PkgController.java b/src/main/java/org/berzerkula/builddb/controllers/PkgController.java
new file mode 100644
index 0000000..404a58b
--- /dev/null
+++ b/src/main/java/org/berzerkula/builddb/controllers/PkgController.java
@@ -0,0 +1,169 @@
+package org.berzerkula.builddb.controllers;
+
+
+import jakarta.persistence.OrderBy;
+import jakarta.validation.Valid;
+import org.berzerkula.builddb.models.Pkg;
+import org.berzerkula.builddb.models.PkgDto;
+import org.berzerkula.builddb.repositories.PkgRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Sort;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Objects;
+
+@Controller
+@RequestMapping("/pkgs")
+public class PkgController {
+
+ private static final Logger logger = LoggerFactory.getLogger(PkgController.class);
+
+ @Autowired
+ private PkgRepository repo;
+
+ @GetMapping({"", "/", "/index"})
+ public String showPkgList(Model model,
+ @RequestParam(defaultValue = "sequence,asc") String[] sort,
+ @RequestParam(defaultValue = "") String repotable) {
+ try {
+ String sortField = sort[0];
+ String sortDirection = sort[1];
+
+ Sort.Direction direction = sortDirection.equals("desc") ? Sort.Direction.DESC : Sort.Direction.ASC;
+ Sort.Order order = new Sort.Order(direction, sortField);
+
+ List<Pkg> pkgs = repo.findAll(Sort.by(order));
+
+ model.addAttribute("pkgs", pkgs);
+ model.addAttribute("sortField", sortField);
+ model.addAttribute("sortDirection", sortDirection);
+ model.addAttribute("reverseSortDirection", sortDirection.equals("asc") ? "desc" : "asc");
+
+ } catch (Exception e) {
+ logger.error(e.getMessage());
+ }
+
+ return "pkgs/index";
+ }
+
+ @GetMapping("/add")
+ public String showAddPkgForm(Model model) {
+ PkgDto pkgDto = new PkgDto();
+ model.addAttribute("pkgDto", pkgDto);
+ return "pkgs/add";
+ }
+
+ @PostMapping("/add")
+ public String addPkg(
+ @Valid
+ @ModelAttribute
+ PkgDto pkgDto, BindingResult result ) {
+
+ if (result.hasErrors()) {
+ return "pkgs/add";
+ }
+
+ try {
+
+ Pkg pkg = new Pkg();
+ pkg.setSequence(pkgDto.getSequence());
+ pkg.setName(pkgDto.getName());
+ pkg.setVersion(pkgDto.getVersion());
+ pkg.setConfigure(pkgDto.getConfigure());
+ pkg.setBuild(pkgDto.getBuild());
+ pkg.setInstall(pkgDto.getInstall());
+ pkg.setSetup(pkgDto.getSetup());
+ pkg.setNotes(pkgDto.getNotes());
+ pkg.setUrl(pkgDto.getUrl());
+
+ repo.save(pkg);
+
+ } catch (Exception e) {
+ logger.error(e.getMessage());
+ }
+
+ return "redirect:/pkgs";
+ }
+
+ @GetMapping("/edit")
+ public String showEditPkgForm(Model model, @RequestParam int id) {
+
+ try {
+ Pkg pkg = repo.findById(id).get();
+ model.addAttribute("pkg", pkg);
+
+ PkgDto pkgDto = new PkgDto();
+ pkgDto.setSequence(pkg.getSequence());
+ pkgDto.setName(pkg.getName());
+ pkgDto.setVersion(pkg.getVersion());
+ pkgDto.setConfigure(pkg.getConfigure());
+ pkgDto.setBuild(pkg.getBuild());
+ pkgDto.setInstall(pkg.getInstall());
+ pkgDto.setSetup(pkg.getSetup());
+ pkgDto.setNotes(pkg.getNotes());
+ pkgDto.setUrl(pkg.getUrl());
+
+ model.addAttribute("pkgDto", pkgDto);
+
+ } catch(Exception ex) {
+ logger.error("Exception: {}", ex.getMessage());
+ }
+
+ return "pkgs/edit";
+ }
+
+ @PostMapping("/edit")
+ public String editPkg(
+ Model model,
+ @RequestParam int id,
+ @Valid
+ @ModelAttribute PkgDto pkgDto, BindingResult result ) {
+
+ try {
+ Pkg pkg = repo.findById(id).get();
+ model.addAttribute("pkg", pkg);
+
+ if (result.hasErrors()) {
+ return "pkgs/edit";
+ }
+
+ pkg.setSequence(pkgDto.getSequence());
+ pkg.setName(pkgDto.getName());
+ pkg.setVersion(pkgDto.getVersion());
+ pkg.setConfigure(pkgDto.getConfigure());
+ pkg.setBuild(pkgDto.getBuild());
+ pkg.setInstall(pkgDto.getInstall());
+ pkg.setSetup(pkgDto.getSetup());
+ pkg.setNotes(pkgDto.getNotes());
+ pkg.setUrl(pkgDto.getUrl());
+
+ repo.save(pkg);
+
+ } catch(Exception ex) {
+ logger.error("Exception: {}", ex.getMessage());
+ }
+ return "redirect:/pkgs/#" + id;
+
+ }
+
+ @GetMapping("/delete")
+ public String deletePkg(@RequestParam int id) {
+
+ try {
+ Pkg pkg = repo.findById(id).get();
+
+ repo.delete(pkg);
+
+ } catch (Exception ex) {
+ logger.error("Exception: {}", ex.getMessage());
+ }
+
+ return "redirect:/pkgs/";
+ }
+}
diff --git a/src/main/java/org/berzerkula/builddb/models/AppUser.java b/src/main/java/org/berzerkula/builddb/models/AppUser.java
new file mode 100644
index 0000000..21c5a33
--- /dev/null
+++ b/src/main/java/org/berzerkula/builddb/models/AppUser.java
@@ -0,0 +1,107 @@
+package org.berzerkula.builddb.models;
+
+import jakarta.persistence.*;
+
+import java.util.Date;
+
+@Entity
+@Table(name="users")
+public class AppUser {
+
+ @Id
+ @GeneratedValue(strategy=GenerationType.IDENTITY)
+ private int id;
+
+ private String firstName;
+ private String lastName;
+
+ @Column(unique = true, nullable = false)
+ private String email;
+
+ private String phone;
+ private String address;
+ private String password;
+ private String role;
+ private Date createdAt;
+
+ public AppUser(String firstName, String lastName, String email, String phone, String address,
+ String password, String role) {
+ this.firstName = firstName;
+ this.lastName = lastName;
+ this.email = email;
+ this.password = password;
+ this.role = role;
+ this.createdAt = new Date();
+ }
+
+ public AppUser() {}
+
+ public int getId() {
+ return id;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public String getPhone() {
+ return phone;
+ }
+
+ public void setPhone(String phone) {
+ this.phone = phone;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public void setAddress(String address) {
+ this.address = address;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getRole() {
+ return role;
+ }
+
+ public void setRole(String role) {
+ this.role = role;
+ }
+
+ public Date getCreatedAt() {
+ return createdAt;
+ }
+
+ public void setCreatedAt(Date createdAt) {
+ this.createdAt = createdAt;
+ }
+
+}
diff --git a/src/main/java/org/berzerkula/builddb/models/Pkg.java b/src/main/java/org/berzerkula/builddb/models/Pkg.java
new file mode 100644
index 0000000..5ce80ba
--- /dev/null
+++ b/src/main/java/org/berzerkula/builddb/models/Pkg.java
@@ -0,0 +1,118 @@
+package org.berzerkula.builddb.models;
+
+import jakarta.persistence.*;
+
+@Entity
+@Table(name = "rock5b_srv_bld")
+public class Pkg {
+ @Id
+ @GeneratedValue(strategy= GenerationType.IDENTITY)
+ private int id;
+
+ private String name;
+ private Integer sequence;
+ private String version;
+ @Column(columnDefinition = "TEXT")
+ private String configure;
+ @Column(columnDefinition = "TEXT")
+ private String build;
+ @Column(columnDefinition = "TEXT")
+ private String install;
+ @Column(columnDefinition = "TEXT")
+ private String setup;
+ @Column(columnDefinition = "TEXT")
+ private String notes;
+ @Column(columnDefinition = "TEXT")
+ private String url;
+
+ public Pkg(Integer sequence, String name, String version, String configure, String build, String install,
+ String setup, String notes, String url) {
+ this.sequence = sequence;
+ this.name = name;
+ this.version = version;
+ this.configure = configure;
+ this.build = build;
+ this.install = install;
+ this.setup = setup;
+ this.notes = notes;
+ this.url = url;
+ }
+
+ public Pkg() {}
+
+ public int getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Integer getSequence() {
+ return sequence;
+ }
+
+ public void setSequence(Integer seq) {
+ this.sequence = seq;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+ public String getConfigure() {
+ return configure;
+ }
+
+ public void setConfigure(String configure) {
+ this.configure = configure;
+ }
+
+ public String getBuild() {
+ return build;
+ }
+
+ public void setBuild(String build) {
+ this.build = build;
+ }
+
+ public String getInstall() {
+ return install;
+ }
+
+ public void setInstall(String install) {
+ this.install = install;
+ }
+
+ public String getSetup() {
+ return setup;
+ }
+
+ public void setSetup(String setup) {
+ this.setup = setup;
+ }
+
+ public String getNotes() {
+ return notes;
+ }
+
+ public void setNotes(String notes) {
+ this.notes = notes;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+}
diff --git a/src/main/java/org/berzerkula/builddb/models/PkgDto.java b/src/main/java/org/berzerkula/builddb/models/PkgDto.java
new file mode 100644
index 0000000..04c47ea
--- /dev/null
+++ b/src/main/java/org/berzerkula/builddb/models/PkgDto.java
@@ -0,0 +1,104 @@
+package org.berzerkula.builddb.models;
+
+import jakarta.validation.constraints.Digits;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import org.springframework.data.annotation.ReadOnlyProperty;
+
+public class PkgDto {
+ @ReadOnlyProperty
+ private Integer id;
+
+ @NotNull(message = "Required")
+ @Digits(integer = 4, fraction = 0)
+ private Integer sequence;
+
+ @NotEmpty(message = "Required")
+ private String name;
+
+ @NotEmpty(message = "Required")
+ private String version;
+
+ private String configure;
+ private String build;
+ private String install;
+ private String setup;
+ private String notes;
+ private String url;
+
+ public Integer getSequence() {
+ return sequence;
+ }
+
+ public void setSequence(Integer sequence) {
+ this.sequence = sequence;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+ public String getConfigure() {
+ return configure;
+ }
+
+ public void setConfigure(String configure) {
+ this.configure = configure;
+ }
+
+ public String getBuild() {
+ return build;
+ }
+
+ public void setBuild(String build) {
+ this.build = build;
+ }
+
+ public String getInstall() {
+ return install;
+ }
+
+ public void setInstall(String install) {
+ this.install = install;
+ }
+
+ public String getSetup() {
+ return setup;
+ }
+
+ public void setSetup(String setup) {
+ this.setup = setup;
+ }
+
+ public String getNotes() {
+ return notes;
+ }
+
+ public void setNotes(String notes) {
+ this.notes = notes;
+ }
+
+ public Integer getId() {
+ return id;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+}
diff --git a/src/main/java/org/berzerkula/builddb/models/RegisterDto.java b/src/main/java/org/berzerkula/builddb/models/RegisterDto.java
new file mode 100644
index 0000000..eebf014
--- /dev/null
+++ b/src/main/java/org/berzerkula/builddb/models/RegisterDto.java
@@ -0,0 +1,84 @@
+package org.berzerkula.builddb.models;
+
+import jakarta.validation.constraints.*;
+
+public class RegisterDto {
+
+ @NotEmpty
+ private String firstName;
+
+ @NotEmpty
+ private String lastName;
+
+ @NotEmpty
+ @Email
+ private String email;
+
+ private String phone;
+
+ private String address;
+
+ @Size(min = 6, message = "Minimum Password length is 6 characters")
+ private String password;
+
+ private String confirmPassword;
+
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public String getPhone() {
+ return phone;
+ }
+
+ public void setPhone(String phone) {
+ this.phone = phone;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public void setAddress(String address) {
+ this.address = address;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getConfirmPassword() {
+ return confirmPassword;
+ }
+
+ public void setConfirmPassword(String confirmPassword) {
+ this.confirmPassword = confirmPassword;
+ }
+
+
+}
diff --git a/src/main/java/org/berzerkula/builddb/repositories/AppUserRepository.java b/src/main/java/org/berzerkula/builddb/repositories/AppUserRepository.java
new file mode 100644
index 0000000..756d56a
--- /dev/null
+++ b/src/main/java/org/berzerkula/builddb/repositories/AppUserRepository.java
@@ -0,0 +1,10 @@
+package org.berzerkula.builddb.repositories;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import org.berzerkula.builddb.models.AppUser;
+
+public interface AppUserRepository extends JpaRepository<AppUser, Integer> {
+
+ public AppUser findByEmail(String email);
+}
diff --git a/src/main/java/org/berzerkula/builddb/repositories/PkgRepository.java b/src/main/java/org/berzerkula/builddb/repositories/PkgRepository.java
new file mode 100644
index 0000000..30e80eb
--- /dev/null
+++ b/src/main/java/org/berzerkula/builddb/repositories/PkgRepository.java
@@ -0,0 +1,8 @@
+package org.berzerkula.builddb.repositories;
+
+import org.berzerkula.builddb.models.Pkg;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface PkgRepository extends JpaRepository<Pkg, Integer> {
+
+}
diff --git a/src/main/java/org/berzerkula/builddb/services/AppUserService.java b/src/main/java/org/berzerkula/builddb/services/AppUserService.java
new file mode 100644
index 0000000..02cc89f
--- /dev/null
+++ b/src/main/java/org/berzerkula/builddb/services/AppUserService.java
@@ -0,0 +1,36 @@
+package org.berzerkula.builddb.services;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+import org.berzerkula.builddb.models.AppUser;
+import org.berzerkula.builddb.repositories.AppUserRepository;
+
+@Service
+public class AppUserService implements UserDetailsService {
+ @Autowired
+ private AppUserRepository repo;
+
+ @Override
+ public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
+ AppUser appUser = repo.findByEmail(email);
+
+
+ if (appUser != null) {
+ var springUser = User.withUsername(appUser.getEmail())
+ .password(appUser.getPassword())
+ .roles(appUser.getRole())
+ .build();
+
+ return springUser;
+ }
+
+
+ return null;
+ }
+
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
new file mode 100644
index 0000000..f6a7814
--- /dev/null
+++ b/src/main/resources/application.properties
@@ -0,0 +1,15 @@
+spring.application.name=builddb
+
+spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
+spring.datasource.url=jdbc:mysql://nucleus.berzerkula.org:3306/builddb_users
+spring.datasource.username=kb0iic
+spring.datasource.password=ENC(1xjvlowHDUgzAYCjEEaYcSQOQHL2SHo8)
+
+spring.jpa.show-sql=true
+spring.jpa.hibernate.ddl-auto=update
+
+jasypt.encryptor.algorithm=PBEWithMD5AndDES
+jasypt.encryptor.iv-generator-classname=org.jasypt.iv.NoIvGenerator
+jasypt.encryptor.password=builddb
+
+management.endpoints.web.exposure.include=* \ No newline at end of file
diff --git a/src/main/resources/templates/actuatorDashboard.html b/src/main/resources/templates/actuatorDashboard.html
new file mode 100644
index 0000000..e97bb8f
--- /dev/null
+++ b/src/main/resources/templates/actuatorDashboard.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html xmlns:th="http://www.thymeleaf.org">
+<head>
+ <title>spring-boot-actuator</title>
+ <meta charset="UTF-8" />
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
+</head>
+<body>
+<div id="app" class="container">
+ <nav class="navbar navbar-expand-lg fixed-top bg-body-tertiary border-bottom">
+ <div class="container">
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
+ <span class="navbar-toggler-icon"></span>
+ </button>
+ <div class="collapse navbar-collapse" id="navbarSupportedContent">
+ <a class="navbar-brand" href="/actuatorDashboard">spring-boot-actuator</a>
+ <ul class="navbar-nav me-auto mb-2 mb-lg-0">
+ <li class="nav-link text-dark"><a th:href="@{/actuator/metrics}">/metrics</a></li>
+ <li class="nav-link text-dark"><a th:href="@{/actuator/env}">/env</a></li>
+ <li class="nav-link text-dark"><a th:href="@{/actuator/dump}">/dump</a></li>
+ <li class="nav-link text-dark"><a th:href="@{/actuator/health}">/health</a></li>
+ <li class="nav-link text-dark"><a th:href="@{/actuator/beans}">/beans</a></li>
+ <li class="nav-link text-dark"><a th:href="@{/actuator/autoconfig}">/autoconfig</a></li>
+ <li class="nav-link text-dark"><a th:href="@{/actuator/info}">/info</a></li>
+ <li class="nav-link text-dark"><a th:href="@{/actuator/shutdown}">/shutdown</a></li>
+ </ul>
+ </div>
+ </div>
+ </nav>
+
+ <hr>
+ <hr>
+
+ <a href="/" class="btn btn-link">Home</a>
+</div>
+
+<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/resources/templates/admin.html b/src/main/resources/templates/admin.html
new file mode 100644
index 0000000..16f8f7f
--- /dev/null
+++ b/src/main/resources/templates/admin.html
@@ -0,0 +1,53 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>builddb</title>
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
+</head>
+<body>
+
+<div class="container py-5">
+ <div class="rounded border p-4">
+ <h2 class="text-center mb-4">Admin Page</h2>
+ <hr>
+ <a href="/" class="btn btn-link">Home</a>
+ </div>
+</div>
+
+
+<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
+</body>
+</html>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/templates/client.html b/src/main/resources/templates/client.html
new file mode 100644
index 0000000..487d427
--- /dev/null
+++ b/src/main/resources/templates/client.html
@@ -0,0 +1,53 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>builddb</title>
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
+</head>
+<body>
+
+<div class="container py-5">
+ <div class="rounded border p-4">
+ <h2 class="text-center mb-4">Client Page</h2>
+ <hr>
+ <a href="/" class="btn btn-link">Home</a>
+ </div>
+</div>
+
+
+<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
+</body>
+</html>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/templates/contact.html b/src/main/resources/templates/contact.html
new file mode 100644
index 0000000..f1c6a3f
--- /dev/null
+++ b/src/main/resources/templates/contact.html
@@ -0,0 +1,53 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>builddb</title>
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
+</head>
+<body>
+
+<div class="container py-5">
+ <div class="rounded border p-4">
+ <h2 class="text-center mb-4">Contact</h2>
+ <hr>
+ <a href="/" class="btn btn-link">Home</a>
+ </div>
+</div>
+
+
+<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
+</body>
+</html>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html
new file mode 100644
index 0000000..b33e45f
--- /dev/null
+++ b/src/main/resources/templates/index.html
@@ -0,0 +1,37 @@
+<!doctype html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org"
+ xmlns:sec="http://www.thymeleaf.org">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>builddb</title>
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
+</head>
+<body>
+
+<nav th:insert="~{/navbar :: navigation}"></nav>
+
+<hr>
+
+<div class="container py-5">
+ <h1 class="text-center">Builddb</h1>
+
+
+
+ <a class="btn btn-primary me-3" sec:authorize="hasRole('client')" href="/pkgs">Packages</a>
+</div>
+
+<hr>
+
+<div th:if="${tableError}"
+ class="alert alert-danger alert-dismissible fade show" role="alert">
+
+ <strong>Invalid table!</strong>
+ <button type="button" class="btn-close" data-bs-dismiss="alert"
+ aria-label="Close"></button>
+</div>
+
+<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html
new file mode 100644
index 0000000..0893fa0
--- /dev/null
+++ b/src/main/resources/templates/login.html
@@ -0,0 +1,87 @@
+<!doctype html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org"
+ xmlns:sec="http://www.thymeleaf.org">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>builddb</title>
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
+</head>
+<body>
+
+<div class="container py-5">
+ <div class="mx-auto rounded border p-4" style="width: 400px">
+ <h2 class="text-center mb-4">Login</h2>
+ <hr />
+
+
+ <div th:if="${param.error}"
+ class="alert alert-danger alert-dismissible fade show" role="alert">
+
+ <strong>Invalid Email or Password!</strong>
+ <button type="button" class="btn-close" data-bs-dismiss="alert"
+ aria-label="Close"></button>
+ </div>
+
+ <form method="post">
+ <input type="hidden" th:name="${_csrf.parameterName}"
+ th:value="${_csrf.token}" />
+
+
+ <div class="mb-3">
+ <label class="form-label">Email</label>
+ <input class="form-control" name="email" />
+ </div>
+
+ <div class="mb-3">
+ <label class="form-label">Password</label>
+ <input class="form-control" type="password" name="password" />
+ </div>
+
+ <div class="row mb-3">
+ <div class="col d-grid">
+ <button type="submit" class="btn btn-primary">Submit</button>
+ </div>
+ <div class="col d-grid">
+ <a href="/" class="btn btn-outline-primary">Cancel</a>
+ </div>
+ </div>
+ </form>
+ </div>
+</div>
+
+
+<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
+</body>
+</html>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/templates/navbar.html b/src/main/resources/templates/navbar.html
new file mode 100644
index 0000000..0aa60e0
--- /dev/null
+++ b/src/main/resources/templates/navbar.html
@@ -0,0 +1,77 @@
+<nav th:fragment="navigation" class="navbar navbar-expand-lg fixed-top bg-body-tertiary border-bottom">
+ <div class="container">
+ <a class="navbar-brand" href="/">builddb</a>
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
+ <span class="navbar-toggler-icon"></span>
+ </button>
+ <div class="collapse navbar-collapse" id="navbarSupportedContent">
+ <ul class="navbar-nav me-auto mb-2 mb-lg-0">
+ <li class="nav-item">
+ <a class="nav-link text-dark" href="/">Home</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link text-dark" href="/contact">Contact</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link text-dark" href="/user">User</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link text-dark" sec:authorize="hasRole('client')" href="/client">Client</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link text-dark" sec:authorize="hasRole('admin')" href="/admin">Admin</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link text-dark" sec:authorize="hasRole('admin')" href="/actuatorDashboard">Actuator</a>
+ </li>
+ </ul>
+
+ <ul class="navbar-nav me-3" sec:authorize="hasRole('admin')">
+ <li class="nav-item dropdown">
+ <a class="nav-link dropdown-toggle text-dark" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
+ Admin
+ </a>
+ <ul class="dropdown-menu">
+ <li><a class="dropdown-item" href="/profile">Profile</a></li>
+ <li><a class="dropdown-item" href="/">Home</a></li>
+ </ul>
+ </li>
+ </ul>
+
+ <ul class="navbar-nav me-3" sec:authorize="hasRole('client')">
+ <li class="nav-item dropdown">
+ <a class="nav-link dropdown-toggle text-dark" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
+ Client
+ </a>
+ <ul class="dropdown-menu">
+ <li><a class="dropdown-item" href="/profile">Profile</a></li>
+ <li><a class="dropdown-item" href="/">Home</a></li>
+ </ul>
+ </li>
+ </ul>
+
+ <form sec:authorize="isAuthenticated()" method="post" action="/logout">
+ <input type="hidden" th:name="${_csrf.parameterName}"
+ th:value="${_csrf.token}" />
+
+ <button type="submit" class="btn btn-danger">
+ <i class="fa-solid fa-right-from-bracket"></i>
+ </button>
+ </form>
+
+ <ul class="navbar-nav" sec:authorize="!isAuthenticated()">
+ <li class="nav-item">
+ <a href="/register" class="btn btn-outline-primary me-2">
+ <i class="fa-regular fa-id-card"></i>
+ </a>
+ </li>
+ <li class="nav-item">
+ <a href="/login" class="btn btn-primary">
+ <i class="fa-solid fa-right-to-bracket"></i>
+ </a>
+ </li>
+ </ul>
+
+ </div>
+ </div>
+</nav> \ No newline at end of file
diff --git a/src/main/resources/templates/pkgs/add.html b/src/main/resources/templates/pkgs/add.html
new file mode 100644
index 0000000..107784f
--- /dev/null
+++ b/src/main/resources/templates/pkgs/add.html
@@ -0,0 +1,109 @@
+<!doctype html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org"
+ xmlns:sec="http://www.thymeleaf.org">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>builddb</title>
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
+</head>
+<body>
+
+<nav th:insert="~{/navbar :: navigation}"></nav>
+
+<hr>
+
+<div class="container">
+ <div class="col-md-8 mx-auto rounded border p-4 m-4">
+ <h2 class="text-center mb-5">Add Package</h2>
+
+ <form method="post" enctype="multipart/form-data" th:object="${pkgDto}">
+ <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">Sequence</label>
+ <div class="col-sm-8">
+ <input class="form-control" th:field="${pkgDto.sequence}" />
+ <p th:if="${#fields.hasErrors('sequence')}" th:errorclass="text-danger" th:errors="${pkgDto.sequence}"></p>
+ </div>
+ </div>
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">Name</label>
+ <div class="col-sm-8">
+ <input class="form-control" th:field="${pkgDto.name}" />
+ <p th:if="${#fields.hasErrors('name')}" th:errorclass="text-danger" th:errors="${pkgDto.name}"></p>
+ </div>
+ </div>
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">Version</label>
+ <div class="col-sm-8">
+ <input class="form-control" th:field="${pkgDto.version}" />
+ <p th:if="${#fields.hasErrors('version')}" th:errorclass="text-danger" th:errors="${pkgDto.version}"></p>
+ </div>
+ </div>
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">Configure</label>
+ <div class="col-sm-8">
+ <textarea class="form-control" th:field="${pkgDto.configure}" />
+ <p th:if="${#fields.hasErrors('configure')}" th:errorclass="text-danger" th:errors="${pkgDto.configure}"></p>
+ </div>
+ </div>
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">Build</label>
+ <div class="col-sm-8">
+ <textarea class="form-control" th:field="${pkgDto.build}" />
+ <p th:if="${#fields.hasErrors('build')}" th:errorclass="text-danger" th:errors="${pkgDto.build}"></p>
+ </div>
+ </div>
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">Install</label>
+ <div class="col-sm-8">
+ <textarea class="form-control" th:field="${pkgDto.install}" />
+ <p th:if="${#fields.hasErrors('install')}" th:errorclass="text-danger" th:errors="${pkgDto.install}"></p>
+ </div>
+ </div>
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">Setup</label>
+ <div class="col-sm-8">
+ <textarea class="form-control" th:field="${pkgDto.setup}" />
+ <p th:if="${#fields.hasErrors('setup')}" th:errorclass="text-danger" th:errors="${pkgDto.setup}"></p>
+ </div>
+ </div>
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">Notes</label>
+ <div class="col-sm-8">
+ <textarea class="form-control" th:field="${pkgDto.notes}" />
+ <p th:if="${#fields.hasErrors('notes')}" th:errorclass="text-danger" th:errors="${pkgDto.notes}"></p>
+ </div>
+ </div>
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">Url</label>
+ <div class="col-sm-8">
+ <textarea class="form-control" th:field="${pkgDto.url}" />
+ <p th:if="${#fields.hasErrors('Url')}" th:errorclass="text-danger" th:errors="${pkgDto.url}"></p>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="offset-sm-4 col-sm-4 d-grid">
+ <button type="submit" class="btn btn-primary">Submit</button>
+ </div>
+ <div class="col-sm-4 d-grid">
+ <a class="btn btn-outline-primary" href="/pkgs" role="button">Cancel</a>
+ </div>
+ </div>
+ </form>
+ </div>
+</div>
+
+<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/resources/templates/pkgs/edit.html b/src/main/resources/templates/pkgs/edit.html
new file mode 100644
index 0000000..34ad9a8
--- /dev/null
+++ b/src/main/resources/templates/pkgs/edit.html
@@ -0,0 +1,109 @@
+<!doctype html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org"
+ xmlns:sec="http://www.thymeleaf.org">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>builddb</title>
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
+</head>
+<body>
+
+<nav th:insert="~{/navbar :: navigation}"></nav>
+
+<hr>
+
+<div class="container">
+ <div class="col-md-8 mx-auto rounded border p-4 m-4">
+ <h2 class="text-center mb-5">Edit Package</h2>
+
+ <form method="post" enctype="multipart/form-data" th:object="${pkgDto}">
+ <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">Sequence</label>
+ <div class="col-sm-8">
+ <input class="form-control" th:field="${pkgDto.sequence}" />
+ <p th:if="${#fields.hasErrors('sequence')}" th:errorclass="text-danger" th:errors="${pkgDto.sequence}"></p>
+ </div>
+ </div>
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">Name</label>
+ <div class="col-sm-8">
+ <input class="form-control" th:field="${pkgDto.name}" />
+ <p th:if="${#fields.hasErrors('name')}" th:errorclass="text-danger" th:errors="${pkgDto.name}"></p>
+ </div>
+ </div>
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">Version</label>
+ <div class="col-sm-8">
+ <input class="form-control" th:field="${pkgDto.version}" />
+ <p th:if="${#fields.hasErrors('version')}" th:errorclass="text-danger" th:errors="${pkgDto.version}"></p>
+ </div>
+ </div>
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">Configure</label>
+ <div class="col-sm-8">
+ <textarea class="form-control" th:field="${pkgDto.configure}" />
+ <p th:if="${#fields.hasErrors('configure')}" th:errorclass="text-danger" th:errors="${pkgDto.configure}"></p>
+ </div>
+ </div>
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">Build</label>
+ <div class="col-sm-8">
+ <textarea class="form-control" th:field="${pkgDto.build}" />
+ <p th:if="${#fields.hasErrors('build')}" th:errorclass="text-danger" th:errors="${pkgDto.build}"></p>
+ </div>
+ </div>
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">Install</label>
+ <div class="col-sm-8">
+ <textarea class="form-control" th:field="${pkgDto.install}" />
+ <p th:if="${#fields.hasErrors('install')}" th:errorclass="text-danger" th:errors="${pkgDto.install}"></p>
+ </div>
+ </div>
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">Setup</label>
+ <div class="col-sm-8">
+ <textarea class="form-control" th:field="${pkgDto.setup}" />
+ <p th:if="${#fields.hasErrors('setup')}" th:errorclass="text-danger" th:errors="${pkgDto.setup}"></p>
+ </div>
+ </div>
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">Notes</label>
+ <div class="col-sm-8">
+ <textarea class="form-control" th:field="${pkgDto.notes}" />
+ <p th:if="${#fields.hasErrors('notes')}" th:errorclass="text-danger" th:errors="${pkgDto.notes}"></p>
+ </div>
+ </div>
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">Url</label>
+ <div class="col-sm-8">
+ <textarea class="form-control" th:field="${pkgDto.url}" />
+ <p th:if="${#fields.hasErrors('Url')}" th:errorclass="text-danger" th:errors="${pkgDto.url}"></p>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="offset-sm-4 col-sm-4 d-grid">
+ <button type="submit" class="btn btn-primary">Submit</button>
+ </div>
+ <div class="col-sm-4 d-grid">
+ <a class="btn btn-outline-primary" href="/pkgs/" role="button">Cancel</a>
+ </div>
+ </div>
+ </form>
+ </div>
+</div>
+
+<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/resources/templates/pkgs/index.html b/src/main/resources/templates/pkgs/index.html
new file mode 100644
index 0000000..04eb45a
--- /dev/null
+++ b/src/main/resources/templates/pkgs/index.html
@@ -0,0 +1,62 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Builddb</title>
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
+</head>
+<body>
+<nav th:insert="~{/navbar :: navigation}"></nav>
+
+<hr>
+<hr>
+
+<div class="container">
+ <h1 class="text-center my-4">Packages</h1>
+ <a class="btn btn-primary fa fa-add" href="/pkgs/add"></a>
+
+ <hr>
+
+ <table class="table table-bordered table-hover table-striped">
+ <thead>
+ <tr>
+ <th id="0" th:replace="~{pkgs/sorting :: sorting('sequence', 'Seq')}">Seq</th>
+ <th th:replace="~{pkgs/sorting :: sorting('name', 'Name')}">Name</th>
+ <th>Version</th>
+ <th>Configure</th>
+ <th>Build</th>
+ <th>Install</th>
+ <th>Setup</th>
+ <th>Notes</th>
+ <th>Url</th>
+ <th>Action</th>
+ </tr>
+ </thead>
+ <tbody class="table-group-divider">
+ <tr th:each="pkg : ${pkgs}">
+ <td th:id="${pkg.id}" th:text="${pkg.sequence}"></td>
+ <td th:text="${pkg.name}"></td>
+ <td th:text="${pkg.version}"></td>
+ <td th:text="${pkg.configure}"></td>
+ <td th:text="${pkg.build}"></td>
+ <td th:text="${pkg.install}"></td>
+ <td th:text="${pkg.setup}"></td>
+ <td th:text="${pkg.notes}"></td>
+ <td th:text="${pkg.url}"></td>
+ <td style="white-space:nowrap">
+ <a class="btn btn-primary btn-sm fa fa-edit"
+ th:href="@{/pkgs/edit(id=${pkg.id})}"></a>
+ <a class="btn btn-danger btn-sm fa fa-trash"
+ th:href="@{/pkgs/delete(id=${pkg.id})}"
+ onclick="return confirm('Delete?')"></a>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+
+<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/resources/templates/pkgs/sorting.html b/src/main/resources/templates/pkgs/sorting.html
new file mode 100644
index 0000000..199ef68
--- /dev/null
+++ b/src/main/resources/templates/pkgs/sorting.html
@@ -0,0 +1,6 @@
+<th scope="col" th:fragment="sorting(field, label)">
+ <a class="text-decoration-none text-dark"
+ th:href="@{'/pkgs?' + ${sortField!=null ? '&sort=' + field + ',' + (sortField == field ? reverseSortDirection : sortDirection) : ''}}">[[${label}]]</a>
+ <span th:if="${sortField == field}"
+ th:class="${sortDirection == 'asc' ? 'fas fa-arrow-down-short-wide' : 'fas fa-arrow-down-wide-short'}"></span>
+</th> \ No newline at end of file
diff --git a/src/main/resources/templates/profile.html b/src/main/resources/templates/profile.html
new file mode 100644
index 0000000..aa7fb20
--- /dev/null
+++ b/src/main/resources/templates/profile.html
@@ -0,0 +1,84 @@
+<!doctype html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>builddb</title>
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
+</head>
+<body>
+
+<div class="container py-5">
+ <div class="rounded border p-4">
+ <h2 class="text-center mb-4">Profile</h2>
+ <hr>
+
+ <div class="row mb-2">
+ <div class="col">First Name</div>
+ <div class="col" th:text="${appUser.firstName}"></div>
+ </div>
+ <div class="row mb-2">
+ <div class="col">Last Name</div>
+ <div class="col" th:text="${appUser.lastName}"></div>
+ </div>
+ <div class="row mb-2">
+ <div class="col">Email</div>
+ <div class="col" th:text="${appUser.email}"></div>
+ </div>
+ <div class="row mb-2">
+ <div class="col">Phone</div>
+ <div class="col" th:text="${appUser.phone}"></div>
+ </div>
+ <div class="row mb-2">
+ <div class="col">Address</div>
+ <div class="col" th:text="${appUser.address}"></div>
+ </div>
+ <div class="row mb-2">
+ <div class="col">Role</div>
+ <div class="col" th:text="${appUser.role}"></div>
+ </div>
+ <div class="row mb-2">
+ <div class="col">Created At</div>
+ <div class="col" th:text="${appUser.createdAt}"></div>
+ </div>
+
+ <hr>
+ <a href="/" class="btn btn-link">Home</a>
+ </div>
+</div>
+
+
+<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
+</body>
+</html>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/templates/register.html b/src/main/resources/templates/register.html
new file mode 100644
index 0000000..31f240b
--- /dev/null
+++ b/src/main/resources/templates/register.html
@@ -0,0 +1,155 @@
+<!doctype html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>builddb</title>
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
+</head>
+<body>
+
+<div class="container py-5">
+ <div class="row">
+ <div class="col-lg-6 mx-auto rounded border p-4">
+ <h2 class="text-center mb-4">Register</h2>
+ <hr />
+
+
+
+ <div th:if="${success}"
+ class="alert alert-success alert-dismissible fade show" role="alert">
+
+ <strong>Account Created Successfully!</strong>
+ <a class="ms-2" href="/login">Login</a>
+ <button type="button" class="btn-close" data-bs-dismiss="alert"
+ aria-label="Close"></button>
+ </div>
+
+
+ <form method="post" th:object="${registerDto}">
+ <input type="hidden" th:name="${_csrf.parameterName}"
+ th:value="${_csrf.token}" />
+
+
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">First Name*</label>
+ <div class="col-sm-8">
+ <input class="form-control" th:field="${registerDto.firstName}" >
+ <p th:if="${#fields.hasErrors('firstName')}"
+ th:errorclass="text-danger"
+ th:errors="${registerDto.firstName}" ></p>
+ </div>
+ </div>
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">Last Name*</label>
+ <div class="col-sm-8">
+ <input class="form-control" th:field="${registerDto.lastName}" >
+ <p th:if="${#fields.hasErrors('lastName')}"
+ th:errorclass="text-danger"
+ th:errors="${registerDto.lastName}" ></p>
+ </div>
+ </div>
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">Email*</label>
+ <div class="col-sm-8">
+ <input class="form-control" th:field="${registerDto.email}" >
+ <p th:if="${#fields.hasErrors('email')}"
+ th:errorclass="text-danger"
+ th:errors="${registerDto.email}" ></p>
+ </div>
+ </div>
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">Phone</label>
+ <div class="col-sm-8">
+ <input class="form-control" th:field="${registerDto.phone}" >
+ <p th:if="${#fields.hasErrors('phone')}"
+ th:errorclass="text-danger"
+ th:errors="${registerDto.phone}" ></p>
+ </div>
+ </div>
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">Address</label>
+ <div class="col-sm-8">
+ <input class="form-control" th:field="${registerDto.address}" >
+ <p th:if="${#fields.hasErrors('address')}"
+ th:errorclass="text-danger"
+ th:errors="${registerDto.address}" ></p>
+ </div>
+ </div>
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">Password*</label>
+ <div class="col-sm-8">
+ <input class="form-control" type="password"
+ th:field="${registerDto.password}" >
+ <p th:if="${#fields.hasErrors('password')}"
+ th:errorclass="text-danger"
+ th:errors="${registerDto.password}" ></p>
+ </div>
+ </div>
+
+ <div class="row mb-3">
+ <label class="col-sm-4 col-form-label">Confirm Password*</label>
+ <div class="col-sm-8">
+ <input class="form-control" type="password"
+ th:field="${registerDto.confirmPassword}" >
+ <p th:if="${#fields.hasErrors('confirmPassword')}"
+ th:errorclass="text-danger"
+ th:errors="${registerDto.confirmPassword}" ></p>
+ </div>
+ </div>
+
+ <div class="row mb-3">
+ <div class="offset-sm-4 col-sm-4 d-grid">
+ <button type="submit" class="btn btn-primary">Submit</button>
+ </div>
+ <div class="col-sm-4 d-grid">
+ <a href="/" class="btn btn-outline-primary">Cancel</a>
+ </div>
+ </div>
+
+ </form>
+ </div>
+ </div>
+</div>
+
+
+<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
+</body>
+</html>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/templates/user.html b/src/main/resources/templates/user.html
new file mode 100644
index 0000000..2791644
--- /dev/null
+++ b/src/main/resources/templates/user.html
@@ -0,0 +1,53 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>builddb</title>
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
+</head>
+<body>
+
+<div class="container py-5">
+ <div class="rounded border p-4">
+ <h2 class="text-center mb-4">User Page</h2>
+ <hr>
+ <a href="/" class="btn btn-link">Home</a>
+ </div>
+</div>
+
+
+<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
+</body>
+</html>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/java/org/berzerkula/builddb/BuilddbApplicationAppUserTests.java b/src/test/java/org/berzerkula/builddb/BuilddbApplicationAppUserTests.java
new file mode 100644
index 0000000..0cf02d1
--- /dev/null
+++ b/src/test/java/org/berzerkula/builddb/BuilddbApplicationAppUserTests.java
@@ -0,0 +1,111 @@
+package org.berzerkula.builddb;
+
+import org.berzerkula.builddb.models.AppUser;
+import org.berzerkula.builddb.repositories.TestH2AppUserRepository;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import java.util.Date;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+class BuilddbApplicationAppUserTests {
+
+ /*@LocalServerPort
+ private int port;
+
+ private String baseUrl = "http://localhost:" + port + "/";
+ private String basePkgUrl = "http://localhost:" + port + "/pkgs";*/
+
+ private String firstName = "John";
+ private String lastName = "Doe";
+ private String email = "john.doe@example.com";
+ private String phone = "870-555-5555";
+ private String address = "612 Wolf Avenue";
+ private String password = "johndoesecret";
+ private String role = "client";
+ private Date createdAt = new Date();
+
+ @Autowired
+ private TestH2AppUserRepository h2Repository;
+
+ private AppUser testUser = new AppUser();
+
+ @BeforeEach
+ public void setUp() {
+ h2Repository.deleteAll();
+ testUser = setUserDetails(firstName, lastName, email, phone, address, password, role, createdAt);
+ h2Repository.save(testUser);
+ }
+
+ @Test
+ public void testAddAppUser() {
+ testUser = setUserDetails(firstName, lastName, "john.doe2@example.com", phone, address, password, role, createdAt);
+ h2Repository.save(testUser);
+ List<AppUser> responsePkgs = h2Repository.findAll();
+ assertNotNull(responsePkgs);
+ assertEquals(2, responsePkgs.size());
+ assertEquals(firstName, responsePkgs.get(1).getFirstName());
+ assertEquals(lastName, responsePkgs.get(1).getLastName());
+ assertEquals("john.doe2@example.com", responsePkgs.get(1).getEmail());
+ assertEquals(phone, responsePkgs.get(1).getPhone());
+ assertEquals(address, responsePkgs.get(1).getAddress());
+ assertEquals(password, responsePkgs.get(1).getPassword());
+ assertEquals(role, responsePkgs.get(1).getRole());
+ }
+
+ @Test
+ public void testGetAppUser() {
+ h2Repository.save(testUser);
+ List<AppUser> users = h2Repository.findAll();
+ assertEquals(1, users.size());
+ assertEquals(1, h2Repository.findAll().size());
+ assertEquals(createdAt, users.get(0).getCreatedAt());
+ }
+
+ @Test
+ public void testUpdateAppUser() {
+ List<AppUser> users = h2Repository.findAll();
+ testUser = users.get(0);
+ testUser.setFirstName("Jack");
+ h2Repository.save(testUser);
+
+ users = h2Repository.findAll();
+ assertEquals(1, users.size());
+ assertNotEquals(0, testUser.getId());
+ assertEquals("Jack", testUser.getFirstName());
+ }
+
+ @Test
+ public void tsetDeleteAppUser() {
+ List<AppUser> users = h2Repository.findAll();
+ testUser = users.get(0);
+ h2Repository.delete(testUser);
+ users = h2Repository.findAll();
+
+ assertEquals(0, users.size());
+ }
+
+ private AppUser setUserDetails(String firstName, String lastName, String email, String phone,
+ String address, String password, String role, Date createdAt) {
+ AppUser testUser = new AppUser();
+ testUser.setFirstName(firstName);
+ testUser.setLastName(lastName);
+ testUser.setEmail(email);
+ testUser.setPhone(phone);
+ testUser.setAddress(address);
+ testUser.setPassword(password);
+ testUser.setRole(role);
+ testUser.setCreatedAt(createdAt);
+ return testUser;
+ }
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/src/test/java/org/berzerkula/builddb/BuilddbApplicationPkgTests.java b/src/test/java/org/berzerkula/builddb/BuilddbApplicationPkgTests.java
new file mode 100644
index 0000000..98272f6
--- /dev/null
+++ b/src/test/java/org/berzerkula/builddb/BuilddbApplicationPkgTests.java
@@ -0,0 +1,113 @@
+package org.berzerkula.builddb;
+
+import org.berzerkula.builddb.models.Pkg;
+import org.berzerkula.builddb.repositories.TestH2PkgRepository;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+class BuilddbApplicationPkgTests {
+
+ /*@LocalServerPort
+ private int port;
+
+ private String baseUrl = "http://localhost:" + port + "/";
+ private String basePkgUrl = "http://localhost:" + port + "/pkgs";*/
+
+ private Integer pkgSeq = 123;
+ private String pkgName = "test";
+ private String pkgVersion = "1.2.3-test";
+ private String pkgConfigure = "./configure --prefix=/opt/test";
+ private String pkgBuild = "make test";
+ private String pkgInstall = "make install-test";
+ private String pkgSetup = "test setup.";
+ private String pkgNotes = "test notes.";
+ private String pkgUrl = "http://localhost/test-1.2.3-test.tar.gz";
+
+ @Autowired
+ private TestH2PkgRepository h2Repository;
+
+ private Pkg testPkg = new Pkg();
+
+ @BeforeEach
+ public void setUp() {
+ h2Repository.deleteAll();
+ testPkg = setPkgDetails(pkgSeq, pkgName, pkgVersion, pkgConfigure, pkgBuild, pkgInstall, pkgSetup, pkgNotes, pkgUrl);
+ h2Repository.save(testPkg);
+ }
+
+ @Test
+ public void testAddPkg() {
+ testPkg = setPkgDetails(pkgSeq, pkgName, pkgVersion, pkgConfigure, pkgBuild, pkgInstall, pkgSetup, pkgNotes, pkgUrl);
+ h2Repository.save(testPkg);
+ List<Pkg> responsePkgs = h2Repository.findAll();
+ assertNotNull(responsePkgs);
+ assertEquals(2, responsePkgs.size());
+ assertEquals(pkgSeq, responsePkgs.get(1).getSequence());
+ assertEquals(pkgName, responsePkgs.get(1).getName());
+ assertEquals(pkgVersion, responsePkgs.get(1).getVersion());
+ assertEquals(pkgConfigure, responsePkgs.get(1).getConfigure());
+ assertEquals(pkgBuild, responsePkgs.get(1).getBuild());
+ assertEquals(pkgInstall, responsePkgs.get(1).getInstall());
+ assertEquals(pkgSetup, responsePkgs.get(1).getSetup());
+ assertEquals(pkgNotes, responsePkgs.get(1).getNotes());
+ assertEquals(pkgUrl, responsePkgs.get(1).getUrl());
+ }
+
+ @Test
+ public void testGetPkgs() {
+ h2Repository.save(testPkg);
+ List<Pkg> pkgs = h2Repository.findAll();
+ assertEquals(1, pkgs.size());
+ assertEquals(1, h2Repository.findAll().size());
+ }
+
+ @Test
+ public void testUpdatePkg() {
+ List<Pkg> pkgs = h2Repository.findAll();
+ testPkg = pkgs.get(0);
+ testPkg.setSequence(987);
+ h2Repository.save(testPkg);
+
+ pkgs = h2Repository.findAll();
+ assertEquals(1, pkgs.size());
+ assertNotEquals(0, testPkg.getId());
+ assertEquals(987, testPkg.getSequence());
+ }
+
+ @Test
+ public void tsetDeletePkg() {
+ List<Pkg> pkgs = h2Repository.findAll();
+ testPkg = pkgs.get(0);
+ h2Repository.delete(testPkg);
+ pkgs = h2Repository.findAll();
+
+ assertEquals(0, pkgs.size());
+ }
+
+ private Pkg setPkgDetails(Integer seq, String name, String version, String configure, String build,
+ String install, String setup, String notes, String url) {
+ Pkg pkg = new Pkg();
+ pkg.setSequence(seq);
+ pkg.setName(name);
+ pkg.setVersion(version);
+ pkg.setConfigure(configure);
+ pkg.setBuild(build);
+ pkg.setInstall(install);
+ pkg.setSetup(setup);
+ pkg.setNotes(notes);
+ pkg.setUrl(url);
+ return pkg;
+ }
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/src/test/java/org/berzerkula/builddb/BuilddbApplicationTest.java b/src/test/java/org/berzerkula/builddb/BuilddbApplicationTest.java
new file mode 100644
index 0000000..57604be
--- /dev/null
+++ b/src/test/java/org/berzerkula/builddb/BuilddbApplicationTest.java
@@ -0,0 +1,24 @@
+package org.berzerkula.builddb;
+
+import org.assertj.core.api.Assertions;
+import org.berzerkula.builddb.services.AppUserService;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class BuilddbApplicationTest {
+
+ @Autowired
+ AppUserService appUserService;
+
+ @Test
+ public void contextLoads() {
+ Assertions.assertThat(appUserService).isNotNull();
+ }
+
+ @Test
+ public void main() {
+ BuilddbApplication.main(new String[] {});
+ }
+} \ No newline at end of file
diff --git a/src/test/java/org/berzerkula/builddb/controllers/BuilddbTestAccountController.java b/src/test/java/org/berzerkula/builddb/controllers/BuilddbTestAccountController.java
new file mode 100644
index 0000000..50548fd
--- /dev/null
+++ b/src/test/java/org/berzerkula/builddb/controllers/BuilddbTestAccountController.java
@@ -0,0 +1,137 @@
+package org.berzerkula.builddb.controllers;
+
+import org.berzerkula.builddb.config.SecurityConfig;
+import org.berzerkula.builddb.models.AppUser;
+import org.berzerkula.builddb.repositories.AppUserRepository;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.util.Date;
+
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
+
+@Import(SecurityConfig.class)
+@AutoConfigureMockMvc
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+
+public class BuilddbTestAccountController {
+
+ @Autowired
+ private AppUserRepository appUserRepository;
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Test
+ public void shouldReturnIndexOk() throws Exception {
+ mockMvc.perform(get("/"))
+ .andExpect(status().isOk())
+ .andExpect(view().name("index"))
+ .andDo(print());
+ }
+
+ @Test
+ public void shouldReturnLoginOk() throws Exception {
+ mockMvc.perform(get("/login"))
+ .andExpect(status().isOk())
+ .andExpect(view().name("login"))
+ .andDo(print());
+ }
+
+ @Test
+ public void shouldProfileRedirectToLogin() throws Exception {
+ mockMvc.perform(get("/profile"))
+ .andExpect(status().is3xxRedirection())
+ .andDo(print());
+ }
+
+ @WithMockUser(roles="client")
+ @Test
+ public void shouldReturnProfileView() throws Exception {
+
+ AppUser appUser = new AppUser();
+ appUser.setFirstName("John");
+ appUser.setLastName("Doe");
+ appUser.setEmail("test@berzerkula.org");
+ appUser.setPhone("555-555-5555");
+ appUser.setAddress("612 Wolf Avenue");
+ appUser.setRole("client");
+ appUser.setPassword("password");
+ appUser.setCreatedAt(new Date());
+ appUserRepository.save(appUser);
+
+ this.mockMvc.perform(get("/profile")
+ .with(user("test@berzerkula.org")))
+ .andExpect(status().isOk())
+ .andExpect(view().name("profile"))
+ .andDo(print());
+ }
+
+ @Test
+ public void shouldReturnRegisterOk() throws Exception {
+ mockMvc.perform(get("/register"))
+ .andExpect(status().isOk())
+ .andExpect(view().name("register"))
+ .andDo(print());
+ }
+
+ @Test
+ public void shouldRegisterRequiredValidationError() throws Exception {
+ mockMvc.perform(post("/register")
+ .with(csrf())
+ .formField("firstName", "")
+ .formField("lastName", "")
+ .formField("email", "")
+ .formField("phone", "")
+ .formField("address", "")
+ .formField("password", "")
+ .formField("confirmPassword", ""))
+ .andExpect(status().isOk())
+ .andExpect(view().name("register"))
+ .andDo(print());
+ }
+
+ @Test
+ public void shouldRegisterPasswordValidationError() throws Exception {
+ mockMvc.perform(post("/register")
+ .with(csrf())
+ .formField("firstName", "John")
+ .formField("lastName", "Doe")
+ .formField("email", "test@berzerkula.org")
+ .formField("phone", "")
+ .formField("address", "")
+ .formField("password", "password")
+ .formField("confirmPassword", "passwrd"))
+ .andExpect(status().isOk())
+ .andExpect(view().name("register"))
+ .andDo(print());
+ }
+
+ @Test
+ public void shouldRegisterSuccess() throws Exception {
+ mockMvc.perform(post("/register")
+ .with(csrf())
+ .formField("firstName", "John")
+ .formField("lastName", "Doe")
+ .formField("email", "test@berzerkula.org")
+ .formField("phone", "555-555-5555")
+ .formField("address", "612 Wolf Avenue")
+ .formField("password", "password")
+ .formField("confirmPassword", "password"))
+ .andExpect(status().isOk())
+ .andExpect(view().name("register"))
+ .andDo(print());
+ }
+
+}
diff --git a/src/test/java/org/berzerkula/builddb/controllers/BuilddbTestDashboardController.java b/src/test/java/org/berzerkula/builddb/controllers/BuilddbTestDashboardController.java
new file mode 100644
index 0000000..27944ce
--- /dev/null
+++ b/src/test/java/org/berzerkula/builddb/controllers/BuilddbTestDashboardController.java
@@ -0,0 +1,62 @@
+package org.berzerkula.builddb.controllers;
+
+import org.berzerkula.builddb.config.SecurityConfig;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
+
+@Import(SecurityConfig.class)
+@AutoConfigureMockMvc
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+
+public class BuilddbTestDashboardController {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Test
+ @WithMockUser
+ public void shouldReturnActuatorDashboardView() throws Exception {
+ this.mockMvc.perform(get("/actuatorDashboard"))
+ .andExpect(status().isOk())
+ .andExpect(view().name("actuatorDashboard"))
+ .andDo(print());
+ }
+
+ @Test
+ @WithMockUser
+ public void shouldReturnAdminView() throws Exception {
+ this.mockMvc.perform(get("/admin"))
+ .andExpect(status().isOk())
+ .andExpect(view().name("admin"))
+ .andDo(print());
+ }
+
+ @Test
+ @WithMockUser
+ public void shouldReturnClientView() throws Exception {
+ this.mockMvc.perform(get("/client"))
+ .andExpect(status().isOk())
+ .andExpect(view().name("client"))
+ .andDo(print());
+ }
+
+ @Test
+ @WithMockUser
+ public void shouldReturnUserView() throws Exception {
+ this.mockMvc.perform(get("/user"))
+ .andExpect(status().isOk())
+ .andExpect(view().name("user"))
+ .andDo(print());
+ }
+
+}
diff --git a/src/test/java/org/berzerkula/builddb/controllers/BuilddbTestHomeController.java b/src/test/java/org/berzerkula/builddb/controllers/BuilddbTestHomeController.java
new file mode 100644
index 0000000..94ae655
--- /dev/null
+++ b/src/test/java/org/berzerkula/builddb/controllers/BuilddbTestHomeController.java
@@ -0,0 +1,33 @@
+package org.berzerkula.builddb.controllers;
+
+import org.berzerkula.builddb.config.SecurityConfig;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
+
+@Import(SecurityConfig.class)
+@AutoConfigureMockMvc
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+
+public class BuilddbTestHomeController {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Test
+ void shouldReturnContactOk() throws Exception {
+ mockMvc.perform(get("/contact"))
+ .andExpect(status().isOk())
+ .andExpect(view().name("contact"))
+ .andDo(print());
+ }
+
+}
diff --git a/src/test/java/org/berzerkula/builddb/controllers/BuilddbTestPkgController.java b/src/test/java/org/berzerkula/builddb/controllers/BuilddbTestPkgController.java
new file mode 100644
index 0000000..6043aa1
--- /dev/null
+++ b/src/test/java/org/berzerkula/builddb/controllers/BuilddbTestPkgController.java
@@ -0,0 +1,89 @@
+package org.berzerkula.builddb.controllers;
+
+import org.berzerkula.builddb.config.SecurityConfig;
+import org.berzerkula.builddb.models.Pkg;
+import org.berzerkula.builddb.repositories.TestH2PkgRepository;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.util.List;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
+
+@Import(SecurityConfig.class)
+@AutoConfigureMockMvc
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+
+public class BuilddbTestPkgController {
+
+ @Autowired
+ private TestH2PkgRepository pkgRepository;
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Test
+ @WithMockUser(roles="client")
+ public void shouldReturnEmptyPackageListView() throws Exception {
+ this.mockMvc.perform(get("/pkgs"))
+ .andExpect(status().isOk())
+ .andExpect(view().name("pkgs/index"))
+ .andDo(print());
+ }
+
+ @Test
+ @WithMockUser(roles="client")
+ public void shouldReturnAddPackageView() throws Exception {
+
+ this.mockMvc.perform(get("/pkgs/add"))
+ .andExpect(status().isOk())
+ .andExpect(view().name("pkgs/add"))
+ .andDo(print());
+ }
+
+ @Test
+ @WithMockUser(roles="client")
+ public void shouldReturnEditPackageView() throws Exception {
+
+ Pkg pkg = new Pkg();
+ pkg.setSequence(1);
+ pkg.setName("test");
+ pkg.setVersion("1.2.3");
+ pkgRepository.save(pkg);
+ List<Pkg> pkgs = pkgRepository.findAll();
+ pkg = pkgs.get(0);
+ Integer id = pkg.getId();
+
+ this.mockMvc.perform(get("/pkgs/edit?id=" + id))
+ .andExpect(status().isOk())
+ .andExpect(view().name("pkgs/edit"))
+ .andDo(print());
+ }
+
+ @Test
+ @WithMockUser(roles="client")
+ public void shouldGetPopupWhenDeletePackage() throws Exception {
+
+ Pkg pkg = new Pkg();
+ pkg.setSequence(1);
+ pkg.setName("test");
+ pkg.setVersion("1.2.3");
+ pkgRepository.save(pkg);
+ List<Pkg> pkgs = pkgRepository.findAll();
+ pkg = pkgs.get(0);
+ Integer id = pkg.getId();
+
+ this.mockMvc.perform(get("/pkgs/delete?id=" + id))
+ .andExpect(status().is3xxRedirection())
+ .andExpect(view().name("redirect:/pkgs/"))
+ .andDo(print());
+ }
+}
diff --git a/src/test/java/org/berzerkula/builddb/repositories/AppUserRepositoryTest.java b/src/test/java/org/berzerkula/builddb/repositories/AppUserRepositoryTest.java
new file mode 100644
index 0000000..58ca267
--- /dev/null
+++ b/src/test/java/org/berzerkula/builddb/repositories/AppUserRepositoryTest.java
@@ -0,0 +1,31 @@
+package org.berzerkula.builddb.repositories;
+
+import org.assertj.core.api.Assertions;
+import org.berzerkula.builddb.models.AppUser;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+
+@DataJpaTest
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+class AppUserRepositoryTest {
+
+ @Autowired AppUserRepository appUserRepository;
+
+ @Test
+ public void testCreateReadDelete() {
+
+ AppUser appUser = new AppUser("John", "Doe", "john.doe@test.com", "", "",
+ "password","client");
+
+ appUserRepository.save(appUser);
+
+ Iterable<AppUser> appUsers = appUserRepository.findAll();
+ Assertions.assertThat(appUsers).extracting(AppUser::getFirstName).containsOnly("John");
+
+ appUserRepository.deleteAll();
+ Assertions.assertThat(appUserRepository.findAll().isEmpty());
+ }
+
+} \ No newline at end of file
diff --git a/src/test/java/org/berzerkula/builddb/repositories/PkgRepositoryTest.java b/src/test/java/org/berzerkula/builddb/repositories/PkgRepositoryTest.java
new file mode 100644
index 0000000..0203fb0
--- /dev/null
+++ b/src/test/java/org/berzerkula/builddb/repositories/PkgRepositoryTest.java
@@ -0,0 +1,32 @@
+package org.berzerkula.builddb.repositories;
+
+import org.assertj.core.api.Assertions;
+import org.berzerkula.builddb.models.Pkg;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+
+@DataJpaTest
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+class PkgRepositoryTest {
+
+ @Autowired
+ PkgRepository pkgRepository;
+
+ @Test
+ public void testCreateReadDelete() {
+
+ Pkg pkg = new Pkg(123, "Test", "1.2.3", "",
+ "", "", "", "", "");
+
+ pkgRepository.save(pkg);
+
+ Iterable<Pkg> pkgs = pkgRepository.findAll();
+ Assertions.assertThat(pkgs).extracting(Pkg::getSequence).containsOnly(123);
+
+ pkgRepository.deleteAll();
+ Assertions.assertThat(pkgRepository.findAll().isEmpty());
+ }
+
+} \ No newline at end of file
diff --git a/src/test/java/org/berzerkula/builddb/repositories/TestH2AppUserRepository.java b/src/test/java/org/berzerkula/builddb/repositories/TestH2AppUserRepository.java
new file mode 100644
index 0000000..7473119
--- /dev/null
+++ b/src/test/java/org/berzerkula/builddb/repositories/TestH2AppUserRepository.java
@@ -0,0 +1,7 @@
+package org.berzerkula.builddb.repositories;
+
+import org.berzerkula.builddb.models.AppUser;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface TestH2AppUserRepository extends JpaRepository<AppUser, Integer> {
+}
diff --git a/src/test/java/org/berzerkula/builddb/repositories/TestH2PkgRepository.java b/src/test/java/org/berzerkula/builddb/repositories/TestH2PkgRepository.java
new file mode 100644
index 0000000..5916ff2
--- /dev/null
+++ b/src/test/java/org/berzerkula/builddb/repositories/TestH2PkgRepository.java
@@ -0,0 +1,7 @@
+package org.berzerkula.builddb.repositories;
+
+import org.berzerkula.builddb.models.Pkg;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface TestH2PkgRepository extends JpaRepository<Pkg, Integer> {
+}
diff --git a/src/test/java/org/berzerkula/builddb/services/AppUserServiceTest.java b/src/test/java/org/berzerkula/builddb/services/AppUserServiceTest.java
new file mode 100644
index 0000000..d3b19f7
--- /dev/null
+++ b/src/test/java/org/berzerkula/builddb/services/AppUserServiceTest.java
@@ -0,0 +1,54 @@
+package org.berzerkula.builddb.services;
+
+import org.berzerkula.builddb.models.AppUser;
+import org.berzerkula.builddb.repositories.AppUserRepository;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class AppUserServiceTest {
+
+ @InjectMocks
+ private AppUserService appUserService;
+
+ @Mock
+ AppUserRepository appUserRepository;
+
+ @BeforeEach
+ public void init() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ void testFindByEmailAndNullOrNotNull() {
+
+ List<AppUser> appUsers = new ArrayList<AppUser>();
+ AppUser one = new AppUser("John", "Doe", "john.doe@test.com", "", "", "password", "client");
+ AppUser two = new AppUser("John2", "Doe2", "john.doe2@test.com", "", "", "password", "client");
+ AppUser three = new AppUser("John3", "Doe3", "john.doe3@test.com", "", "", "password", "client");
+
+ appUsers.add(one);
+ appUsers.add(two);
+ appUsers.add(three);
+
+ when(appUserRepository.findByEmail("john.doe2@test.com")).thenReturn(two);
+
+ UserDetails userDetails = appUserService.loadUserByUsername("john.doe2@test.com");
+ assertNotNull(userDetails);
+ userDetails = appUserService.loadUserByUsername("");
+ assertNull(userDetails);
+
+ }
+} \ No newline at end of file
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
new file mode 100644
index 0000000..7bc73a2
--- /dev/null
+++ b/src/test/resources/application.properties
@@ -0,0 +1,9 @@
+org.springframework.web: DEBUG
+org.springframework.security: DEBUG
+
+spring.datasource.url=jdbc:h2:mem:testdb
+spring.datasource.driver-class-name=org.h2.Driver
+spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
+spring.h2.console.enabled=true
+spring.jpa.show-sql=true
+spring.jpa.properties.hibernate.format_sql=true \ No newline at end of file