existierendes front und back end

This commit is contained in:
Lorenz Hohermuth 2025-03-28 16:03:38 +01:00
commit 2fe26bb5ed
62 changed files with 3388 additions and 0 deletions

View File

@ -0,0 +1,32 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/

Binary file not shown.

View File

@ -0,0 +1,2 @@
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar

View File

@ -0,0 +1,52 @@
# First part
#============
# Start with an image containing JDK 17
FROM openjdk:17-jdk-slim AS build
# Install Maven
RUN apt-get update && apt-get install -y --no-install-recommends maven git \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Setup working directory
WORKDIR /App
# Copy the pom.xml file to the image
COPY ./pom.xml /App/
# Cache maven dependencies
RUN mvn dependency:resolve
# Copy the entire src directory to the image
COPY ./src /App/src
# Build the app
RUN mvn clean package -Dmaven.test.skip=true
# Current dir is /App
# ==> You will find the result in folder target/
# Second part
#============
# start with a small image
FROM alpine:latest
# add JRE 17
RUN apk add --no-cache openjdk17-jre
# Set working directory
WORKDIR /App
# copy jar form first image to second image
COPY --from=build /App/target/*.jar /App/
# db has to start for backend
COPY wait-for-db.sh /usr/local/bin/wait-for-db.sh
RUN chmod +x /usr/local/bin/wait-for-db.sh
# Expose Port
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java -jar *.jar"]

View File

@ -0,0 +1,40 @@
# Backend for the _secret tresor application_
This is the backend is a Java Springboot application.
It can be used by API-calls.
The data's are stored in a database.
### information about database
prepare a database server and the database used by the application<br>
see [tresordb.sql](tresordb.sql) for an example database<br>
see [application.properties](src/main/resources/application.properties) about database access
## Requests examples
see [UserRequests.http](httprequest/UserRequests.http)<br>
see [SecretRequests.http](httprequest/SecretRequests.http)
## Environment variables
see [application.properties](src/main/resources/application.properties)
## Build image
see Dockerfile
```Bash
docker build -t tresorbackendimg .
```
## Start container local
```Bash
docker run -p 8080:8080 --name tresorbackend tresorbackendimg
```
(c) P.Rutschmann

View File

@ -0,0 +1,73 @@
###
#Get all secrets
GET http://localhost:8080/api/secrets
Accept: application/json
###
# Create new secret
POST http://localhost:8080/api/secrets
Content-Type: application/json
{
"email": "hans.muster@bbw.ch",
"content": {
"kindid": 3,
"kind": "note",
"title": "Spaphira",
"content": "Saphira roch an dem kleinen Erdling."
},
"encryptPassword": "xxxyyy"
}
###
#Post to get secrets with userid 1 and encryptpassword
POST http://localhost:8080/api/secrets/byuserid
Content-Type: application/json
{
"userId": 1,
"encryptPassword": "xxxyyy"
}
###
#Post to get secrets with email and encryptpassword
POST http://localhost:8080/api/secrets/byemail
Content-Type: application/json
{
"email": "hans.muster@bbw.ch",
"encryptPassword": "xxxyyy"
}
###
# Update secret with id 3
PUT http://localhost:8080/api/secrets/3
Content-Type: application/json
{
"userId": 1,
"content": {
"kindid": 3,
"kind": "note",
"title": "Red drageon",
"content": "Er war gewaltig und rot."
},
"encryptPassword": "xxxyyy"
}
###
#Delete secret with id 3
DELETE http://localhost:8080/api/secrets/3
Accept: application/json
###
#Delete secret with id 4
DELETE http://localhost:8080/api/secrets/4
Accept: application/json
###
#Delete secret with id 5
DELETE http://localhost:8080/api/secrets/5
Accept: application/json

View File

@ -0,0 +1,47 @@
###
#Get all users
GET http://localhost:8080/api/users
Accept: application/json
###
# Create user Violett Weber
POST http://localhost:8080/api/users
Content-Type: application/json
{
"firstName": "Violett",
"lastName": "Weber",
"email": "violett.weber@bbw.ch",
"password": "1234",
"passwordConfirmation": "1234"
}
###
#Get user with id 4
GET http://localhost:8080/api/users/4
Accept: application/json
###
# Update user with id 4
PUT http://localhost:8080/api/users/4
Content-Type: application/json
{
"firstName": "Iris",
"lastName":"Weber",
"email": "iris.weber@bbw.ch"
}
###
#Delete user with id 4
DELETE http://localhost:8080/api/users/4
Accept: application/json
###
# Post: get user by email
POST http://localhost:8080/api/users/byemail
Content-Type: application/json
{
"email": "paula.kuster@bbw.ch"
}

308
183_12_1_tresorbackend_rupe-master/mvnw vendored Normal file
View File

@ -0,0 +1,308 @@
#!/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
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.2.0
#
# Required ENV vars:
# ------------------
# JAVA_HOME - location of a JDK home dir
#
# Optional ENV vars
# -----------------
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
# e.g. to debug Maven itself, use
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
# ----------------------------------------------------------------------------
if [ -z "$MAVEN_SKIP_RC" ] ; then
if [ -f /usr/local/etc/mavenrc ] ; then
. /usr/local/etc/mavenrc
fi
if [ -f /etc/mavenrc ] ; then
. /etc/mavenrc
fi
if [ -f "$HOME/.mavenrc" ] ; then
. "$HOME/.mavenrc"
fi
fi
# OS specific support. $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
mingw=false
case "$(uname)" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
else
JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
fi
fi
;;
esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=$(java-config --jre-home)
fi
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
[ -n "$CLASSPATH" ] &&
CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
fi
# For Mingw, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
fi
if [ -z "$JAVA_HOME" ]; then
javaExecutable="$(which javac)"
if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
readLink=$(which readlink)
if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
if $darwin ; then
javaHome="$(dirname "\"$javaExecutable\"")"
javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
else
javaExecutable="$(readlink -f "\"$javaExecutable\"")"
fi
javaHome="$(dirname "\"$javaExecutable\"")"
javaHome=$(expr "$javaHome" : '\(.*\)/bin')
JAVA_HOME="$javaHome"
export JAVA_HOME
fi
fi
fi
if [ -z "$JAVACMD" ] ; then
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
else
JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
fi
fi
if [ ! -x "$JAVACMD" ] ; then
echo "Error: JAVA_HOME is not defined correctly." >&2
echo " We cannot execute $JAVACMD" >&2
exit 1
fi
if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
if [ -z "$1" ]
then
echo "Path not specified to find_maven_basedir"
return 1
fi
basedir="$1"
wdir="$1"
while [ "$wdir" != '/' ] ; do
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
if [ -d "${wdir}" ]; then
wdir=$(cd "$wdir/.." || exit 1; pwd)
fi
# end of workaround
done
printf '%s' "$(cd "$basedir" || exit 1; pwd)"
}
# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
# Remove \r in case we run on Windows within Git Bash
# and check out the repository with auto CRLF management
# enabled. Otherwise, we may read lines that are delimited with
# \r\n and produce $'-Xarg\r' rather than -Xarg due to word
# splitting rules.
tr -s '\r\n' ' ' < "$1"
fi
}
log() {
if [ "$MVNW_VERBOSE" = true ]; then
printf '%s\n' "$1"
fi
}
BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
if [ -z "$BASE_DIR" ]; then
exit 1;
fi
MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
log "$MAVEN_PROJECTBASEDIR"
##########################################################################################
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
# This allows using the maven wrapper in projects that prohibit checking in binary data.
##########################################################################################
wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
if [ -r "$wrapperJarPath" ]; then
log "Found $wrapperJarPath"
else
log "Couldn't find $wrapperJarPath, downloading it ..."
if [ -n "$MVNW_REPOURL" ]; then
wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
else
wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
fi
while IFS="=" read -r key value; do
# Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
safeValue=$(echo "$value" | tr -d '\r')
case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
esac
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
log "Downloading from: $wrapperUrl"
if $cygwin; then
wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
fi
if command -v wget > /dev/null; then
log "Found wget ... using wget"
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
else
wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
fi
elif command -v curl > /dev/null; then
log "Found curl ... using curl"
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
else
curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
fi
else
log "Falling back to using Java to download"
javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
# For Cygwin, switch paths to Windows format before running javac
if $cygwin; then
javaSource=$(cygpath --path --windows "$javaSource")
javaClass=$(cygpath --path --windows "$javaClass")
fi
if [ -e "$javaSource" ]; then
if [ ! -e "$javaClass" ]; then
log " - Compiling MavenWrapperDownloader.java ..."
("$JAVA_HOME/bin/javac" "$javaSource")
fi
if [ -e "$javaClass" ]; then
log " - Running MavenWrapperDownloader.java ..."
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
fi
fi
fi
fi
##########################################################################################
# End of extension
##########################################################################################
# If specified, validate the SHA-256 sum of the Maven wrapper jar file
wrapperSha256Sum=""
while IFS="=" read -r key value; do
case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
esac
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
if [ -n "$wrapperSha256Sum" ]; then
wrapperSha256Result=false
if command -v sha256sum > /dev/null; then
if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
wrapperSha256Result=true
fi
elif command -v shasum > /dev/null; then
if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
wrapperSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
exit 1
fi
if [ $wrapperSha256Result = false ]; then
echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
exit 1
fi
fi
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
[ -n "$CLASSPATH" ] &&
CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
fi
# Provide a "standardized" way to retrieve the CLI args that will
# work with both Windows and non-Windows executions.
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
export MAVEN_CMD_LINE_ARGS
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
# shellcheck disable=SC2086 # safe args
exec "$JAVACMD" \
$MAVEN_OPTS \
$MAVEN_DEBUG_OPTS \
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

View File

@ -0,0 +1,205 @@
@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 https://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.2.0
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
@REM ----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM set title of command window
title %0
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
@REM ==== END VALIDATION ====
:init
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
)
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
if exist %WRAPPER_JAR% (
if "%MVNW_VERBOSE%" == "true" (
echo Found %WRAPPER_JAR%
)
) else (
if not "%MVNW_REPOURL%" == "" (
SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
)
if "%MVNW_VERBOSE%" == "true" (
echo Couldn't find %WRAPPER_JAR%, downloading it ...
echo Downloading from: %WRAPPER_URL%
)
powershell -Command "&{"^
"$webclient = new-object System.Net.WebClient;"^
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
"}"^
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
"}"
if "%MVNW_VERBOSE%" == "true" (
echo Finished downloading %WRAPPER_JAR%
)
)
@REM End of extension
@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
SET WRAPPER_SHA_256_SUM=""
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
)
IF NOT %WRAPPER_SHA_256_SUM%=="" (
powershell -Command "&{"^
"$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
"If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
" Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
" Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
" Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
" exit 1;"^
"}"^
"}"
if ERRORLEVEL 1 goto error
)
@REM Provide a "standardized" way to retrieve the CLI args that will
@REM work with both Windows and non-Windows executions.
set MAVEN_CMD_LINE_ARGS=%*
%MAVEN_JAVA_EXE% ^
%JVM_CONFIG_MAVEN_PROPS% ^
%MAVEN_OPTS% ^
%MAVEN_DEBUG_OPTS% ^
-classpath %WRAPPER_JAR% ^
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%"=="on" pause
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
cmd /C exit /B %ERROR_CODE%

View File

@ -0,0 +1,80 @@
<?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.2.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>ch.bbw.pr</groupId>
<artifactId>tresorbackend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>tresorbackend</name>
<description>tresorbackend</description>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</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-web</artifactId>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.ulisesbocchio/jasypt-spring-boot-starter -->
<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>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,13 @@
package ch.bbw.pr.tresorbackend;
import ch.bbw.pr.tresorbackend.model.ConfigProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.CrossOrigin;
@SpringBootApplication
public class TresorbackendApplication {
public static void main(String[] args) {
SpringApplication.run(TresorbackendApplication.class, args);
}
}

View File

@ -0,0 +1,220 @@
package ch.bbw.pr.tresorbackend.controller;
import ch.bbw.pr.tresorbackend.model.Secret;
import ch.bbw.pr.tresorbackend.model.NewSecret;
import ch.bbw.pr.tresorbackend.model.EncryptCredentials;
import ch.bbw.pr.tresorbackend.model.User;
import ch.bbw.pr.tresorbackend.service.SecretService;
import ch.bbw.pr.tresorbackend.service.UserService;
import ch.bbw.pr.tresorbackend.util.EncryptUtil;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import org.jasypt.exceptions.EncryptionOperationNotPossibleException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
/**
* SecretController
* @author Peter Rutschmann
*/
@RestController
@AllArgsConstructor
@RequestMapping("api/secrets")
public class SecretController {
private SecretService secretService;
private UserService userService;
// create secret REST API
@CrossOrigin(origins = "${CROSS_ORIGIN}")
@PostMapping
public ResponseEntity<String> createSecret2(@Valid @RequestBody NewSecret newSecret, BindingResult bindingResult) {
//input validation
if (bindingResult.hasErrors()) {
List<String> errors = bindingResult.getFieldErrors().stream()
.map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
.collect(Collectors.toList());
System.out.println("SecretController.createSecret " + errors);
JsonArray arr = new JsonArray();
errors.forEach(arr::add);
JsonObject obj = new JsonObject();
obj.add("message", arr);
String json = new Gson().toJson(obj);
System.out.println("SecretController.createSecret, validation fails: " + json);
return ResponseEntity.badRequest().body(json);
}
System.out.println("SecretController.createSecret, input validation passed");
User user = userService.findByEmail(newSecret.getEmail());
//transfer secret and encrypt content
Secret secret = new Secret(
null,
user.getId(),
new EncryptUtil(newSecret.getEncryptPassword()).encrypt(newSecret.getContent().toString())
);
//save secret in db
secretService.createSecret(secret);
System.out.println("SecretController.createSecret, secret saved in db");
JsonObject obj = new JsonObject();
obj.addProperty("answer", "Secret saved");
String json = new Gson().toJson(obj);
System.out.println("SecretController.createSecret " + json);
return ResponseEntity.accepted().body(json);
}
// Build Get Secrets by userId REST API
@CrossOrigin(origins = "${CROSS_ORIGIN}")
@PostMapping("/byuserid")
public ResponseEntity<List<Secret>> getSecretsByUserId(@RequestBody EncryptCredentials credentials) {
System.out.println("SecretController.getSecretsByUserId " + credentials);
List<Secret> secrets = secretService.getSecretsByUserId(credentials.getUserId());
if (secrets.isEmpty()) {
System.out.println("SecretController.getSecretsByUserId secret isEmpty");
return ResponseEntity.notFound().build();
}
//Decrypt content
for(Secret secret: secrets) {
try {
secret.setContent(new EncryptUtil(credentials.getEncryptPassword()).decrypt(secret.getContent()));
} catch (EncryptionOperationNotPossibleException e) {
System.out.println("SecretController.getSecretsByUserId " + e + " " + secret);
secret.setContent("not encryptable. Wrong password?");
}
}
System.out.println("SecretController.getSecretsByUserId " + secrets);
return ResponseEntity.ok(secrets);
}
// Build Get Secrets by email REST API
@CrossOrigin(origins = "${CROSS_ORIGIN}")
@PostMapping("/byemail")
public ResponseEntity<List<Secret>> getSecretsByEmail(@RequestBody EncryptCredentials credentials) {
System.out.println("SecretController.getSecretsByEmail " + credentials);
User user = userService.findByEmail(credentials.getEmail());
List<Secret> secrets = secretService.getSecretsByUserId(user.getId());
if (secrets.isEmpty()) {
System.out.println("SecretController.getSecretsByEmail secret isEmpty");
return ResponseEntity.notFound().build();
}
//Decrypt content
for(Secret secret: secrets) {
try {
secret.setContent(new EncryptUtil(credentials.getEncryptPassword()).decrypt(secret.getContent()));
} catch (EncryptionOperationNotPossibleException e) {
System.out.println("SecretController.getSecretsByEmail " + e + " " + secret);
secret.setContent("not encryptable. Wrong password?");
}
}
System.out.println("SecretController.getSecretsByEmail " + secrets);
return ResponseEntity.ok(secrets);
}
// Build Get All Secrets REST API
// http://localhost:8080/api/secrets
@CrossOrigin(origins = "${CROSS_ORIGIN}")
@GetMapping
public ResponseEntity<List<Secret>> getAllSecrets() {
List<Secret> secrets = secretService.getAllSecrets();
return new ResponseEntity<>(secrets, HttpStatus.OK);
}
// Build Update Secrete REST API
// http://localhost:8080/api/secrets/1
@CrossOrigin(origins = "${CROSS_ORIGIN}")
@PutMapping("{id}")
public ResponseEntity<String> updateSecret(
@PathVariable("id") Long secretId,
@Valid @RequestBody NewSecret newSecret,
BindingResult bindingResult) {
//input validation
if (bindingResult.hasErrors()) {
List<String> errors = bindingResult.getFieldErrors().stream()
.map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
.collect(Collectors.toList());
System.out.println("SecretController.createSecret " + errors);
JsonArray arr = new JsonArray();
errors.forEach(arr::add);
JsonObject obj = new JsonObject();
obj.add("message", arr);
String json = new Gson().toJson(obj);
System.out.println("SecretController.updateSecret, validation fails: " + json);
return ResponseEntity.badRequest().body(json);
}
//get Secret with id
Secret dbSecrete = secretService.getSecretById(secretId);
if(dbSecrete == null){
System.out.println("SecretController.updateSecret, secret not found in db");
JsonObject obj = new JsonObject();
obj.addProperty("answer", "Secret not found in db");
String json = new Gson().toJson(obj);
System.out.println("SecretController.updateSecret failed:" + json);
return ResponseEntity.badRequest().body(json);
}
User user = userService.findByEmail(newSecret.getEmail());
//check if Secret in db has not same userid
if(dbSecrete.getUserId() != user.getId()){
System.out.println("SecretController.updateSecret, not same user id");
JsonObject obj = new JsonObject();
obj.addProperty("answer", "Secret has not same user id");
String json = new Gson().toJson(obj);
System.out.println("SecretController.updateSecret failed:" + json);
return ResponseEntity.badRequest().body(json);
}
//check if Secret can be decrypted with password
try {
new EncryptUtil(newSecret.getEncryptPassword()).decrypt(dbSecrete.getContent());
} catch (EncryptionOperationNotPossibleException e) {
System.out.println("SecretController.updateSecret, invalid password");
JsonObject obj = new JsonObject();
obj.addProperty("answer", "Password not correct.");
String json = new Gson().toJson(obj);
System.out.println("SecretController.updateSecret failed:" + json);
return ResponseEntity.badRequest().body(json);
}
//modify Secret in db.
Secret secret = new Secret(
secretId,
user.getId(),
new EncryptUtil(newSecret.getEncryptPassword()).encrypt(newSecret.getContent().toString())
);
Secret updatedSecret = secretService.updateSecret(secret);
//save secret in db
secretService.createSecret(secret);
System.out.println("SecretController.updateSecret, secret updated in db");
JsonObject obj = new JsonObject();
obj.addProperty("answer", "Secret updated");
String json = new Gson().toJson(obj);
System.out.println("SecretController.updateSecret " + json);
return ResponseEntity.accepted().body(json);
}
// Build Delete Secret REST API
@CrossOrigin(origins = "${CROSS_ORIGIN}")
@DeleteMapping("{id}")
public ResponseEntity<String> deleteSecret(@PathVariable("id") Long secretId) {
//todo: Some kind of brute force delete, perhaps test first userid and encryptpassword
secretService.deleteSecret(secretId);
System.out.println("SecretController.deleteSecret succesfully: " + secretId);
return new ResponseEntity<>("Secret successfully deleted!", HttpStatus.OK);
}
}

View File

@ -0,0 +1,181 @@
package ch.bbw.pr.tresorbackend.controller;
import ch.bbw.pr.tresorbackend.model.ConfigProperties;
import ch.bbw.pr.tresorbackend.model.EmailAdress;
import ch.bbw.pr.tresorbackend.model.RegisterUser;
import ch.bbw.pr.tresorbackend.model.User;
import ch.bbw.pr.tresorbackend.service.PasswordEncryptionService;
import ch.bbw.pr.tresorbackend.service.UserService;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
/**
* UserController
* @author Peter Rutschmann
*/
@RestController
@AllArgsConstructor
@RequestMapping("api/users")
public class UserController {
private UserService userService;
private PasswordEncryptionService passwordService;
private final ConfigProperties configProperties;
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@Autowired
public UserController(ConfigProperties configProperties, UserService userService,
PasswordEncryptionService passwordService) {
this.configProperties = configProperties;
System.out.println("UserController.UserController: cross origin: " + configProperties.getOrigin());
// Logging in the constructor
logger.info("UserController initialized: " + configProperties.getOrigin());
logger.debug("UserController.UserController: Cross Origin Config: {}", configProperties.getOrigin());
this.userService = userService;
this.passwordService = passwordService;
}
// build create User REST API
@CrossOrigin(origins = "${CROSS_ORIGIN}")
@PostMapping
public ResponseEntity<String> createUser(@Valid @RequestBody RegisterUser registerUser, BindingResult bindingResult) {
//captcha
//todo ergänzen
System.out.println("UserController.createUser: captcha passed.");
//input validation
if (bindingResult.hasErrors()) {
List<String> errors = bindingResult.getFieldErrors().stream()
.map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
.collect(Collectors.toList());
System.out.println("UserController.createUser " + errors);
JsonArray arr = new JsonArray();
errors.forEach(arr::add);
JsonObject obj = new JsonObject();
obj.add("message", arr);
String json = new Gson().toJson(obj);
System.out.println("UserController.createUser, validation fails: " + json);
return ResponseEntity.badRequest().body(json);
}
System.out.println("UserController.createUser: input validation passed");
//password validation
//todo ergänzen
System.out.println("UserController.createUser, password validation passed");
//transform registerUser to user
User user = new User(
null,
registerUser.getFirstName(),
registerUser.getLastName(),
registerUser.getEmail(),
passwordService.hashPassword(registerUser.getPassword())
);
User savedUser = userService.createUser(user);
System.out.println("UserController.createUser, user saved in db");
JsonObject obj = new JsonObject();
obj.addProperty("answer", "User Saved");
String json = new Gson().toJson(obj);
System.out.println("UserController.createUser " + json);
return ResponseEntity.accepted().body(json);
}
// build get user by id REST API
// http://localhost:8080/api/users/1
@CrossOrigin(origins = "${CROSS_ORIGIN}")
@GetMapping("{id}")
public ResponseEntity<User> getUserById(@PathVariable("id") Long userId) {
User user = userService.getUserById(userId);
return new ResponseEntity<>(user, HttpStatus.OK);
}
// Build Get All Users REST API
// http://localhost:8080/api/users
@CrossOrigin(origins = "${CROSS_ORIGIN}")
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
List<User> users = userService.getAllUsers();
return new ResponseEntity<>(users, HttpStatus.OK);
}
// Build Update User REST API
// http://localhost:8080/api/users/1
@CrossOrigin(origins = "${CROSS_ORIGIN}")
@PutMapping("{id}")
public ResponseEntity<User> updateUser(@PathVariable("id") Long userId,
@RequestBody User user) {
user.setId(userId);
User updatedUser = userService.updateUser(user);
return new ResponseEntity<>(updatedUser, HttpStatus.OK);
}
// Build Delete User REST API
@CrossOrigin(origins = "${CROSS_ORIGIN}")
@DeleteMapping("{id}")
public ResponseEntity<String> deleteUser(@PathVariable("id") Long userId) {
userService.deleteUser(userId);
return new ResponseEntity<>("User successfully deleted!", HttpStatus.OK);
}
// get user id by email
@CrossOrigin(origins = "${CROSS_ORIGIN}")
@PostMapping("/byemail")
public ResponseEntity<String> getUserIdByEmail(@RequestBody EmailAdress email, BindingResult bindingResult) {
System.out.println("UserController.getUserIdByEmail: " + email);
//input validation
if (bindingResult.hasErrors()) {
List<String> errors = bindingResult.getFieldErrors().stream()
.map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
.collect(Collectors.toList());
System.out.println("UserController.createUser " + errors);
JsonArray arr = new JsonArray();
errors.forEach(arr::add);
JsonObject obj = new JsonObject();
obj.add("message", arr);
String json = new Gson().toJson(obj);
System.out.println("UserController.createUser, validation fails: " + json);
return ResponseEntity.badRequest().body(json);
}
System.out.println("UserController.getUserIdByEmail: input validation passed");
User user = userService.findByEmail(email.getEmail());
if (user == null) {
System.out.println("UserController.getUserIdByEmail, no user found with email: " + email);
JsonObject obj = new JsonObject();
obj.addProperty("message", "No user found with this email");
String json = new Gson().toJson(obj);
System.out.println("UserController.getUserIdByEmail, fails: " + json);
return ResponseEntity.badRequest().body(json);
}
System.out.println("UserController.getUserIdByEmail, user find by email");
JsonObject obj = new JsonObject();
obj.addProperty("answer", user.getId());
String json = new Gson().toJson(obj);
System.out.println("UserController.getUserIdByEmail " + json);
return ResponseEntity.accepted().body(json);
}
}

View File

@ -0,0 +1,19 @@
package ch.bbw.pr.tresorbackend.model;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* ConfigProperties
* @author Peter Rutschmann
*/
@Component
public class ConfigProperties {
@Value("${CROSS_ORIGIN}")
private String crossOrigin;
public String getOrigin() {
return crossOrigin;
}
}

View File

@ -0,0 +1,18 @@
package ch.bbw.pr.tresorbackend.model;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.*;
/**
* EmailAdress
* @author Peter Rutschmann
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class EmailAdress {
@NotEmpty (message="E-Mail is required.")
private String email;
}

View File

@ -0,0 +1,20 @@
package ch.bbw.pr.tresorbackend.model;
import jakarta.validation.constraints.NotEmpty;
import lombok.*;
/**
* EncryptCredentials
* @author Peter Rutschmann
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class EncryptCredentials {
private long userId;
private String email;
@NotEmpty(message="encryption password id is required.")
private String encryptPassword;
}

View File

@ -0,0 +1,30 @@
package ch.bbw.pr.tresorbackend.model;
import com.fasterxml.jackson.annotation.JsonRawValue;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* NewSecret
* @author Peter Rutschmann
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class NewSecret {
@NotEmpty (message="email is required.")
private String email;
@NotNull (message="secret is required.")
private JsonNode content;
@NotEmpty (message="encryption password id is required.")
private String encryptPassword;
}

View File

@ -0,0 +1,33 @@
package ch.bbw.pr.tresorbackend.model;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.*;
/**
* RegisterUser
* @author Peter Rutschmann
*/
@Value
public class RegisterUser {
@NotEmpty(message="Firstname is required.")
@Size(min=2, max=25, message="Firstname size has to be 2 up to 25 characters.")
private String firstName;
@NotEmpty (message="Lastname is required.")
@Size(min=2, max=25, message="Lastname size has to be 2 up to 25 characters.")
private String lastName;
@NotEmpty (message="E-Mail is required.")
private String email;
@NotEmpty (message="Password is required.")
private String password;
@NotEmpty (message="Password-confirmation is required.")
private String passwordConfirmation;
private String recaptchaToken;
}

View File

@ -0,0 +1,28 @@
package ch.bbw.pr.tresorbackend.model;
import com.fasterxml.jackson.annotation.JsonRawValue;
import jakarta.persistence.*;
import lombok.*;
/**
* Secret
* @author Peter Rutschmann
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Entity
@Table(name = "secret")
public class Secret {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, name="user_id")
private Long userId;
@Column(nullable = false, name="content")
private String content;
}

View File

@ -0,0 +1,35 @@
package ch.bbw.pr.tresorbackend.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* User
* @author Peter Rutschmann
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, name="first_name")
private String firstName;
@Column(nullable = false, name="last_name")
private String lastName;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password;
}

View File

@ -0,0 +1,14 @@
package ch.bbw.pr.tresorbackend.repository;
import ch.bbw.pr.tresorbackend.model.Secret;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
/**
* SecretRepository
* @author Peter Rutschmann
*/
public interface SecretRepository extends JpaRepository<Secret, Long> {
List<Secret> findByUserId(Long userId);
}

View File

@ -0,0 +1,14 @@
package ch.bbw.pr.tresorbackend.repository;
import ch.bbw.pr.tresorbackend.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
/**
* UserRepository
* @author Peter Rutschmann
*/
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}

View File

@ -0,0 +1,21 @@
package ch.bbw.pr.tresorbackend.service;
import org.springframework.stereotype.Service;
/**
* PasswordEncryptionService
* @author Peter Rutschmann
*/
@Service
public class PasswordEncryptionService {
//todo ergänzen!
public PasswordEncryptionService() {
//todo anpassen!
}
public String hashPassword(String password) {
//todo anpassen!
return password;
}
}

View File

@ -0,0 +1,22 @@
package ch.bbw.pr.tresorbackend.service;
import ch.bbw.pr.tresorbackend.model.Secret;
import java.util.List;
/**
* SecretService
* @author Peter Rutschmann
*/
public interface SecretService {
Secret createSecret(Secret secret);
Secret getSecretById(Long secretId);
List<Secret> getAllSecrets();
Secret updateSecret(Secret secret);
void deleteSecret(Long secretId);
List<Secret> getSecretsByUserId(Long userId);
}

View File

@ -0,0 +1,22 @@
package ch.bbw.pr.tresorbackend.service;
import ch.bbw.pr.tresorbackend.model.User;
import java.util.List;
/**
* UserService
* @author Peter Rutschmann
*/
public interface UserService {
User createUser(User user);
User getUserById(Long userId);
User findByEmail(String email);
List<User> getAllUsers();
User updateUser(User user);
void deleteUser(Long userId);
}

View File

@ -0,0 +1,57 @@
package ch.bbw.pr.tresorbackend.service.impl;
import ch.bbw.pr.tresorbackend.model.Secret;
import ch.bbw.pr.tresorbackend.repository.SecretRepository;
import ch.bbw.pr.tresorbackend.service.SecretService;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
/**
* SecretServiceImpl
* @author Peter Rutschmann
*/
@Service
@AllArgsConstructor
public class SecretServiceImpl implements SecretService {
private SecretRepository secretRepository;
@Override
public Secret createSecret(Secret card) {
return secretRepository.save(card);
}
@Override
public Secret getSecretById(Long secretId) {
Optional<Secret> optionalSecret = secretRepository.findById(secretId);
return optionalSecret.get();
}
@Override
public List<Secret> getAllSecrets() {
return (List<Secret>) secretRepository.findAll();
}
@Override
public Secret updateSecret(Secret secret) {
Secret existingSecret = secretRepository.findById(secret.getId()).get();
existingSecret.setUserId(secret.getUserId());
existingSecret.setContent(secret.getContent());
Secret updatedSecret = secretRepository.save(existingSecret);
return updatedSecret;
}
@Override
public void deleteSecret(Long secretId) {
secretRepository.deleteById(secretId);
}
@Override
public List<Secret> getSecretsByUserId(Long userId) {
return secretRepository.findByUserId(userId);
}
}

View File

@ -0,0 +1,58 @@
package ch.bbw.pr.tresorbackend.service.impl;
import ch.bbw.pr.tresorbackend.model.User;
import ch.bbw.pr.tresorbackend.repository.UserRepository;
import ch.bbw.pr.tresorbackend.service.UserService;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
/**
* UserServiceImpl
* @author Peter Rutschmann
*/
@Service
@AllArgsConstructor
public class UserServiceImpl implements UserService {
private UserRepository userRepository;
@Override
public User createUser(User user) {
return userRepository.save(user);
}
@Override
public User getUserById(Long userId) {
Optional<User> optionalUser = userRepository.findById(userId);
return optionalUser.get();
}
@Override
public User findByEmail(String email) {
Optional<User> optionalUser = userRepository.findByEmail(email);
return optionalUser.get();
}
@Override
public List<User> getAllUsers() {
return (List<User>) userRepository.findAll();
}
@Override
public User updateUser(User user) {
User existingUser = userRepository.findById(user.getId()).get();
existingUser.setFirstName(user.getFirstName());
existingUser.setLastName(user.getLastName());
existingUser.setEmail(user.getEmail());
User updatedUser = userRepository.save(existingUser);
return updatedUser;
}
@Override
public void deleteUser(Long userId) {
userRepository.deleteById(userId);
}
}

View File

@ -0,0 +1,28 @@
package ch.bbw.pr.tresorbackend.util;
import org.jasypt.util.text.AES256TextEncryptor;
/**
* EncryptUtil
* Used to encrypt content.
* Not implemented yet.
* @author Peter Rutschmann
*/
public class EncryptUtil {
//todo ergänzen!
public EncryptUtil(String secretKey) {
//todo ergänzen!
}
public String encrypt(String data) {
//todo anpassen!
return data;
}
public String decrypt(String data) {
//todo anpassen!
return data;
}
}

View File

@ -0,0 +1,11 @@
spring.application.name=tresorbackend
#variables can be overwritten by environment variables
spring.datasource.url=jdbc:mysql://localhost:3306/tresordb
spring.datasource.username=root
spring.datasource.password=1234
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
spring.jpa.hibernate.ddl-auto=update
CROSS_ORIGIN=http://localhost:3000

View File

@ -0,0 +1,13 @@
package ch.bbw.pr.tresorbackend;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class TresorbackendApplicationTests {
@Test
void contextLoads() {
}
}

View File

@ -0,0 +1,51 @@
--
-- Datenbank: `tresordb`
--
DROP DATABASE IF EXISTS tresordb;
CREATE DATABASE tresordb;
USE tresordb;
-- --------------------------------------------------------
--
-- table user
--
CREATE TABLE user (
id int NOT NULL AUTO_INCREMENT,
first_name varchar(30) NOT NULL,
last_name varchar(30) NOT NULL,
email varchar(30) NOT NULL,
password longtext NOT NULL,
PRIMARY KEY (id)
);
--
-- table user content
--
INSERT INTO `user` (`id`, `first_name`, `last_name`, `email`, `password`) VALUES
(1, 'Hans', 'Muster', 'hans.muster@bbw.ch', 'abcd'),
(2, 'Paula', 'Kuster', 'paula.kuster@bbw.ch', 'efgh'),
(3, 'Andrea', 'Oester', 'andrea.oester@bbw.ch', 'ijkl');
--
-- table secret
--
CREATE TABLE secret (
id int NOT NULL AUTO_INCREMENT,
user_id int NOT NULL,
content json NOT NULL,
PRIMARY KEY (id)
);
--
-- table secret content
--
INSERT INTO `secret` (`id`, `user_id`, `content`) VALUES
(1, 1, '{"kindid":1,"kind":"credential","userName":"muster","password":"1234","url":"www.bbw.ch"}'),
(2, 1, '{"kindid":2,"kind":"creditcard","cardtype":"Visa","cardnumber":"4242 4242 4242 4241","expiration":"12/27","cvv":"789"}'),
(3, 1, '{"kindid":3,"kind":"note","title":"Eragon","content":"Und Eragon ging auf den Drachen zu."}');

View File

@ -0,0 +1,16 @@
#!/bin/bash
set -e
host="$1"
shift
cmd="$@"
echo "Waiting for $host to be ready..."
until mysqladmin ping -h "$host" --silent; do
echo "MariaDB is unavailable - waiting..."
sleep 2
done
echo "MariaDB is up - executing command"
exec $cmd

View File

@ -0,0 +1,7 @@
#connection to backend api
# example http://localhost:8080/api
# port is optional
REACT_APP_API_PROTOCOL=http
REACT_APP_API_HOST=localhost
REACT_APP_API_PORT=8080
REACT_APP_API_PATH=/api

View File

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Node.js Modules
node_modules/
package-lock.json
yarn.lock
# Build Artifacts
build/
dist/
# IntelliJ IDEA Files
.idea/
*.iml
*.ipr
*.iws
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
*.log
# testing
coverage/
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor Settings
.vscode/
*.swp

View File

@ -0,0 +1,36 @@
# Step one
# Build Stage: Node.js wird verwendet, um die App zu bauen
FROM node:14 AS build
# Set working directory
WORKDIR /App
# copy package.json and package-lock.json
COPY package*.json /App/
# install Node.js-dependencies
RUN npm install
# copy application files
COPY . /App/
# build the app
RUN npm run build
# Step two
# Serve Stage: use Nginx
FROM nginx:latest
# Copy the build files from the build stage to Nginx's html directory
COPY --from=build /App/build /usr/share/nginx/html
# copy nginx config to get a perfomanter app
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose the default Nginx port
EXPOSE 80
# Start Nginx
CMD ["nginx", "-g", "daemon off;"]

View File

@ -0,0 +1,45 @@
# The secret tresor application frontend
## Technik
- basiert auf React
## Umgebungsvariablen
- In der **Datei .env** sind die von der Applikation **verwendeten Umgebungsvariablen** deklariert.
- Falls die Umgebungsvariablen _aussen_ nicht gesetzt sind, wird ein Default-Wert verwendet.
## Hinweise zur Anwendung der Applikation
- Die Applikation ist für den Unterricht gedacht.
- Die vorliegende Version hat keine Security implementiert.
- So spielt das Passwort (noch) keine Rolle.
- Wenn die Applikation gestartet ist, kann man mit</br>
_Admin->All user_</br>
Vom Backend das Verzeichnis aller User abrufen.
- Mit</br>
_User->Register_</br>
Kein ein neuer User via Backend gespeichert werden
- Danach ist ein Login des Users notwendig.</br>
_User->Login_ mit Email und Passwort</br>
Diese Login-Daten sind für die _Secrets_ Funktionen notwendig.
- Es können drei unterschiedliche Secrets gespeicher werden.
- Und es können die eigenen Secrets abgerufen werden.
## Build image
see Dockerfile
```Bash
docker build -t tresorfrontendimg .
```
## Start container local
```Bash
docker run -p 80:80 --name tresorfrontend tresorfrontendimg
(c) 2024 P. Rutschmann

View File

@ -0,0 +1,10 @@
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri /index.html;
}
}

View File

@ -0,0 +1,42 @@
{
"name": "01_tresorfrontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.6.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-google-recaptcha": "^3.1.0",
"react-router-dom": "^6.22.3",
"react-scripts": "5.0.1",
"reactstrap": "^9.2.2",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="./favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -0,0 +1,15 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,44 @@
import React, {useState} from 'react';
import {BrowserRouter, Route, Routes} from "react-router-dom";
import './App.css';
import './css/mvp.css';
import Home from './pages/Home';
import Layout from "./pages/Layout";
import NoPage from "./pages/NoPage";
import Users from './pages/user/Users';
import LoginUser from "./pages/user/LoginUser";
import RegisterUser from "./pages/user/RegisterUser";
import Secrets from "./pages/secret/Secrets";
import NewCredential from "./pages/secret/NewCredential";
import NewCreditCard from "./pages/secret/NewCreditCard";
import NewNote from "./pages/secret/NewNote";
/**
* App
* @author Peter Rutschmann
*/
function App() {
const [loginValues, setLoginValues] = useState({
email: "",
password: "",
});
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout loginValues={loginValues}/>}>
<Route index element={<Home/>}/>}/>
<Route path="/user/users" element={<Users loginValues={loginValues}/>}/>
<Route path="/user/login" element={<LoginUser loginValues={loginValues} setLoginValues={setLoginValues}/>}/>
<Route path="/user/register" element={<RegisterUser loginValues={loginValues} setLoginValues={setLoginValues}/>}/>
<Route path="/secret/secrets" element={<Secrets loginValues={loginValues}/>}/>
<Route path="/secret/newcredential" element={<NewCredential loginValues={loginValues}/>}/>
<Route path="/secret/newcreditcard" element={<NewCreditCard loginValues={loginValues}/>}/>
<Route path="/secret/newnote" element={<NewNote loginValues={loginValues}/>}/>
<Route path="*" element={<NoPage/>}/>
</Route>
</Routes>
</BrowserRouter>
)
}
export default App;

View File

@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@ -0,0 +1,75 @@
/**
* Fetch methodes for secret api calls
* @author Peter Rutschmann
*/
//Post secret to server
export const postSecret = async ({loginValues, content}) => {
const protocol = process.env.REACT_APP_API_PROTOCOL; // "http"
const host = process.env.REACT_APP_API_HOST; // "localhost"
const port = process.env.REACT_APP_API_PORT; // "8080"
const path = process.env.REACT_APP_API_PATH; // "/api"
const portPart = port ? `:${port}` : ''; // port is optional
const API_URL = `${protocol}://${host}${portPart}${path}`;
console.log(loginValues)
try {
const response = await fetch(`${API_URL}/secrets`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: loginValues.email,
encryptPassword: loginValues.password,
content: content
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Server response failed.');
}
const data = await response.json();
console.log('Secret successfully posted:', data);
return data;
} catch (error) {
console.error('Error posting secret:', error.message);
throw new Error('Failed to save secret. ' || error.message);
}
};
//get all secrets for a user identified by its email
export const getSecretsforUser = async (loginValues) => {
const protocol = process.env.REACT_APP_API_PROTOCOL; // "http"
const host = process.env.REACT_APP_API_HOST; // "localhost"
const port = process.env.REACT_APP_API_PORT; // "8080"
const path = process.env.REACT_APP_API_PATH; // "/api"
const portPart = port ? `:${port}` : ''; // port is optional
const API_URL = `${protocol}://${host}${portPart}${path}`;
try {
const response = await fetch(`${API_URL}/secrets/byemail`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: loginValues.email,
encryptPassword: loginValues.password
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Server response failed.');
}
const data = await response.json();
console.log('Secret successfully got:', data);
return data;
} catch (error) {
console.error('Failed to get secrets:', error.message);
throw new Error('Failed to get secrets. ' || error.message);
}
};

View File

@ -0,0 +1,71 @@
/**
* Fetch methodes for user api calls
* @author Peter Rutschmann
*/
export const getUsers = async () => {
const protocol = process.env.REACT_APP_API_PROTOCOL; // "http"
const host = process.env.REACT_APP_API_HOST; // "localhost"
const port = process.env.REACT_APP_API_PORT; // "8080"
const path = process.env.REACT_APP_API_PATH; // "/api"
const portPart = port ? `:${port}` : ''; // port is optional
const API_URL = `${protocol}://${host}${portPart}${path}`;
try {
const response = await fetch(`${API_URL}/users`, {
method: 'Get',
headers: {
'Accept': 'application/json'
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Server response failed.');
}
const data = await response.json();
console.log('User successfully got:', data);
return data;
} catch (error) {
console.error('Failed to get user:', error.message);
throw new Error('Failed to get user. ' || error.message);
}
}
export const postUser = async (content) => {
const protocol = process.env.REACT_APP_API_PROTOCOL; // "http"
const host = process.env.REACT_APP_API_HOST; // "localhost"
const port = process.env.REACT_APP_API_PORT; // "8080"
const path = process.env.REACT_APP_API_PATH; // "/api"
const portPart = port ? `:${port}` : ''; // port is optional
const API_URL = `${protocol}://${host}${portPart}${path}`;
try {
const response = await fetch(`${API_URL}/users`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
firstName: `${content.firstName}`,
lastName: `${content.lastName}`,
email: `${content.email}`,
password: `${content.password}`,
passwordConfirmation: `${content.passwordConfirmation}`
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Server response failed.');
}
const data = await response.json();
console.log('User successfully posted:', data);
return data;
} catch (error) {
console.error('Failed to post user:', error.message);
throw new Error('Failed to save user. ' || error.message);
}
};

View File

@ -0,0 +1,470 @@
/* MVP.css v1.7.4 - https://github.com/andybrewer/mvp */
:root {
--border-radius: 5px;
--box-shadow: 2px 2px 10px;
--color: #118bee;
--color-accent: #118bee15;
--color-bg: #fff;
--color-bg-secondary: #e9e9e9;
--color-secondary: #920de9;
--color-secondary-accent: #920de90b;
--color-shadow: #f4f4f4;
--color-text: #000;
--color-text-secondary: #999;
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
--hover-brightness: 1.2;
--justify-important: center;
--justify-normal: left;
--line-height: 1.5;
--width-card: 285px;
--width-card-medium: 460px;
--width-card-wide: 800px;
--width-content: 1080px;
}
/*
@media (prefers-color-scheme: dark) {
:root {
--color: #0097fc;
--color-accent: #0097fc4f;
--color-bg: #333;
--color-bg-secondary: #555;
--color-secondary: #e20de9;
--color-secondary-accent: #e20de94f;
--color-shadow: #bbbbbb20;
--color-text: #f7f7f7;
--color-text-secondary: #aaa;
}
}
*/
/* Layout */
article aside {
background: var(--color-secondary-accent);
border-left: 4px solid var(--color-secondary);
padding: 0.01rem 0.8rem;
}
body {
background: var(--color-bg);
color: var(--color-text);
font-family: var(--font-family);
line-height: var(--line-height);
margin: 0;
overflow-x: hidden;
padding: 0;
}
footer,
header,
main {
margin: 0 auto;
max-width: var(--width-content);
padding: 3rem 1rem;
}
hr {
background-color: var(--color-bg-secondary);
border: none;
height: 1px;
margin: 4rem 0;
width: 100%;
}
section {
display: flex;
flex-wrap: wrap;
justify-content: var(--justify-important);
}
section img,
article img {
max-width: 100%;
}
section pre {
overflow: auto;
}
section aside {
border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow) var(--color-shadow);
margin: 1rem;
padding: 1.25rem;
width: var(--width-card);
}
section aside:hover {
box-shadow: var(--box-shadow) var(--color-bg-secondary);
}
[hidden] {
display: none;
}
/* Headers */
article header,
div header,
main header {
padding-top: 0;
}
header {
text-align: var(--justify-important);
}
header a b,
header a em,
header a i,
header a strong {
margin-left: 0.5rem;
margin-right: 0.5rem;
}
header nav img {
margin: 1rem 0;
}
section header {
padding-top: 0;
width: 100%;
}
/* Nav */
nav {
align-items: center;
display: flex;
font-weight: bold;
justify-content: space-between;
/*Peru margin-bottom: 7rem; */
margin-bottom: 1rem;
}
nav ul {
list-style: none;
padding: 0;
}
nav ul li {
display: inline-block;
margin: 0 0.5rem;
position: relative;
text-align: left;
}
/* Nav Dropdown */
nav ul li:hover ul {
display: block;
}
nav ul li ul {
background: var(--color-bg);
border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow) var(--color-shadow);
display: none;
height: auto;
left: -2px;
padding: .5rem 1rem;
position: absolute;
top: 1.7rem;
white-space: nowrap;
width: auto;
z-index: 1;
}
nav ul li ul::before {
/* fill gap above to make mousing over them easier */
content: "";
position: absolute;
left: 0;
right: 0;
top: -0.5rem;
height: 0.5rem;
}
nav ul li ul li,
nav ul li ul li a {
display: block;
}
/* Typography */
code,
samp {
background-color: var(--color-accent);
border-radius: var(--border-radius);
color: var(--color-text);
display: inline-block;
margin: 0 0.1rem;
padding: 0 0.5rem;
}
details {
margin: 1.3rem 0;
}
details summary {
font-weight: bold;
cursor: pointer;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: var(--line-height);
}
mark {
padding: 0.1rem;
}
ol li,
ul li {
padding: 0.2rem 0;
}
p {
margin: 0.75rem 0;
padding: 0;
}
pre {
margin: 1rem 0;
max-width: var(--width-card-wide);
padding: 1rem 0;
}
pre code,
pre samp {
display: block;
max-width: var(--width-card-wide);
padding: 0.5rem 2rem;
white-space: pre-wrap;
}
small {
color: var(--color-text-secondary);
}
sup {
background-color: var(--color-secondary);
border-radius: var(--border-radius);
color: var(--color-bg);
font-size: xx-small;
font-weight: bold;
margin: 0.2rem;
padding: 0.2rem 0.3rem;
position: relative;
top: -2px;
}
/* Links */
a {
color: var(--color);
display: inline-block;
font-weight: bold;
text-decoration: none;
}
a:hover {
filter: brightness(var(--hover-brightness));
text-decoration: underline;
}
a b,
a em,
a i,
a strong,
button {
border-radius: var(--border-radius);
display: inline-block;
font-size: medium;
font-weight: bold;
line-height: var(--line-height);
margin: 0.5rem 0;
padding: 1rem 2rem;
}
button {
font-family: var(--font-family);
}
button:hover {
cursor: pointer;
filter: brightness(var(--hover-brightness));
}
a b,
a strong,
button {
background-color: var(--color);
border: 2px solid var(--color);
color: var(--color-bg);
}
a em,
a i {
border: 2px solid var(--color);
border-radius: var(--border-radius);
color: var(--color);
display: inline-block;
padding: 1rem 2rem;
}
article aside a {
color: var(--color-secondary);
}
/* Images */
figure {
margin: 0;
padding: 0;
}
figure img {
max-width: 100%;
}
figure figcaption {
color: var(--color-text-secondary);
}
/* Forms */
button:disabled,
input:disabled {
background: var(--color-bg-secondary);
border-color: var(--color-bg-secondary);
color: var(--color-text-secondary);
cursor: not-allowed;
}
button[disabled]:hover {
filter: none;
}
form {
border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow) var(--color-shadow);
display: block;
max-width: var(--width-card-wide);
min-width: var(--width-card);
padding: 1.5rem;
text-align: var(--justify-normal);
}
form header {
margin: 1.5rem 0;
padding: 1.5rem 0;
}
input,
label,
select,
textarea {
display: block;
font-size: inherit;
max-width: var(--width-card-wide);
}
input[type="checkbox"],
input[type="radio"] {
display: inline-block;
}
input[type="checkbox"]+label,
input[type="radio"]+label {
display: inline-block;
font-weight: normal;
position: relative;
top: 1px;
}
input,
select,
textarea {
border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius);
margin-bottom: 1rem;
padding: 0.4rem 0.8rem;
}
input[readonly],
textarea[readonly] {
background-color: var(--color-bg-secondary);
}
label {
font-weight: bold;
margin-bottom: 0.2rem;
}
/* Tables */
table {
border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius);
border-spacing: 0;
display: inline-block;
max-width: 100%;
overflow-x: auto;
padding: 0;
white-space: nowrap;
}
table td,
table th,
table tr {
padding: 0.4rem 0.8rem;
text-align: var(--justify-important);
}
table thead {
background-color: var(--color);
border-collapse: collapse;
border-radius: var(--border-radius);
color: var(--color-bg);
margin: 0;
padding: 0;
}
table thead th:first-child {
border-top-left-radius: var(--border-radius);
}
table thead th:last-child {
border-top-right-radius: var(--border-radius);
}
table thead th:first-child,
table tr td:first-child {
text-align: var(--justify-normal);
}
table tr:nth-child(even) {
background-color: var(--color-accent);
}
/* Quotes */
blockquote {
display: block;
font-size: x-large;
line-height: var(--line-height);
margin: 1rem auto;
max-width: var(--width-card-medium);
padding: 1.5rem 1rem;
text-align: var(--justify-important);
}
blockquote footer {
color: var(--color-text-secondary);
display: block;
font-size: small;
line-height: var(--line-height);
padding: 1.5rem 0;
}

View File

@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@ -0,0 +1,20 @@
import '../App.css';
/**
* Home
* @author Peter Rutschmann
*/
const Home = () => {
return (
<>
<h1>Speichern Sie Ihre Daten sicher ab.</h1>
<form>
<p>In dieser Applikation können Sie, nachdem Sie sich registriert haben, Ihre sensitiven Daten verschlüsselt
in einer Datenbank speichern.</p>
<p>Erstellen Sie ein neues Secret. Wählen Sie zwischen Credentials, Credit-Cards und Notes.</p>
</form>
</>
);
};
export default Home;

View File

@ -0,0 +1,47 @@
import { Outlet, Link } from "react-router-dom";
/**
* Layout
* @author Peter Rutschmann
*/
const Layout = ({loginValues}) => {
return (
<>
<nav>
<h1>The secret tresor application</h1>
<p>{loginValues.email === '' ? 'No user logged in' : 'user:' + loginValues.email}</p>
<ul>
<li><a href="/">Secrets</a>
<ul>
<li><Link to="/secret/secrets">my secrets</Link></li>
<li><Link to="/secret/newcredential">new credential</Link></li>
<li><Link to="/secret/newcreditcard">new credit-card</Link></li>
<li><Link to="/secret/newnote">new note</Link></li>
</ul>
</li>
<li><a href="/">User</a>
<ul>
<li><Link to="/user/login">login</Link></li>
<li><Link to="/user/register">register</Link></li>
</ul>
</li>
<li><a href="/">Admin</a>
<ul>
<li><Link to="/user/users">All users</Link></li>
<li>Add user</li>
<li><Link to="/user/users/:id">Edit user</Link></li>
<li>All secrets</li>
</ul>
</li>
<li>
<Link to="/">About</Link>
</li>
</ul>
</nav>
<hr/>
<Outlet/>
</>
)
};
export default Layout;

View File

@ -0,0 +1,16 @@
import '../App.css';
/**
* NoPage
* @author Peter Rutschmann
*/
const NoPage = () => {
return(
<div>
<h1>404</h1>
<p>Page currently not supported.</p>
</div>
)
};
export default NoPage;

View File

@ -0,0 +1,85 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {postSecret} from "../../comunication/FetchSecrets";
/**
* NewCredential
* @author Peter Rutschmann
*/
function NewCredential({loginValues}) {
const initialState = {
kindid: 1,
kind:"credential",
userName: "",
password: "",
url: ""
};
const [credentialValues, setCredentialValues] = useState(initialState);
const [errorMessage, setErrorMessage] = useState('');
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
setErrorMessage('');
console.log(loginValues)
try {
const content = credentialValues;
await postSecret({loginValues, content});
setCredentialValues(initialState);
navigate('/secret/secrets');
} catch (error) {
console.error('Failed to fetch to server:', error.message);
setErrorMessage(error.message);
}
};
return (
<div>
<h2>Add new credential secret</h2>
<form onSubmit={handleSubmit}>
<section>
<aside>
<div>
<label>username:</label>
<input
type="text"
value={credentialValues.userName}
onChange={(e) =>
setCredentialValues(prevValues => ({...prevValues, userName: e.target.value}))}
required
placeholder="Please enter username"
/>
</div>
<div>
<label>password:</label>
<input
type="text"
value={credentialValues.password}
onChange={(e) =>
setCredentialValues(prevValues => ({...prevValues, password: e.target.value}))}
required
placeholder="Please enter password"
/>
</div>
<div>
<label>url:</label>
<input
type="text"
value={credentialValues.url}
onChange={(e) =>
setCredentialValues(prevValues => ({...prevValues, url: e.target.value}))}
required
placeholder="Please enter url"
/>
</div>
<button type="submit">save secret</button>
{errorMessage && <p style={{color: 'red'}}>{errorMessage}</p>}
</aside>
</section>
</form>
</div>
);
}
export default NewCredential;

View File

@ -0,0 +1,103 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {postSecret} from "../../comunication/FetchSecrets";
/**
* NewCreditCard
* @author Peter Rutschmann
*/
function NewCreditCard({loginValues}) {
const initialState = {
kindid: 2,
kind:"creditcard",
cardtype: "",
cardnumber: "",
expiration: "",
cvv: ""
};
const [creditCardValues, setCreditCardValues] = useState(initialState);
const [errorMessage, setErrorMessage] = useState('');
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
setErrorMessage('');
try {
const content = creditCardValues;
await postSecret({loginValues, content});
setCreditCardValues(initialState);
navigate('/secret/secrets');
} catch (error) {
console.error('Failed to fetch to server:', error.message);
setErrorMessage(error.message);
}
};
return (
<div>
<h2>Add new credit-card secret</h2>
<form onSubmit={handleSubmit}>
<section>
<aside>
<div>
<label>card type:</label>
<select
value={creditCardValues.cardtype}
onChange={(e) =>
setCreditCardValues((prevValues) => ({
...prevValues,
cardtype: e.target.value,
}))}
required
>
<option value="" disabled>
Please select card type
</option>
<option value="Visa">Visa</option>
<option value="Mastercard">Mastercard</option>
</select>
</div>
<div>
<label>cardnumber:</label>
<input
type="text"
value={creditCardValues.cardnumber}
onChange={(e) =>
setCreditCardValues(prevValues => ({...prevValues, cardnumber: e.target.value}))}
required
placeholder="Please enter cardnumber"
/>
</div>
<div>
<label>expiration (mm/yy):</label>
<input
type="text"
value={creditCardValues.expiration}
onChange={(e) =>
setCreditCardValues(prevValues => ({...prevValues, expiration: e.target.value}))}
required
placeholder="Please enter expiration"
/>
</div>
<div>
<label>cvv:</label>
<input
type="text"
value={creditCardValues.cvv}
onChange={(e) =>
setCreditCardValues(prevValues => ({...prevValues, cvv: e.target.value}))}
required
placeholder="Please enter cvv"
/>
</div>
<button type="submit">Save secret</button>
{errorMessage && <p style={{color: 'red'}}>{errorMessage}</p>}
</aside>
</section>
</form>
</div>
);
}
export default NewCreditCard;

View File

@ -0,0 +1,79 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {postSecret} from "../../comunication/FetchSecrets";
/**
* NewNote
* @author Peter Rutschmann
*/
function NewNote({loginValues}) {
const initialState = {
kindid: 3,
kind:"note",
title: "",
content: "",
};
const [noteValues, setNoteValues] = useState(initialState);
const [errorMessage, setErrorMessage] = useState('');
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
setErrorMessage('');
try {
const content = noteValues;
await postSecret({loginValues, content});
setNoteValues(initialState);
navigate('/secret/secrets');
} catch (error) {
console.error('Failed to fetch to server:', error.message);
setErrorMessage(error.message);
}
};
return (
<div>
<h2>Add new note secret</h2>
<form onSubmit={handleSubmit}>
<section>
<aside>
<div>
<label>title:</label>
<input
type="text"
value={noteValues.title}
onChange={(e) =>
setNoteValues(prevValues => ({...prevValues, title: e.target.value}))}
required
placeholder="Please enter a title *"
/>
</div>
<div>
<label>content:</label>
<textarea
rows={4}
style={{
resize: 'both', // Ermöglicht Größenänderung in beide Richtungen
width: '24%', // Standardbreite (kann angepasst werden)
minWidth: '190px', // Minimale Breite
minHeight: '100px', // Minimale Höhe
}}
value={noteValues.content}
onChange={(e) =>
setNoteValues(prevValues =>
({...prevValues, content: e.target.value}))}
required
placeholder="Please enter a content *"
/>
</div>
<button type="submit">Save secret</button>
{errorMessage && <p style={{color: 'red'}}>{errorMessage}</p>}
</aside>
</section>
</form>
</div>
);
}
export default NewNote;

View File

@ -0,0 +1,70 @@
import '../../App.css';
import React, {useEffect, useState} from 'react';
import {getSecretsforUser} from "../../comunication/FetchSecrets";
/**
* Secrets
* @author Peter Rutschmann
*/
const Secrets = ({loginValues}) => {
const [secrets, setSecrets] = useState([]);
const [errorMessage, setErrorMessage] = useState('');
useEffect(() => {
const fetchSecrets = async () => {
setErrorMessage('');
if( ! loginValues.email){
console.error('Secrets: No valid email, please do login first:' + loginValues);
setErrorMessage("No valid email, please do login first.");
} else {
try {
const data = await getSecretsforUser(loginValues);
console.log(data);
setSecrets(data);
} catch (error) {
console.error('Failed to fetch to server:', error.message);
setErrorMessage(error.message);
}
}
};
fetchSecrets();
}, [loginValues]);
return (
<>
<h1>my secrets</h1>
{errorMessage && <p style={{color: 'red'}}>{errorMessage}</p>}
<form>
<h2>secrets</h2>
<table border="1">
<thead>
<tr>
<th>secret id</th>
<th>user id</th>
<th>content</th>
</tr>
</thead>
<tbody>
{secrets?.length > 0 ? (
secrets.map(secret => (
<tr key={secret.id}>
<td>{secret.id}</td>
<td>{secret.userId}</td>
<td>
<pre>{JSON.stringify(secret.content, null, 2)}</pre>
</td>
</tr>
))
) : (
<tr>
<td colSpan="3">No secrets available</td>
</tr>
)}
</tbody>
</table>
</form>
</>
);
};
export default Secrets;

View File

@ -0,0 +1,52 @@
import { useNavigate } from 'react-router-dom';
/**
* LoginUser
* @author Peter Rutschmann
*/
function LoginUser({loginValues, setLoginValues}) {
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
console.log(loginValues);
navigate('/')
};
return (
<div>
<h2>Login user</h2>
<form onSubmit={handleSubmit}>
<section>
<aside>
<div>
<label>Email:</label>
<input
type="text"
value={loginValues.email}
onChange={(e) =>
setLoginValues(prevValues => ({...prevValues, email: e.target.value}))}
required
placeholder="Please enter your email *"
/>
</div>
<div>
<label>Password:</label>
<input
type="text"
value={loginValues.password}
onChange={(e) =>
setLoginValues(prevValues => ({...prevValues, password: e.target.value}))}
required
placeholder="Please enter your password *"
/>
</div>
</aside>
</section>
<button type="submit">Login</button>
</form>
</div>
);
}
export default LoginUser;

View File

@ -0,0 +1,117 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {postUser} from "../../comunication/FetchUser";
/**
* RegisterUser
* @author Peter Rutschmann
*/
function RegisterUser({loginValues, setLoginValues}) {
const navigate = useNavigate();
const initialState = {
firstName: "",
lastName: "",
email: "",
password: "",
passwordConfirmation: "",
errorMessage: ""
};
const [credentials, setCredentials] = useState(initialState);
const [errorMessage, setErrorMessage] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
setErrorMessage('');
//validate
if(credentials.password !== credentials.passwordConfirmation) {
console.log("password != passwordConfirmation");
setErrorMessage('Password and password-confirmation are not equal.');
return;
}
try {
await postUser(credentials);
setLoginValues({userName: credentials.email, password: credentials.password});
setCredentials(initialState);
navigate('/');
} catch (error) {
console.error('Failed to fetch to server:', error.message);
setErrorMessage(error.message);
}
};
return (
<div>
<h2>Register user</h2>
<form onSubmit={handleSubmit}>
<section>
<aside>
<div>
<label>Firstname:</label>
<input
type="text"
value={credentials.firstName}
onChange={(e) =>
setCredentials(prevValues => ({...prevValues, firstName: e.target.value}))}
required
placeholder="Please enter your firstname *"
/>
</div>
<div>
<label>Lastname:</label>
<input
type="text"
value={credentials.lastName}
onChange={(e) =>
setCredentials(prevValues => ({...prevValues, lastName: e.target.value}))}
required
placeholder="Please enter your lastname *"
/>
</div>
<div>
<label>Email:</label>
<input
type="text"
value={credentials.email}
onChange={(e) =>
setCredentials(prevValues => ({...prevValues, email: e.target.value}))}
required
placeholder="Please enter your email"
/>
</div>
</aside>
<aside>
<div>
<label>Password:</label>
<input
type="text"
value={credentials.password}
onChange={(e) =>
setCredentials(prevValues => ({...prevValues, password: e.target.value}))}
required
placeholder="Please enter your pwd *"
/>
</div>
<div>
<label>Password confirmation:</label>
<input
type="text"
value={credentials.passwordConfirmation}
onChange={(e) =>
setCredentials(prevValues => ({...prevValues, passwordConfirmation: e.target.value}))}
required
placeholder="Please confirm your pwd *"
/>
</div>
</aside>
</section>
<button type="submit">Register</button>
{errorMessage && <p style={{ color: 'red' }}>{errorMessage}</p>}
</form>
</div>
);
}
export default RegisterUser;

View File

@ -0,0 +1,40 @@
import '../../App.css';
import React, {useEffect, useState} from "react";
import {getUsers} from "../../comunication/FetchUser";
/**
* Users
* @author Peter Rutschmann
*/
const Users = ({loginValues}) => {
const [users, setUsers] = useState([]);
const [errorMessage, setErrorMessage] = useState('');
useEffect(() => {
const fetchUsers = async () => {
try {
const users = await getUsers();
console.log(users);
setUsers(users);
} catch (error) {
console.error('Failed to fetch to server:', error.message);
setErrorMessage(error.message);
}
};
fetchUsers();
}, [loginValues]);
return (
<>
<h1>Client list</h1>
<ul>
{users.map(user => (
<li key={user.id}>{user.id} {user.firstName} {user.lastName} - {user.email} - {user.password}</li>
))}
</ul>
{errorMessage && <p style={{ color: 'red' }}>{errorMessage}</p>}
</>
);
};
export default Users;

View File

@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';