From 1e18e0ad7a47536be92384bbf815e0923a06698d Mon Sep 17 00:00:00 2001 From: realtradam Date: Tue, 23 Jul 2024 20:47:31 -0400 Subject: split front and back end, add react to project --- .mvn/wrapper/maven-wrapper.properties | 19 - Dockerfile | 20 - backend/.mvn/wrapper/maven-wrapper.properties | 19 + backend/Dockerfile | 20 + backend/mvnw | 259 ++ backend/mvnw.cmd | 149 ++ backend/pom.xml | 112 + .../src/main/java/com/blog/web/WebApplication.java | 11 + .../blog/web/controllers/ArticleController.java | 125 + .../com/blog/web/controllers/AuthController.java | 56 + .../src/main/java/com/blog/web/dto/ArticleDto.java | 114 + .../java/com/blog/web/dto/ArticlePublicDto.java | 96 + .../java/com/blog/web/dto/RegistrationDto.java | 55 + .../java/com/blog/web/mappers/ArticleMapper.java | 22 + .../src/main/java/com/blog/web/models/Article.java | 104 + .../src/main/java/com/blog/web/models/Role.java | 36 + .../main/java/com/blog/web/models/UserEntity.java | 72 + .../com/blog/web/repository/ArticleRepository.java | 12 + .../com/blog/web/repository/RoleRepository.java | 10 + .../com/blog/web/repository/UserRepository.java | 14 + .../web/security/CustomUserDetailsService.java | 29 + .../java/com/blog/web/security/SecurityConfig.java | 38 + .../java/com/blog/web/security/SecurityUtil.java | 12 + .../java/com/blog/web/services/ArticleService.java | 22 + .../java/com/blog/web/services/UserService.java | 16 + .../blog/web/services/impl/ArticleServiceImpl.java | 94 + .../blog/web/services/impl/UserServiceImpl.java | 60 + backend/src/main/resources/application.properties | 10 + ...abstract-polygonal-banner-background-vector.jpg | Bin 0 -> 1841814 bytes .../main/resources/templates/articles-list.html | 16 + .../main/resources/templates/articles/edit.html | 63 + .../src/main/resources/templates/articles/new.html | 62 + .../main/resources/templates/articles/show.html | 87 + .../src/main/resources/templates/auth/login.html | 48 + .../main/resources/templates/auth/register.html | 63 + backend/src/main/resources/templates/index.html | 146 ++ backend/src/main/resources/templates/layout.html | 149 ++ backend/src/main/resources/templates/post.html | 266 ++ .../java/com/blog/web/WebApplicationTests.java | 13 + .../web/repository/ArticleRepositoryTests.java | 81 + .../com/blog/web/services/ArticleServiceTests.java | 78 + frontend/.eslintrc.cjs | 18 + frontend/.gitignore | 24 + frontend/README.md | 30 + frontend/index.html | 13 + frontend/package-lock.json | 2739 ++++++++++++++++++++ frontend/package.json | 28 + frontend/public/vite.svg | 1 + frontend/src/App.css | 42 + frontend/src/App.tsx | 35 + frontend/src/assets/react.svg | 1 + frontend/src/index.css | 68 + frontend/src/main.tsx | 10 + frontend/src/vite-env.d.ts | 1 + frontend/tsconfig.app.json | 27 + frontend/tsconfig.json | 11 + frontend/tsconfig.node.json | 13 + frontend/vite.config.ts | 7 + mvnw | 259 -- mvnw.cmd | 149 -- pom.xml | 112 - src/main/java/com/blog/web/WebApplication.java | 11 - .../blog/web/controllers/ArticleController.java | 125 - .../com/blog/web/controllers/AuthController.java | 56 - src/main/java/com/blog/web/dto/ArticleDto.java | 114 - .../java/com/blog/web/dto/ArticlePublicDto.java | 96 - .../java/com/blog/web/dto/RegistrationDto.java | 55 - .../java/com/blog/web/mappers/ArticleMapper.java | 22 - src/main/java/com/blog/web/models/Article.java | 104 - src/main/java/com/blog/web/models/Role.java | 36 - src/main/java/com/blog/web/models/UserEntity.java | 72 - .../com/blog/web/repository/ArticleRepository.java | 12 - .../com/blog/web/repository/RoleRepository.java | 10 - .../com/blog/web/repository/UserRepository.java | 14 - .../web/security/CustomUserDetailsService.java | 29 - .../java/com/blog/web/security/SecurityConfig.java | 38 - .../java/com/blog/web/security/SecurityUtil.java | 12 - .../java/com/blog/web/services/ArticleService.java | 22 - .../java/com/blog/web/services/UserService.java | 16 - .../blog/web/services/impl/ArticleServiceImpl.java | 94 - .../blog/web/services/impl/UserServiceImpl.java | 60 - src/main/resources/application.properties | 10 - ...abstract-polygonal-banner-background-vector.jpg | Bin 1841814 -> 0 bytes src/main/resources/templates/articles-list.html | 16 - src/main/resources/templates/articles/edit.html | 63 - src/main/resources/templates/articles/new.html | 62 - src/main/resources/templates/articles/show.html | 87 - src/main/resources/templates/auth/login.html | 48 - src/main/resources/templates/auth/register.html | 63 - src/main/resources/templates/index.html | 146 -- src/main/resources/templates/layout.html | 149 -- src/main/resources/templates/post.html | 266 -- .../java/com/blog/web/WebApplicationTests.java | 13 - .../web/repository/ArticleRepositoryTests.java | 81 - .../com/blog/web/services/ArticleServiceTests.java | 78 - 95 files changed, 5707 insertions(+), 2639 deletions(-) delete mode 100644 .mvn/wrapper/maven-wrapper.properties delete mode 100644 Dockerfile create mode 100644 backend/.mvn/wrapper/maven-wrapper.properties create mode 100644 backend/Dockerfile create mode 100755 backend/mvnw create mode 100644 backend/mvnw.cmd create mode 100644 backend/pom.xml create mode 100644 backend/src/main/java/com/blog/web/WebApplication.java create mode 100644 backend/src/main/java/com/blog/web/controllers/ArticleController.java create mode 100644 backend/src/main/java/com/blog/web/controllers/AuthController.java create mode 100644 backend/src/main/java/com/blog/web/dto/ArticleDto.java create mode 100644 backend/src/main/java/com/blog/web/dto/ArticlePublicDto.java create mode 100644 backend/src/main/java/com/blog/web/dto/RegistrationDto.java create mode 100644 backend/src/main/java/com/blog/web/mappers/ArticleMapper.java create mode 100644 backend/src/main/java/com/blog/web/models/Article.java create mode 100644 backend/src/main/java/com/blog/web/models/Role.java create mode 100644 backend/src/main/java/com/blog/web/models/UserEntity.java create mode 100644 backend/src/main/java/com/blog/web/repository/ArticleRepository.java create mode 100644 backend/src/main/java/com/blog/web/repository/RoleRepository.java create mode 100644 backend/src/main/java/com/blog/web/repository/UserRepository.java create mode 100644 backend/src/main/java/com/blog/web/security/CustomUserDetailsService.java create mode 100644 backend/src/main/java/com/blog/web/security/SecurityConfig.java create mode 100644 backend/src/main/java/com/blog/web/security/SecurityUtil.java create mode 100644 backend/src/main/java/com/blog/web/services/ArticleService.java create mode 100644 backend/src/main/java/com/blog/web/services/UserService.java create mode 100644 backend/src/main/java/com/blog/web/services/impl/ArticleServiceImpl.java create mode 100644 backend/src/main/java/com/blog/web/services/impl/UserServiceImpl.java create mode 100644 backend/src/main/resources/application.properties create mode 100644 backend/src/main/resources/static/abstract-polygonal-banner-background-vector.jpg create mode 100644 backend/src/main/resources/templates/articles-list.html create mode 100644 backend/src/main/resources/templates/articles/edit.html create mode 100644 backend/src/main/resources/templates/articles/new.html create mode 100644 backend/src/main/resources/templates/articles/show.html create mode 100644 backend/src/main/resources/templates/auth/login.html create mode 100644 backend/src/main/resources/templates/auth/register.html create mode 100644 backend/src/main/resources/templates/index.html create mode 100644 backend/src/main/resources/templates/layout.html create mode 100644 backend/src/main/resources/templates/post.html create mode 100644 backend/src/test/java/com/blog/web/WebApplicationTests.java create mode 100644 backend/src/test/java/com/blog/web/repository/ArticleRepositoryTests.java create mode 100644 backend/src/test/java/com/blog/web/services/ArticleServiceTests.java create mode 100644 frontend/.eslintrc.cjs create mode 100644 frontend/.gitignore create mode 100644 frontend/README.md create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/vite.svg create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/assets/react.svg create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/vite-env.d.ts create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts delete mode 100755 mvnw delete mode 100644 mvnw.cmd delete mode 100644 pom.xml delete mode 100644 src/main/java/com/blog/web/WebApplication.java delete mode 100644 src/main/java/com/blog/web/controllers/ArticleController.java delete mode 100644 src/main/java/com/blog/web/controllers/AuthController.java delete mode 100644 src/main/java/com/blog/web/dto/ArticleDto.java delete mode 100644 src/main/java/com/blog/web/dto/ArticlePublicDto.java delete mode 100644 src/main/java/com/blog/web/dto/RegistrationDto.java delete mode 100644 src/main/java/com/blog/web/mappers/ArticleMapper.java delete mode 100644 src/main/java/com/blog/web/models/Article.java delete mode 100644 src/main/java/com/blog/web/models/Role.java delete mode 100644 src/main/java/com/blog/web/models/UserEntity.java delete mode 100644 src/main/java/com/blog/web/repository/ArticleRepository.java delete mode 100644 src/main/java/com/blog/web/repository/RoleRepository.java delete mode 100644 src/main/java/com/blog/web/repository/UserRepository.java delete mode 100644 src/main/java/com/blog/web/security/CustomUserDetailsService.java delete mode 100644 src/main/java/com/blog/web/security/SecurityConfig.java delete mode 100644 src/main/java/com/blog/web/security/SecurityUtil.java delete mode 100644 src/main/java/com/blog/web/services/ArticleService.java delete mode 100644 src/main/java/com/blog/web/services/UserService.java delete mode 100644 src/main/java/com/blog/web/services/impl/ArticleServiceImpl.java delete mode 100644 src/main/java/com/blog/web/services/impl/UserServiceImpl.java delete mode 100644 src/main/resources/application.properties delete mode 100644 src/main/resources/static/abstract-polygonal-banner-background-vector.jpg delete mode 100644 src/main/resources/templates/articles-list.html delete mode 100644 src/main/resources/templates/articles/edit.html delete mode 100644 src/main/resources/templates/articles/new.html delete mode 100644 src/main/resources/templates/articles/show.html delete mode 100644 src/main/resources/templates/auth/login.html delete mode 100644 src/main/resources/templates/auth/register.html delete mode 100644 src/main/resources/templates/index.html delete mode 100644 src/main/resources/templates/layout.html delete mode 100644 src/main/resources/templates/post.html delete mode 100644 src/test/java/com/blog/web/WebApplicationTests.java delete mode 100644 src/test/java/com/blog/web/repository/ArticleRepositoryTests.java delete mode 100644 src/test/java/com/blog/web/services/ArticleServiceTests.java diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index 8f96f52..0000000 --- a/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,19 +0,0 @@ -# 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. -wrapperVersion=3.3.2 -distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index ade5503..0000000 --- a/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM eclipse-temurin:17.0.11_9-jdk-alpine as base - -# Make temporary build container -From base as build -COPY . . - -# Build our app -RUN ./mvnw package - -# Go back to our main container and copy over the build jar to it -FROM base -COPY --from=build target/*.jar app.jar - -# Add user -RUN addgroup -S spring && adduser -S spring -G spring && \ - chown -R spring:spring app.jar -USER spring:spring - -# Run the app -ENTRYPOINT ["java","-jar","/app.jar"] diff --git a/backend/.mvn/wrapper/maven-wrapper.properties b/backend/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8f96f52 --- /dev/null +++ b/backend/.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 +# +# 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. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..ade5503 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,20 @@ +FROM eclipse-temurin:17.0.11_9-jdk-alpine as base + +# Make temporary build container +From base as build +COPY . . + +# Build our app +RUN ./mvnw package + +# Go back to our main container and copy over the build jar to it +FROM base +COPY --from=build target/*.jar app.jar + +# Add user +RUN addgroup -S spring && adduser -S spring -G spring && \ + chown -R spring:spring app.jar +USER spring:spring + +# Run the app +ENTRYPOINT ["java","-jar","/app.jar"] diff --git a/backend/mvnw b/backend/mvnw new file mode 100755 index 0000000..d7c358e --- /dev/null +++ b/backend/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 +# +# 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.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-,maven-mvnd--}/ +[ -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/backend/mvnw.cmd b/backend/mvnw.cmd new file mode 100644 index 0000000..6f779cf --- /dev/null +++ b/backend/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 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.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-,maven-mvnd--}/ +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/backend/pom.xml b/backend/pom.xml new file mode 100644 index 0000000..db9c68f --- /dev/null +++ b/backend/pom.xml @@ -0,0 +1,112 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.1 + + + com.blog + web + 0.0.1-SNAPSHOT + web + Demo project for Spring Boot + + + + + + + + + + + + + + + 17 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + nz.net.ultraq.thymeleaf + thymeleaf-layout-dialect + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.postgresql + postgresql + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-security + + + org.thymeleaf.extras + thymeleaf-extras-springsecurity6 + + + org.apache.commons + commons-lang3 + + + com.h2database + h2 + 2.3.230 + test + + + junit + junit + test + + + org.springframework.security + spring-security-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + + \ No newline at end of file diff --git a/backend/src/main/java/com/blog/web/WebApplication.java b/backend/src/main/java/com/blog/web/WebApplication.java new file mode 100644 index 0000000..f5dd2ef --- /dev/null +++ b/backend/src/main/java/com/blog/web/WebApplication.java @@ -0,0 +1,11 @@ +package com.blog.web; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class WebApplication { + public static void main(String[] args) { + SpringApplication.run(WebApplication.class, args); + } +} diff --git a/backend/src/main/java/com/blog/web/controllers/ArticleController.java b/backend/src/main/java/com/blog/web/controllers/ArticleController.java new file mode 100644 index 0000000..6cd5d50 --- /dev/null +++ b/backend/src/main/java/com/blog/web/controllers/ArticleController.java @@ -0,0 +1,125 @@ +package com.blog.web.controllers; + +import com.blog.web.dto.ArticleDto; +import com.blog.web.dto.ArticlePublicDto; +import com.blog.web.models.Article; +import com.blog.web.models.UserEntity; +import com.blog.web.services.ArticleService; +import com.blog.web.services.UserService; +import jakarta.validation.Valid; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.List; + +@RestController +@Controller +public class ArticleController { + private ArticleService articleService; + private UserService userService; + + public ArticleController(ArticleService articleService, UserService userService) { + this.articleService = articleService; + this.userService = userService; + } + + @GetMapping("/get") + public Article getMethod() { + return new Article( + 5, + "blah", + "blah", + "blah", + new UserEntity(), + LocalDateTime.now(), + LocalDateTime.now() + ); + } + + @GetMapping("/articles") + public HashSet listArticles(Model model) { + HashSet articles = new HashSet(articleService.findAllArticles()); + //UserEntity user = userService.getLoggedInUser().orElse(new UserEntity()); + //model.addAttribute("user", user); + //model.addAttribute("articles", articles); + //return "index"; + return articles; + } + + @GetMapping("/articles/{articleId}") + public String showArticle(@PathVariable("articleId") long articleId, Model model) { + ArticleDto articleDto = articleService.findArticleById(articleId); + model.addAttribute("article", articleDto); + UserEntity user = userService.getLoggedInUser().orElse(new UserEntity()); + model.addAttribute("user", user); + return "articles/show"; + } + + @GetMapping("/articles/new") + public String createArticleForm(Model model) { + model.addAttribute("user", userService.getLoggedInUser().orElse(new UserEntity())); + model.addAttribute("article", new Article()); + return "articles/new"; + } + + @PostMapping("/articles/new") + public String saveArticle(@Valid @ModelAttribute("article") ArticleDto articleDto, BindingResult result, Model model) { + // if non-authenticated in user tries to create an article + // redirect them to login page + UserEntity user = userService.getLoggedInUser().orElse(null); + if (user == null) { + return "redirect:/userlogin"; + } else if (result.hasErrors()) { + model.addAttribute("article", articleDto); + return "articles/new"; + } else { + articleService.saveArticle(articleDto); + return "redirect:/articles"; + } + } + + @GetMapping("/articles/delete/{articleId}") + public String deleteArticle(@PathVariable("articleId") Long articleId) { + articleService.delete(articleId); + return "redirect:/articles"; + } + + @GetMapping("/articles/edit/{articleId}") + public String editArticleForm(@PathVariable("articleId") long articleId, Model model) { + UserEntity user = userService.getLoggedInUser().orElse(null); + if (user != null) { + model.addAttribute("user", user); + ArticleDto articleDto = articleService.findArticleById(articleId); + model.addAttribute("article", articleDto); + } + return "articles/edit"; + } + + @PostMapping("/articles/edit/{articleId}") + public String updateArticle(@PathVariable("articleId") Long articleId, @Valid @ModelAttribute("article") ArticleDto article, BindingResult result) { + if (result.hasErrors()) { + return "articles/edit"; + } + article.setId(articleId); + articleService.updateArticle(article); + return "redirect:/articles"; + } + + @GetMapping("/articles/search") + public String searchArticle(@RequestParam(value = "search") String search, Model model) { + UserEntity user = userService.getLoggedInUser().orElse(new UserEntity()); + model.addAttribute("user", user); + List articles = articleService.searchArticles(search); + model.addAttribute("articles", articles); + return "index"; + } + + @GetMapping("/") + public String getArticles() { + return "redirect:/articles"; + } +} diff --git a/backend/src/main/java/com/blog/web/controllers/AuthController.java b/backend/src/main/java/com/blog/web/controllers/AuthController.java new file mode 100644 index 0000000..efb3672 --- /dev/null +++ b/backend/src/main/java/com/blog/web/controllers/AuthController.java @@ -0,0 +1,56 @@ +package com.blog.web.controllers; + +import com.blog.web.dto.RegistrationDto; +import com.blog.web.models.UserEntity; +import com.blog.web.services.UserService; +import jakarta.validation.Valid; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; + +@Controller +public class AuthController { + private final UserService userService; + + public AuthController(UserService userService) { + this.userService = userService; + } + + @GetMapping("/userlogin") + public String login(Model model) { + final UserEntity user = userService.getLoggedInUser().orElse(new UserEntity()); + model.addAttribute("user", user); + return "auth/login"; + } + + @GetMapping("/register") + public String getRegisterForm(Model model) { + final RegistrationDto user = new RegistrationDto(); + model.addAttribute("user", user); + return "auth/register"; + } + + @PostMapping("/register/save") + public String register(@Valid @ModelAttribute("user") RegistrationDto user, BindingResult result, Model model) { + UserEntity existingUserEmail = userService.findByEmail(user.getEmail()).orElse(null); + if (existingUserEmail != null && StringUtils.isBlank(existingUserEmail.getEmail())) { + result.rejectValue("email", "There is already a user with this email"); + } + + UserEntity existingUsername = userService.findByUsername(user.getUsername()).orElse(null); + if (existingUsername != null && StringUtils.isBlank(existingUsername.getUsername())) { + result.rejectValue("username", "There is already a user with this username"); + } + + if (result.hasErrors()) { + model.addAttribute("user", user); + return "register"; + } + userService.saveUser(user); + return "redirect:/articles?success"; + } +} diff --git a/backend/src/main/java/com/blog/web/dto/ArticleDto.java b/backend/src/main/java/com/blog/web/dto/ArticleDto.java new file mode 100644 index 0000000..d275f3b --- /dev/null +++ b/backend/src/main/java/com/blog/web/dto/ArticleDto.java @@ -0,0 +1,114 @@ +package com.blog.web.dto; + +import com.blog.web.models.Article; +import com.blog.web.models.UserEntity; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotEmpty; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.validator.constraints.URL; + +import java.time.LocalDateTime; + +public class ArticleDto { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @NotEmpty(message = "Article title should not be empty") + private String title; + @NotEmpty(message = "Article Photo URL should not be empty") + @URL(message = "Article Photo URL should be a URL") + private String photoUrl; + @NotEmpty(message = "Article Content should not be empty") + private String content; + @CreationTimestamp + private LocalDateTime createdOn; + @UpdateTimestamp + private LocalDateTime updatedOn; + @ManyToOne + @JoinColumn(name = "created_by", nullable = false) + private UserEntity createdBy; + + public ArticleDto(long id, String title, String photoUrl, String content, UserEntity createdBy, LocalDateTime createdOn, LocalDateTime updatedOn) { + this.id = id; + this.title = title; + this.photoUrl = photoUrl; + this.content = content; + this.createdBy = createdBy; + this.createdOn = createdOn; + this.updatedOn = updatedOn; + } + + public ArticleDto() { + } + + ; + + public ArticleDto(Article article) { + this.id = article.getId(); + this.title = article.getTitle(); + this.photoUrl = article.getPhotoUrl(); + this.content = article.getContent(); + this.createdBy = article.getCreatedBy(); + this.createdOn = article.getCreatedOn(); + this.updatedOn = article.getUpdatedOn(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public @NotEmpty(message = "Article title should not be empty") String getTitle() { + return title; + } + + public void setTitle(@NotEmpty(message = "Article title should not be empty") String title) { + this.title = title; + } + + public @NotEmpty(message = "Article Photo URL should not be empty") @URL(message = "Article Photo URL should be a URL") String getPhotoUrl() { + return photoUrl; + } + + public void setPhotoUrl(@NotEmpty(message = "Article Photo URL should not be empty") @URL(message = "Article Photo URL should be a URL") String photoUrl) { + this.photoUrl = photoUrl; + } + + public @NotEmpty(message = "Article Content should not be empty") String getContent() { + return content; + } + + public void setContent(@NotEmpty(message = "Article Content should not be empty") String content) { + this.content = content; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + } + + public LocalDateTime getUpdatedOn() { + return updatedOn; + } + + public void setUpdatedOn(LocalDateTime updatedOn) { + this.updatedOn = updatedOn; + } + + public UserEntity getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(UserEntity createdBy) { + this.createdBy = createdBy; + } + + public String getUsername() { return createdBy.getUsername(); } +} diff --git a/backend/src/main/java/com/blog/web/dto/ArticlePublicDto.java b/backend/src/main/java/com/blog/web/dto/ArticlePublicDto.java new file mode 100644 index 0000000..3ced6d2 --- /dev/null +++ b/backend/src/main/java/com/blog/web/dto/ArticlePublicDto.java @@ -0,0 +1,96 @@ +package com.blog.web.dto; + +import com.blog.web.models.Article; + +import java.time.LocalDateTime; + +public class ArticlePublicDto { + private Long id; + private String title; + private String photoUrl; + private String content; + private LocalDateTime createdOn; + private LocalDateTime updatedOn; + private String createdBy; + + public ArticlePublicDto(long id, String title, String photoUrl, String content, String createdBy, LocalDateTime createdOn, LocalDateTime updatedOn) { + this.id = id; + this.title = title; + this.photoUrl = photoUrl; + this.content = content; + this.createdBy = createdBy; + this.createdOn = createdOn; + this.updatedOn = updatedOn; + } + + public ArticlePublicDto() { + } + + ; + + public ArticlePublicDto(Article article) { + this.id = article.getId(); + this.title = article.getTitle(); + this.photoUrl = article.getPhotoUrl(); + this.content = article.getContent(); + this.createdBy = article.getCreatedBy().getUsername(); + this.createdOn = article.getCreatedOn(); + this.updatedOn = article.getUpdatedOn(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle( String title) { + this.title = title; + } + + public String getPhotoUrl() { + return photoUrl; + } + + public void setPhotoUrl(String photoUrl) { + this.photoUrl = photoUrl; + } + + public String getContent() { + return content; + } + + public void setContent( String content) { + this.content = content; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + } + + public LocalDateTime getUpdatedOn() { + return updatedOn; + } + + public void setUpdatedOn(LocalDateTime updatedOn) { + this.updatedOn = updatedOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } +} diff --git a/backend/src/main/java/com/blog/web/dto/RegistrationDto.java b/backend/src/main/java/com/blog/web/dto/RegistrationDto.java new file mode 100644 index 0000000..5b86557 --- /dev/null +++ b/backend/src/main/java/com/blog/web/dto/RegistrationDto.java @@ -0,0 +1,55 @@ +package com.blog.web.dto; + +import jakarta.validation.constraints.NotEmpty; + +public class RegistrationDto { + private Long id; + @NotEmpty + private String username; + @NotEmpty + private String email; + + @NotEmpty + private String password; + + public RegistrationDto() { + } + + public RegistrationDto(String username, String email, String password) { + this.username = username; + this.email = email; + this.password = password; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public @NotEmpty String getUsername() { + return username; + } + + public void setUsername(@NotEmpty String username) { + this.username = username; + } + + public @NotEmpty String getEmail() { + return email; + } + + public void setEmail(@NotEmpty String email) { + this.email = email; + } + + public void setPassword(@NotEmpty String password) { + this.password = password; + } + + public @NotEmpty String getPassword() { + return password; + } +} diff --git a/backend/src/main/java/com/blog/web/mappers/ArticleMapper.java b/backend/src/main/java/com/blog/web/mappers/ArticleMapper.java new file mode 100644 index 0000000..8fe729e --- /dev/null +++ b/backend/src/main/java/com/blog/web/mappers/ArticleMapper.java @@ -0,0 +1,22 @@ +package com.blog.web.mappers; + +import com.blog.web.dto.ArticleDto; +import com.blog.web.dto.ArticlePublicDto; +import com.blog.web.models.Article; + +public class ArticleMapper { + private ArticleMapper() { + } + + public static Article mapToArticle(ArticleDto articleDto) { + return new Article(articleDto); + } + + public static ArticleDto mapToArticleDto(Article article) { + return new ArticleDto(article); + } + + public static ArticlePublicDto mapToArticlePublicDto(Article article) { + return new ArticlePublicDto(article); + } +} diff --git a/backend/src/main/java/com/blog/web/models/Article.java b/backend/src/main/java/com/blog/web/models/Article.java new file mode 100644 index 0000000..b54907a --- /dev/null +++ b/backend/src/main/java/com/blog/web/models/Article.java @@ -0,0 +1,104 @@ +package com.blog.web.models; + +import com.blog.web.dto.ArticleDto; +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +@Entity +public class Article { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + private String title; + private String photoUrl; + private String content; + @CreationTimestamp + private LocalDateTime createdOn; + @UpdateTimestamp + private LocalDateTime updatedOn; + @ManyToOne + @JoinColumn(name = "created_by", nullable = false) + private UserEntity createdBy; + + public Article(long id, String title, String photoUrl, String content, UserEntity createdBy, LocalDateTime createdOn, LocalDateTime updatedOn) { + this.id = id; + this.title = title; + this.photoUrl = photoUrl; + this.content = content; + this.createdBy = createdBy; + this.createdOn = createdOn; + this.updatedOn = updatedOn; + } + + public Article() { + } + + public Article(ArticleDto articleDto) { + this.id = articleDto.getId(); + this.title = articleDto.getTitle(); + this.photoUrl = articleDto.getPhotoUrl(); + this.content = articleDto.getContent(); + this.createdBy = articleDto.getCreatedBy(); + this.createdOn = articleDto.getCreatedOn(); + this.updatedOn = articleDto.getUpdatedOn(); + } + + public long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getPhotoUrl() { + return photoUrl; + } + + public void setPhotoUrl(String photoUrl) { + this.photoUrl = photoUrl; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + } + + public LocalDateTime getUpdatedOn() { + return updatedOn; + } + + public void setUpdatedOn(LocalDateTime updatedOn) { + this.updatedOn = updatedOn; + } + + public UserEntity getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(UserEntity createdBy) { + this.createdBy = createdBy; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } +} diff --git a/backend/src/main/java/com/blog/web/models/Role.java b/backend/src/main/java/com/blog/web/models/Role.java new file mode 100644 index 0000000..2b7143f --- /dev/null +++ b/backend/src/main/java/com/blog/web/models/Role.java @@ -0,0 +1,36 @@ +package com.blog.web.models; + +import jakarta.persistence.*; + +import java.util.ArrayList; +import java.util.List; + +@Entity(name = "roles") +public class Role { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + private String name; + @ManyToMany(mappedBy = "roles") + private List users = new ArrayList<>(); + + public long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getUsers() { + return users; + } +} diff --git a/backend/src/main/java/com/blog/web/models/UserEntity.java b/backend/src/main/java/com/blog/web/models/UserEntity.java new file mode 100644 index 0000000..bf45b21 --- /dev/null +++ b/backend/src/main/java/com/blog/web/models/UserEntity.java @@ -0,0 +1,72 @@ +package com.blog.web.models; + +import jakarta.persistence.*; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Entity(name = "users") +// Named UserEntity to prevent conflicts with Java User object +public class UserEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + private String username; + private String email; + private String password; + @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) + @JoinTable(name = "user_roles", joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")}) + private final List roles = new ArrayList<>(); + + public boolean equals(UserEntity user) { + return this.id == user.getId(); + } + + public User toSecurityUser() { + return new User(this.getEmail(), this.getPassword(), this.getRoles().stream().map((role) -> new SimpleGrantedAuthority(role.getName())).collect(Collectors.toList())); + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles.clear(); + this.roles.addAll(roles); + } +} diff --git a/backend/src/main/java/com/blog/web/repository/ArticleRepository.java b/backend/src/main/java/com/blog/web/repository/ArticleRepository.java new file mode 100644 index 0000000..594cb15 --- /dev/null +++ b/backend/src/main/java/com/blog/web/repository/ArticleRepository.java @@ -0,0 +1,12 @@ +package com.blog.web.repository; + +import com.blog.web.models.Article; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; + +public interface ArticleRepository extends JpaRepository { + @Query("SELECT a from Article a WHERE a.title LIKE CONCAT('%', :search, '%')") + List
searchArticles(String search); +} diff --git a/backend/src/main/java/com/blog/web/repository/RoleRepository.java b/backend/src/main/java/com/blog/web/repository/RoleRepository.java new file mode 100644 index 0000000..f271bf1 --- /dev/null +++ b/backend/src/main/java/com/blog/web/repository/RoleRepository.java @@ -0,0 +1,10 @@ +package com.blog.web.repository; + +import com.blog.web.models.Role; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface RoleRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/backend/src/main/java/com/blog/web/repository/UserRepository.java b/backend/src/main/java/com/blog/web/repository/UserRepository.java new file mode 100644 index 0000000..30eefc5 --- /dev/null +++ b/backend/src/main/java/com/blog/web/repository/UserRepository.java @@ -0,0 +1,14 @@ +package com.blog.web.repository; + +import com.blog.web.models.UserEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); + + Optional findByUsername(String username); + + Optional findFirstByUsername(String username); +} diff --git a/backend/src/main/java/com/blog/web/security/CustomUserDetailsService.java b/backend/src/main/java/com/blog/web/security/CustomUserDetailsService.java new file mode 100644 index 0000000..ee3e950 --- /dev/null +++ b/backend/src/main/java/com/blog/web/security/CustomUserDetailsService.java @@ -0,0 +1,29 @@ +package com.blog.web.security; + +import com.blog.web.models.UserEntity; +import com.blog.web.repository.UserRepository; +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 java.util.stream.Collectors; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + private final UserRepository userRepository; + + public CustomUserDetailsService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + UserEntity userEntity = userRepository.findFirstByUsername(username).orElse(new UserEntity()); + if (userEntity.getUsername() != null) { + return userEntity.toSecurityUser(); + } else { + throw new UsernameNotFoundException("Invalid username"); + } + } +} diff --git a/backend/src/main/java/com/blog/web/security/SecurityConfig.java b/backend/src/main/java/com/blog/web/security/SecurityConfig.java new file mode 100644 index 0000000..17e09c7 --- /dev/null +++ b/backend/src/main/java/com/blog/web/security/SecurityConfig.java @@ -0,0 +1,38 @@ +package com.blog.web.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +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(securedEnabled = true) +public class SecurityConfig { + private CustomUserDetailsService userDetailsService; + + public SecurityConfig(CustomUserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Bean + public static PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // disabling csrf leaves us vulnerable, in a real production app do not do this + http.csrf(c -> c.disable()).cors(c -> c.disable()).authorizeHttpRequests(auths -> auths.anyRequest().permitAll()).formLogin(form -> form.loginPage("/userlogin").usernameParameter("username").passwordParameter("password").defaultSuccessUrl("/articles").loginProcessingUrl("/userlogin").failureUrl("/userlogin?error=true").permitAll()).logout(logout -> logout.logoutUrl("/logout").logoutSuccessUrl("/articles")); + return http.build(); + } + + public void configure(AuthenticationManagerBuilder builder) throws Exception { + builder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); + } +} diff --git a/backend/src/main/java/com/blog/web/security/SecurityUtil.java b/backend/src/main/java/com/blog/web/security/SecurityUtil.java new file mode 100644 index 0000000..ef0b3d9 --- /dev/null +++ b/backend/src/main/java/com/blog/web/security/SecurityUtil.java @@ -0,0 +1,12 @@ +package com.blog.web.security; + +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +public class SecurityUtil { + public static String getSessionUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return !(authentication instanceof AnonymousAuthenticationToken) ? authentication.getName() : null; + } +} diff --git a/backend/src/main/java/com/blog/web/services/ArticleService.java b/backend/src/main/java/com/blog/web/services/ArticleService.java new file mode 100644 index 0000000..1bfe38f --- /dev/null +++ b/backend/src/main/java/com/blog/web/services/ArticleService.java @@ -0,0 +1,22 @@ +package com.blog.web.services; + +import com.blog.web.dto.ArticleDto; +import com.blog.web.dto.ArticlePublicDto; +import com.blog.web.models.Article; + +import java.util.List; +import java.util.Optional; + +public interface ArticleService { + List findAllArticles(); + + Optional
saveArticle(ArticleDto article); + + ArticleDto findArticleById(long articleId); + + void updateArticle(ArticleDto articleDto); + + boolean delete(Long articleId); + + List searchArticles(String search); +} diff --git a/backend/src/main/java/com/blog/web/services/UserService.java b/backend/src/main/java/com/blog/web/services/UserService.java new file mode 100644 index 0000000..b9c231b --- /dev/null +++ b/backend/src/main/java/com/blog/web/services/UserService.java @@ -0,0 +1,16 @@ +package com.blog.web.services; + +import com.blog.web.dto.RegistrationDto; +import com.blog.web.models.UserEntity; + +import java.util.Optional; + +public interface UserService { + void saveUser(RegistrationDto registrationDto); + + Optional findByEmail(String email); + + Optional findByUsername(String username); + + public Optional getLoggedInUser(); +} diff --git a/backend/src/main/java/com/blog/web/services/impl/ArticleServiceImpl.java b/backend/src/main/java/com/blog/web/services/impl/ArticleServiceImpl.java new file mode 100644 index 0000000..7073073 --- /dev/null +++ b/backend/src/main/java/com/blog/web/services/impl/ArticleServiceImpl.java @@ -0,0 +1,94 @@ +package com.blog.web.services.impl; + +import com.blog.web.dto.ArticleDto; +import com.blog.web.dto.ArticlePublicDto; +import com.blog.web.models.Article; +import com.blog.web.models.UserEntity; +import com.blog.web.repository.ArticleRepository; +import com.blog.web.repository.UserRepository; +import com.blog.web.security.SecurityUtil; +import com.blog.web.services.ArticleService; +import com.blog.web.services.UserService; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.blog.web.mappers.ArticleMapper; + +import static com.blog.web.mappers.ArticleMapper.mapToArticle; +import static com.blog.web.mappers.ArticleMapper.mapToArticleDto; + +@Service +public class ArticleServiceImpl implements ArticleService { + final private ArticleRepository articleRepository; + final private UserRepository userRepository; + final private UserService userService; + + public ArticleServiceImpl(ArticleRepository articleRepository, UserRepository userRepository, UserService userService) { + this.userRepository = userRepository; + this.articleRepository = articleRepository; + this.userService = userService; + } + + + @Override + public List findAllArticles() { + List
articles = articleRepository.findAll(); + return articles.stream().map(ArticleMapper::mapToArticlePublicDto).collect(Collectors.toList()); + } + + @Override + public Optional
saveArticle(ArticleDto articleDto) { + String username = SecurityUtil.getSessionUser(); + UserEntity user = userRepository.findByUsername(username).orElse(null); + if (user == null) { + return null; + } + Article article = mapToArticle(articleDto); + article.setCreatedBy(user); + return Optional.of(articleRepository.save(article)); + } + + @Override + public ArticleDto findArticleById(long articleId) { + Article article = articleRepository.findById(articleId).get(); + return mapToArticleDto(article); + } + + @Override + public void updateArticle(ArticleDto articleDto) { + String username = SecurityUtil.getSessionUser(); + UserEntity user = userRepository.findByUsername(username).orElse(null); + if (user == null) { + return; + } + Article article = mapToArticle(articleDto); + article.setCreatedBy(user); + articleRepository.save(article); + } + + @Override + public boolean delete(Long articleId) { + final UserEntity user = userService.getLoggedInUser().orElse(null); + if (user == null) { + return false; + } + String userId = user.getUsername(); + ArticleDto article = this.findArticleById(articleId); + String ownerId = article.getUsername(); + if (ownerId.equals(userId)) { + articleRepository.deleteById(articleId); + return true; + } else { + return false; + } + } + + @Override + public List searchArticles(String search) { + List
articles = articleRepository.searchArticles(search); + return articles.stream().map(article -> mapToArticleDto(article)).collect(Collectors.toList()); + } +} diff --git a/backend/src/main/java/com/blog/web/services/impl/UserServiceImpl.java b/backend/src/main/java/com/blog/web/services/impl/UserServiceImpl.java new file mode 100644 index 0000000..859e72c --- /dev/null +++ b/backend/src/main/java/com/blog/web/services/impl/UserServiceImpl.java @@ -0,0 +1,60 @@ +package com.blog.web.services.impl; + +import com.blog.web.dto.RegistrationDto; +import com.blog.web.models.Role; +import com.blog.web.models.UserEntity; +import com.blog.web.repository.RoleRepository; +import com.blog.web.repository.UserRepository; +import com.blog.web.security.SecurityUtil; +import com.blog.web.services.UserService; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.Optional; + +@Service +public class UserServiceImpl implements UserService { + private final UserRepository userRepository; + private final RoleRepository roleRepository; + private final PasswordEncoder passwordEncoder; + + public UserServiceImpl(UserRepository userRepository, RoleRepository roleRepository, PasswordEncoder passwordEncoder) { + this.userRepository = userRepository; + this.roleRepository = roleRepository; + this.passwordEncoder = passwordEncoder; + } + + @Override + public void saveUser(RegistrationDto registrationDto) { + UserEntity user = new UserEntity(); + user.setUsername(registrationDto.getUsername()); + user.setEmail(registrationDto.getEmail()); + user.setPassword(passwordEncoder.encode(registrationDto.getPassword())); + + final Role role = roleRepository.findByName("User").orElse(new Role()); + user.setRoles(Arrays.asList(role)); + userRepository.save(user); + } + + @Override + public Optional findByEmail(String email) { + return userRepository.findByEmail(email); + } + + @Override + public Optional findByUsername(String username) { + return userRepository.findByUsername(username); + } + + public Optional getLoggedInUser() { + final Optional user; + String username = SecurityUtil.getSessionUser(); + if (username != null) { + user = this.findByUsername(username); + } else { + user = Optional.of(new UserEntity()); + } + return user; + } +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties new file mode 100644 index 0000000..102e2c2 --- /dev/null +++ b/backend/src/main/resources/application.properties @@ -0,0 +1,10 @@ +spring.application.name=web +spring.datasource.url=jdbc:postgresql://localhost:5432/spring +spring.datasource.username=tradam +spring.datasource.driver-class-name=org.postgresql.Driver + +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true + +spring.security.user.name=zxcv +spring.security.user.password=zxcv diff --git a/backend/src/main/resources/static/abstract-polygonal-banner-background-vector.jpg b/backend/src/main/resources/static/abstract-polygonal-banner-background-vector.jpg new file mode 100644 index 0000000..1c6746e Binary files /dev/null and b/backend/src/main/resources/static/abstract-polygonal-banner-background-vector.jpg differ diff --git a/backend/src/main/resources/templates/articles-list.html b/backend/src/main/resources/templates/articles-list.html new file mode 100644 index 0000000..799c751 --- /dev/null +++ b/backend/src/main/resources/templates/articles-list.html @@ -0,0 +1,16 @@ + + + + + Title + + +

blah blah

+
+
my aga article
+

+ ... +

+
+ + \ No newline at end of file diff --git a/backend/src/main/resources/templates/articles/edit.html b/backend/src/main/resources/templates/articles/edit.html new file mode 100644 index 0000000..f4faee9 --- /dev/null +++ b/backend/src/main/resources/templates/articles/edit.html @@ -0,0 +1,63 @@ + + + + +
+
+ +
+
+ + +

Please fill out this field.

+
+
+ + +

Please fill out this field.

+
+
+
+
+ + +

Please fill out this field.

+
+
+
+
+ + +
+
+ + + \ No newline at end of file diff --git a/backend/src/main/resources/templates/articles/new.html b/backend/src/main/resources/templates/articles/new.html new file mode 100644 index 0000000..f5924d6 --- /dev/null +++ b/backend/src/main/resources/templates/articles/new.html @@ -0,0 +1,62 @@ + + + + +
+
+
+
+ + +

Please fill out this field.

+
+
+ + +

Please fill out this field.

+
+
+
+
+ + +

Please fill out this field.

+
+
+
+
+ + +
+
+ + + \ No newline at end of file diff --git a/backend/src/main/resources/templates/articles/show.html b/backend/src/main/resources/templates/articles/show.html new file mode 100644 index 0000000..206aaae --- /dev/null +++ b/backend/src/main/resources/templates/articles/show.html @@ -0,0 +1,87 @@ + + + + + + + +
+

+

Welcome to Ghostwind CSS

+
+ + +
+ + +
+ +
+ +
+ + + +
+ + + +
+ + + +
+

Subscribe to Ghostwind CSS

+

Get the latest posts delivered right to your inbox

+
+
+
+ + +
+
+
+
+ + + + +
+ Avatar of Author +
+

Ghostwind CSS

+

Tailwind CSS version of Ghost's Casper theme by TailwindToolbox.com

+
+
+ +
+ + +
+ + +
+ +
+ + + + + + + + \ No newline at end of file diff --git a/backend/src/main/resources/templates/auth/login.html b/backend/src/main/resources/templates/auth/login.html new file mode 100644 index 0000000..47bc63e --- /dev/null +++ b/backend/src/main/resources/templates/auth/login.html @@ -0,0 +1,48 @@ + + + + +
Invalid Username/Password
+
You have been logged out
+ +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + + \ No newline at end of file diff --git a/backend/src/main/resources/templates/auth/register.html b/backend/src/main/resources/templates/auth/register.html new file mode 100644 index 0000000..dc57ca4 --- /dev/null +++ b/backend/src/main/resources/templates/auth/register.html @@ -0,0 +1,63 @@ + + + + +
+
Username or Email already exists
+
+
+
+ + +

Please fill out this field.

+
+
+ + +

Please fill out this field.

+
+
+
+
+ + +

Please fill out this field.

+
+
+
+
+ + +
+
+ + + \ No newline at end of file diff --git a/backend/src/main/resources/templates/index.html b/backend/src/main/resources/templates/index.html new file mode 100644 index 0000000..0c42c21 --- /dev/null +++ b/backend/src/main/resources/templates/index.html @@ -0,0 +1,146 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/main/resources/templates/layout.html b/backend/src/main/resources/templates/layout.html new file mode 100644 index 0000000..f3e0278 --- /dev/null +++ b/backend/src/main/resources/templates/layout.html @@ -0,0 +1,149 @@ + + + + + + Title Layout + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +

+ โ˜• Spring! +

+

Welcome to my Blog

+
+
+ + + +
+ +
+
+ +
+
+ + + +
+
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+ + + +
+
+ + + + + + \ No newline at end of file diff --git a/backend/src/main/resources/templates/post.html b/backend/src/main/resources/templates/post.html new file mode 100644 index 0000000..a775ff2 --- /dev/null +++ b/backend/src/main/resources/templates/post.html @@ -0,0 +1,266 @@ + + + + + + + Tailwind Starter Template - Ghostwind CSS : Tailwind Toolbox + + + + + + + + + + + + + + +
+

04 JULY 2023 / GETTING STARTED

+

Welcome to Ghostwind CSS

+
+ + +
+ + +
+ +
+ +
+ + + + + +

+ ๐Ÿ‘‹ Welcome fellow Tailwind CSS and Ghost fan. This starter template is an attempt to replicate the default Ghost theme "Casper" using Tailwind CSS and vanilla Javascript. +

+ +

The basic blog page layout is available and all using the default Tailwind CSS classes (although there are a few hardcoded style tags). If you are going to use this in your project, you will want to convert the classes into components.

+ +

Sed dignissim lectus ut tincidunt vulputate. Fusce tincidunt lacus purus, in mattis tortor sollicitudin pretium. Phasellus at diam posuere, scelerisque nisl sit amet, tincidunt urna. Cras nisi diam, pulvinar ut molestie eget, eleifend ac magna. Sed at lorem condimentum, dignissim lorem eu, blandit massa. Phasellus eleifend turpis vel erat bibendum scelerisque. Maecenas id risus dictum, rhoncus odio vitae, maximus purus. Etiam efficitur dolor in dolor molestie ornare. Aenean pulvinar diam nec neque tincidunt, vitae molestie quam fermentum. Donec ac pretium diam. Suspendisse sed odio risus. Nunc nec luctus nisi. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Duis nec nulla eget sem dictum elementum.

+ +
    +
  1. Maecenas accumsan lacus sit amet elementum porta. Aliquam eu libero lectus. Fusce vehicula dictum mi. In non dolor at sem ullamcorper venenatis ut sed dui. Ut ut est quam. Suspendisse quam quam, commodo sit amet placerat in, interdum a ipsum. Morbi sit amet tellus scelerisque tortor semper posuere.
  2. +
  3. Morbi varius posuere blandit. Praesent gravida bibendum neque eget commodo. Duis auctor ornare mauris, eu accumsan odio viverra in. Proin sagittis maximus pharetra. Nullam lorem mauris, faucibus ut odio tempus, ultrices aliquet ex. Nam id quam eget ipsum luctus hendrerit. Ut eros magna, eleifend ac ornare vulputate, pretium nec felis.
  4. +
  5. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nunc vitae pretium elit. Cras leo mauris, tristique in risus ac, tristique rutrum velit. Mauris accumsan tempor felis vitae gravida. Cras egestas convallis malesuada. Etiam ac ante id tortor vulputate pretium. Maecenas vel sapien suscipit, elementum odio et, consequat tellus.
  6. +
+ +
Example of blockquote - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam at ipsum eu nunc commodo posuere et sit amet ligula.
+ +

Example code block:

+
+					
+<header class="site-header outer">
+  <div class="inner">
+    {{> "site-nav"}}
+  </div>
+</header>
+					
+				
+ + + + +
+ + + +
+

Subscribe to Ghostwind CSS

+

Get the latest posts delivered right to your inbox

+
+
+
+ + +
+
+
+
+ + + + +
+ Avatar of Author +
+

Ghostwind CSS

+

Tailwind CSS version of Ghost's Casper theme by TailwindToolbox.com

+
+
+ +
+ + +
+ + +
+ +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/test/java/com/blog/web/WebApplicationTests.java b/backend/src/test/java/com/blog/web/WebApplicationTests.java new file mode 100644 index 0000000..c3a5ef9 --- /dev/null +++ b/backend/src/test/java/com/blog/web/WebApplicationTests.java @@ -0,0 +1,13 @@ +package com.blog.web; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class WebApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/backend/src/test/java/com/blog/web/repository/ArticleRepositoryTests.java b/backend/src/test/java/com/blog/web/repository/ArticleRepositoryTests.java new file mode 100644 index 0000000..96c5f5b --- /dev/null +++ b/backend/src/test/java/com/blog/web/repository/ArticleRepositoryTests.java @@ -0,0 +1,81 @@ + +package com.blog.web.repository; + +import com.blog.web.dto.ArticleDto; +import com.blog.web.dto.RegistrationDto; +import com.blog.web.models.Article; +import com.blog.web.models.UserEntity; +import com.blog.web.services.ArticleService; +import com.blog.web.services.UserService; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.context.support.WithMockUser; + +import java.time.LocalDateTime; +import java.util.List; + +@SpringBootTest +@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2) +public class ArticleRepositoryTests { + + @Autowired + private ArticleRepository articleRepository; + @Autowired + private UserService userService; + @Autowired + private ArticleService articleService; + + + @Test + @WithMockUser(username="test", password="blah") + public void ArticleRepository_Search_ReturnSearch() { + // Arrange + RegistrationDto userDto = new RegistrationDto( + "test", + "test@example.com", + "blah" + ); + userService.saveUser(userDto); + UserEntity user = userService.findByUsername("test").orElseThrow(); + final ArticleDto articleDto1 = new ArticleDto( + 1, + "Title", + "https://duckduckgo.com", + "Content", + user, + LocalDateTime.now(), + LocalDateTime.now() + ); + articleService.saveArticle(articleDto1); + final ArticleDto articleDto2 = new ArticleDto( + 2, + "itleblah", + "https://duckduckgo.com", + "Content", + user, + LocalDateTime.now(), + LocalDateTime.now() + ); + articleService.saveArticle(articleDto2); + final ArticleDto articleDto3 = new ArticleDto( + 3, + "dontfindme", + "https://duckduckgo.com", + "Content", + user, + LocalDateTime.now(), + LocalDateTime.now() + ); + articleService.saveArticle(articleDto3); + + // Act + List
list = articleRepository.searchArticles("itle"); // partial string should find 2 articles + + // Assert + Assertions.assertEquals(2, list.size()); + } +} diff --git a/backend/src/test/java/com/blog/web/services/ArticleServiceTests.java b/backend/src/test/java/com/blog/web/services/ArticleServiceTests.java new file mode 100644 index 0000000..d55516a --- /dev/null +++ b/backend/src/test/java/com/blog/web/services/ArticleServiceTests.java @@ -0,0 +1,78 @@ +package com.blog.web.services; + +import com.blog.web.dto.ArticleDto; +import com.blog.web.models.Article; +import com.blog.web.models.UserEntity; +import com.blog.web.repository.ArticleRepository; +import com.blog.web.repository.UserRepository; +import com.blog.web.security.SecurityUtil; +import com.blog.web.services.impl.ArticleServiceImpl; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.context.support.WithMockUser; + +import java.time.LocalDateTime; +import java.util.Optional; + +import static org.mockito.AdditionalAnswers.returnsFirstArg; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +//@SpringBootTest +@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2) +@ExtendWith(MockitoExtension.class) +public class ArticleServiceTests { + + @Mock + private ArticleRepository articleRepository; + @Mock + private UserRepository userRepository; + @InjectMocks + private ArticleServiceImpl articleService; + + @Test + @WithMockUser(username="test", password="blah") + public void ArticleService_SaveArticle_ReturnsArticle() { + //RegistrationDto userDto = new RegistrationDto( + // "test", + // "test@example.com", + // "blah" + //); + //userService.saveUser(userDto); + UserEntity user = new UserEntity(); + user.setUsername("test"); + user.setEmail("test@example.com"); + user.setPassword("blah"); + //UserEntity user = userService.findByUsername("test"); + final ArticleDto articleDto = new ArticleDto( + 1, + "Title", + "https://duckduckgo.com", + "Content", + user, + LocalDateTime.now(), + LocalDateTime.now() + ); + + when(userRepository.findByUsername(anyString())).thenReturn(Optional.of(user)); + when(articleRepository.save(any(Article.class))).then(returnsFirstArg()); + //when(SecurityUtil.getSessionUser()).thenReturn(user.getUsername()); + try(MockedStatic utilities = Mockito.mockStatic(SecurityUtil.class)) { + utilities.when(SecurityUtil::getSessionUser).thenReturn(user.getUsername()); + Assertions.assertEquals(user.getUsername(), SecurityUtil.getSessionUser()); + Article article = articleService.saveArticle(articleDto).orElse(null); + + Assertions.assertNotNull(article); + } + } +} diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs new file mode 100644 index 0000000..d6c9537 --- /dev/null +++ b/frontend/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..e1cdc89 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,30 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default { + // other rules... + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: __dirname, + }, +} +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..e4b78ea --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..20c5f1d --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2739 @@ +{ + "name": "spring-blog", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "spring-blog", + "version": "0.0.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^7.15.0", + "@typescript-eslint/parser": "^7.15.0", + "@vitejs/plugin-react-swc": "^3.5.0", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", + "typescript": "^5.2.2", + "vite": "^5.3.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.0.tgz", + "integrity": "sha512-JlPfZ/C7yn5S5p0yKk7uhHTTnFlvTgLetl2VxqE518QgyM7C9bSfFTYvB/Q/ftkq0RIPY4ySxTz+/wKJ/dXC0w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.19.0.tgz", + "integrity": "sha512-RDxUSY8D1tWYfn00DDi5myxKgOk6RvWPxhmWexcICt/MEC6yEMr4HNCu1sXXYLw8iAsg0D44NuU+qNq7zVWCrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.0.tgz", + "integrity": "sha512-emvKHL4B15x6nlNTBMtIaC9tLPRpeA5jMvRLXVbl/W9Ie7HhkrE7KQjvgS9uxgatL1HmHWDXk5TTS4IaNJxbAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.0.tgz", + "integrity": "sha512-fO28cWA1dC57qCd+D0rfLC4VPbh6EOJXrreBmFLWPGI9dpMlER2YwSPZzSGfq11XgcEpPukPTfEVFtw2q2nYJg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.19.0.tgz", + "integrity": "sha512-2Rn36Ubxdv32NUcfm0wB1tgKqkQuft00PtM23VqLuCUR4N5jcNWDoV5iBC9jeGdgS38WK66ElncprqgMUOyomw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.19.0.tgz", + "integrity": "sha512-gJuzIVdq/X1ZA2bHeCGCISe0VWqCoNT8BvkQ+BfsixXwTOndhtLUpOg0A1Fcx/+eA6ei6rMBzlOz4JzmiDw7JQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.19.0.tgz", + "integrity": "sha512-0EkX2HYPkSADo9cfeGFoQ7R0/wTKb7q6DdwI4Yn/ULFE1wuRRCHybxpl2goQrx4c/yzK3I8OlgtBu4xvted0ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.19.0.tgz", + "integrity": "sha512-GlIQRj9px52ISomIOEUq/IojLZqzkvRpdP3cLgIE1wUWaiU5Takwlzpz002q0Nxxr1y2ZgxC2obWxjr13lvxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.19.0.tgz", + "integrity": "sha512-N6cFJzssruDLUOKfEKeovCKiHcdwVYOT1Hs6dovDQ61+Y9n3Ek4zXvtghPPelt6U0AH4aDGnDLb83uiJMkWYzQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.19.0.tgz", + "integrity": "sha512-2DnD3mkS2uuam/alF+I7M84koGwvn3ZVD7uG+LEWpyzo/bq8+kKnus2EVCkcvh6PlNB8QPNFOz6fWd5N8o1CYg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.19.0.tgz", + "integrity": "sha512-D6pkaF7OpE7lzlTOFCB2m3Ngzu2ykw40Nka9WmKGUOTS3xcIieHe82slQlNq69sVB04ch73thKYIWz/Ian8DUA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.0.tgz", + "integrity": "sha512-HBndjQLP8OsdJNSxpNIN0einbDmRFg9+UQeZV1eiYupIRuZsDEoeGU43NQsS34Pp166DtwQOnpcbV/zQxM+rWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.19.0.tgz", + "integrity": "sha512-HxfbvfCKJe/RMYJJn0a12eiOI9OOtAUF4G6ozrFUK95BNyoJaSiBjIOHjZskTUffUrB84IPKkFG9H9nEvJGW6A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.19.0.tgz", + "integrity": "sha512-HxDMKIhmcguGTiP5TsLNolwBUK3nGGUEoV/BO9ldUBoMLBssvh4J0X8pf11i1fTV7WShWItB1bKAKjX4RQeYmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.19.0.tgz", + "integrity": "sha512-xItlIAZZaiG/u0wooGzRsx11rokP4qyc/79LkAOdznGRAbOFc+SfEdfUOszG1odsHNgwippUJavag/+W/Etc6Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.0.tgz", + "integrity": "sha512-xNo5fV5ycvCCKqiZcpB65VMR11NJB+StnxHz20jdqRAktfdfzhgjTiJ2doTDQE/7dqGaV5I7ZGqKpgph6lCIag==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/core": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.0.tgz", + "integrity": "sha512-d4vMzH6ICllDwlPuhset2h8gu/USHdbyfJim+2hQEdxC0UONtfpmu38XBgNqRjStrji1Q5M10jfeUZL3cu1i8g==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.9" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.7.0", + "@swc/core-darwin-x64": "1.7.0", + "@swc/core-linux-arm-gnueabihf": "1.7.0", + "@swc/core-linux-arm64-gnu": "1.7.0", + "@swc/core-linux-arm64-musl": "1.7.0", + "@swc/core-linux-x64-gnu": "1.7.0", + "@swc/core-linux-x64-musl": "1.7.0", + "@swc/core-win32-arm64-msvc": "1.7.0", + "@swc/core-win32-ia32-msvc": "1.7.0", + "@swc/core-win32-x64-msvc": "1.7.0" + }, + "peerDependencies": { + "@swc/helpers": "*" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.0.tgz", + "integrity": "sha512-2ylhM7f0HwUwLrFYZAe/dse8PCbPsYcJS3Dt7Q8NT3PUn7vy6QOMxNcOPPuDrnmaXqQQO3oxdmRapguTxaat9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.0.tgz", + "integrity": "sha512-SgVnN4gT1Rb9YfTkp4FCUITqSs7Yj0uB2SUciu5CV3HuGvS5YXCUzh+KrwpLFtx8NIgivISKcNnb41mJi98X8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.0.tgz", + "integrity": "sha512-+Z9Dayart1iKJQEJJ9N/KS4z5EdXJE3WPFikY0jonKTo4Dd8RuyVz5yLvqcIMeVdz/SwximATaL6iJXw7hZS9A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.0.tgz", + "integrity": "sha512-UnLrCiZ1EI4shznJn0xP6DLgsXUSwtfsdgHhGYCrvbgVBBve3S9iFgVFEB3SPl7Q/TdowNbrN4zHU0oChfiNfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.0.tgz", + "integrity": "sha512-H724UANA+ptsfwKRr9mnaDa9cb5fw0oFysiGKTgb3DMYcgk3Od0jMTnXVPFSVpo7FlmyxeC9K8ueUPBOoOK6XA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.0.tgz", + "integrity": "sha512-SY3HA0K0Dpqt1HIfMLGpwL4hd4UaL2xHP5oZXPlRQPhUDZrbb4PbI3ZJnh66c63eL4ZR8EJ+HRFI0Alx5p69Zw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.0.tgz", + "integrity": "sha512-cEJ2ebtV1v/5Ilb55E05J6F5SrHKQWzUttIhR5Mkayyo+yvPslcpByuFC3D+J7X1ebziTOBpWuMpUdjLfh3SMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.0.tgz", + "integrity": "sha512-ecQOOmzEssz+m0pR4xDYCGuvn3E/l0nQ3tk5jp1NA1lsAy4bMV0YbYCHjptYvWL/UjhIerIp3IlCJ8x5DodSog==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.0.tgz", + "integrity": "sha512-gz81seZkRn3zMnVOc7L5k6F4vQC82gIxmHiL+GedK+A37XI/X26AASU3zxvORnqQbwQYXQ+AEVckxBmFlz3v2g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.0.tgz", + "integrity": "sha512-b5Fd1xEOw9uqBpj2lqsaR4Iq9UhiL84hNDcEsi6DQA7Y1l85waQAslTbS0E4/pJ1PISAs0jW0zIGLco1eaWBOg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true + }, + "node_modules/@swc/types": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.12.tgz", + "integrity": "sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==", + "dev": true, + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.17.0.tgz", + "integrity": "sha512-pyiDhEuLM3PuANxH7uNYan1AaFs5XE0zw1hq69JBvGvE7gSuEoQl1ydtEe/XQeoC3GQxLXyOVa5kNOATgM638A==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/type-utils": "7.17.0", + "@typescript-eslint/utils": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.17.0.tgz", + "integrity": "sha512-puiYfGeg5Ydop8eusb/Hy1k7QmOU6X3nvsqCgzrB2K4qMavK//21+PzNE8qeECgNOIoertJPUC1SpegHDI515A==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/typescript-estree": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.17.0.tgz", + "integrity": "sha512-0P2jTTqyxWp9HiKLu/Vemr2Rg1Xb5B7uHItdVZ6iAenXmPo4SZ86yOPCJwMqpCyaMiEHTNqizHfsbmCFT1x9SA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.17.0.tgz", + "integrity": "sha512-XD3aaBt+orgkM/7Cei0XNEm1vwUxQ958AOLALzPlbPqb8C1G8PZK85tND7Jpe69Wualri81PLU+Zc48GVKIMMA==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.17.0", + "@typescript-eslint/utils": "7.17.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.17.0.tgz", + "integrity": "sha512-a29Ir0EbyKTKHnZWbNsrc/gqfIBqYPwj3F2M+jWE/9bqfEHg0AMtXzkbUkOG6QgEScxh2+Pz9OXe11jHDnHR7A==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.17.0.tgz", + "integrity": "sha512-72I3TGq93t2GoSBWI093wmKo0n6/b7O4j9o8U+f65TVD0FS6bI2180X5eGEr8MA8PhKMvYe9myZJquUT2JkCZw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.17.0.tgz", + "integrity": "sha512-r+JFlm5NdB+JXc7aWWZ3fKSm1gn0pkswEwIYsrGPdsT2GjsRATAKXiNtp3vgAAO1xZhX8alIOEQnNMl3kbTgJw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/typescript-estree": "7.17.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.17.0.tgz", + "integrity": "sha512-RVGC9UhPOCsfCdI9pU++K4nD7to+jTcMIbXTSOcrLqUEW6gF2pU1UUbYJKc9cvcRSK1UDeMJ7pdMxf4bhMpV/A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.17.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.0.tgz", + "integrity": "sha512-yrknSb3Dci6svCd/qhHqhFPDSw0QtjumcqdKMoNNzmOl5lMXTTiqzjWtG4Qask2HdvvzaNgSunbQGet8/GrKdA==", + "dev": true, + "dependencies": { + "@swc/core": "^1.5.7" + }, + "peerDependencies": { + "vite": "^4 || ^5" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.9.tgz", + "integrity": "sha512-QK49YrBAo5CLNLseZ7sZgvgTy21E6NEw22eZqc4teZfH8pxV3yXc9XXOYfUI6JNpw7mfHNkAeWtBxrTyykB6HA==", + "dev": true, + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", + "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.19.0.tgz", + "integrity": "sha512-5r7EYSQIowHsK4eTZ0Y81qpZuJz+MUuYeqmmYmRMl1nwhdmbiYqt5jwzf6u7wyOzJgYqtCRMtVRKOtHANBz7rA==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.19.0", + "@rollup/rollup-android-arm64": "4.19.0", + "@rollup/rollup-darwin-arm64": "4.19.0", + "@rollup/rollup-darwin-x64": "4.19.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.19.0", + "@rollup/rollup-linux-arm-musleabihf": "4.19.0", + "@rollup/rollup-linux-arm64-gnu": "4.19.0", + "@rollup/rollup-linux-arm64-musl": "4.19.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.19.0", + "@rollup/rollup-linux-riscv64-gnu": "4.19.0", + "@rollup/rollup-linux-s390x-gnu": "4.19.0", + "@rollup/rollup-linux-x64-gnu": "4.19.0", + "@rollup/rollup-linux-x64-musl": "4.19.0", + "@rollup/rollup-win32-arm64-msvc": "4.19.0", + "@rollup/rollup-win32-ia32-msvc": "4.19.0", + "@rollup/rollup-win32-x64-msvc": "4.19.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.4.tgz", + "integrity": "sha512-Cw+7zL3ZG9/NZBB8C+8QbQZmR54GwqIz+WMI4b3JgdYJvX+ny9AjJXqkGQlDXSXRP9rP0B4tbciRMOVEKulVOA==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.39", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..f2c55dd --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,28 @@ +{ + "name": "spring-blog", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^7.15.0", + "@typescript-eslint/parser": "^7.15.0", + "@vitejs/plugin-react-swc": "^3.5.0", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", + "typescript": "^5.2.2", + "vite": "^5.3.4" + } +} diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..afe48ac --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,35 @@ +import { useState } from 'react' +import reactLogo from './assets/react.svg' +import viteLogo from '/vite.svg' +import './App.css' + +function App() { + const [count, setCount] = useState(0) + + return ( + <> + +

Vite + React

+
+ +

+ Edit src/App.tsx and save to test HMR +

+
+

+ Click on the Vite and React logos to learn more +

+ + ) +} + +export default App diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..6119ad9 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..3d7150d --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.tsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..d739292 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..ea9d0cd --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..3afdd6e --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "noEmit": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..861b04b --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react-swc' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/mvnw b/mvnw deleted file mode 100755 index d7c358e..0000000 --- a/mvnw +++ /dev/null @@ -1,259 +0,0 @@ -#!/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.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-,maven-mvnd--}/ -[ -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 deleted file mode 100644 index 6f779cf..0000000 --- a/mvnw.cmd +++ /dev/null @@ -1,149 +0,0 @@ -<# : 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 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.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-,maven-mvnd--}/ -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 deleted file mode 100644 index db9c68f..0000000 --- a/pom.xml +++ /dev/null @@ -1,112 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.3.1 - - - com.blog - web - 0.0.1-SNAPSHOT - web - Demo project for Spring Boot - - - - - - - - - - - - - - - 17 - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-thymeleaf - - - nz.net.ultraq.thymeleaf - thymeleaf-layout-dialect - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-devtools - runtime - true - - - org.postgresql - postgresql - runtime - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.boot - spring-boot-starter-validation - - - org.springframework.boot - spring-boot-starter-security - - - org.thymeleaf.extras - thymeleaf-extras-springsecurity6 - - - org.apache.commons - commons-lang3 - - - com.h2database - h2 - 2.3.230 - test - - - junit - junit - test - - - org.springframework.security - spring-security-test - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - - - - \ No newline at end of file diff --git a/src/main/java/com/blog/web/WebApplication.java b/src/main/java/com/blog/web/WebApplication.java deleted file mode 100644 index f5dd2ef..0000000 --- a/src/main/java/com/blog/web/WebApplication.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.blog.web; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class WebApplication { - public static void main(String[] args) { - SpringApplication.run(WebApplication.class, args); - } -} diff --git a/src/main/java/com/blog/web/controllers/ArticleController.java b/src/main/java/com/blog/web/controllers/ArticleController.java deleted file mode 100644 index 6cd5d50..0000000 --- a/src/main/java/com/blog/web/controllers/ArticleController.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.blog.web.controllers; - -import com.blog.web.dto.ArticleDto; -import com.blog.web.dto.ArticlePublicDto; -import com.blog.web.models.Article; -import com.blog.web.models.UserEntity; -import com.blog.web.services.ArticleService; -import com.blog.web.services.UserService; -import jakarta.validation.Valid; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.validation.BindingResult; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.HashSet; -import java.util.List; - -@RestController -@Controller -public class ArticleController { - private ArticleService articleService; - private UserService userService; - - public ArticleController(ArticleService articleService, UserService userService) { - this.articleService = articleService; - this.userService = userService; - } - - @GetMapping("/get") - public Article getMethod() { - return new Article( - 5, - "blah", - "blah", - "blah", - new UserEntity(), - LocalDateTime.now(), - LocalDateTime.now() - ); - } - - @GetMapping("/articles") - public HashSet listArticles(Model model) { - HashSet articles = new HashSet(articleService.findAllArticles()); - //UserEntity user = userService.getLoggedInUser().orElse(new UserEntity()); - //model.addAttribute("user", user); - //model.addAttribute("articles", articles); - //return "index"; - return articles; - } - - @GetMapping("/articles/{articleId}") - public String showArticle(@PathVariable("articleId") long articleId, Model model) { - ArticleDto articleDto = articleService.findArticleById(articleId); - model.addAttribute("article", articleDto); - UserEntity user = userService.getLoggedInUser().orElse(new UserEntity()); - model.addAttribute("user", user); - return "articles/show"; - } - - @GetMapping("/articles/new") - public String createArticleForm(Model model) { - model.addAttribute("user", userService.getLoggedInUser().orElse(new UserEntity())); - model.addAttribute("article", new Article()); - return "articles/new"; - } - - @PostMapping("/articles/new") - public String saveArticle(@Valid @ModelAttribute("article") ArticleDto articleDto, BindingResult result, Model model) { - // if non-authenticated in user tries to create an article - // redirect them to login page - UserEntity user = userService.getLoggedInUser().orElse(null); - if (user == null) { - return "redirect:/userlogin"; - } else if (result.hasErrors()) { - model.addAttribute("article", articleDto); - return "articles/new"; - } else { - articleService.saveArticle(articleDto); - return "redirect:/articles"; - } - } - - @GetMapping("/articles/delete/{articleId}") - public String deleteArticle(@PathVariable("articleId") Long articleId) { - articleService.delete(articleId); - return "redirect:/articles"; - } - - @GetMapping("/articles/edit/{articleId}") - public String editArticleForm(@PathVariable("articleId") long articleId, Model model) { - UserEntity user = userService.getLoggedInUser().orElse(null); - if (user != null) { - model.addAttribute("user", user); - ArticleDto articleDto = articleService.findArticleById(articleId); - model.addAttribute("article", articleDto); - } - return "articles/edit"; - } - - @PostMapping("/articles/edit/{articleId}") - public String updateArticle(@PathVariable("articleId") Long articleId, @Valid @ModelAttribute("article") ArticleDto article, BindingResult result) { - if (result.hasErrors()) { - return "articles/edit"; - } - article.setId(articleId); - articleService.updateArticle(article); - return "redirect:/articles"; - } - - @GetMapping("/articles/search") - public String searchArticle(@RequestParam(value = "search") String search, Model model) { - UserEntity user = userService.getLoggedInUser().orElse(new UserEntity()); - model.addAttribute("user", user); - List articles = articleService.searchArticles(search); - model.addAttribute("articles", articles); - return "index"; - } - - @GetMapping("/") - public String getArticles() { - return "redirect:/articles"; - } -} diff --git a/src/main/java/com/blog/web/controllers/AuthController.java b/src/main/java/com/blog/web/controllers/AuthController.java deleted file mode 100644 index efb3672..0000000 --- a/src/main/java/com/blog/web/controllers/AuthController.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.blog.web.controllers; - -import com.blog.web.dto.RegistrationDto; -import com.blog.web.models.UserEntity; -import com.blog.web.services.UserService; -import jakarta.validation.Valid; -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.validation.BindingResult; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; - -@Controller -public class AuthController { - private final UserService userService; - - public AuthController(UserService userService) { - this.userService = userService; - } - - @GetMapping("/userlogin") - public String login(Model model) { - final UserEntity user = userService.getLoggedInUser().orElse(new UserEntity()); - model.addAttribute("user", user); - return "auth/login"; - } - - @GetMapping("/register") - public String getRegisterForm(Model model) { - final RegistrationDto user = new RegistrationDto(); - model.addAttribute("user", user); - return "auth/register"; - } - - @PostMapping("/register/save") - public String register(@Valid @ModelAttribute("user") RegistrationDto user, BindingResult result, Model model) { - UserEntity existingUserEmail = userService.findByEmail(user.getEmail()).orElse(null); - if (existingUserEmail != null && StringUtils.isBlank(existingUserEmail.getEmail())) { - result.rejectValue("email", "There is already a user with this email"); - } - - UserEntity existingUsername = userService.findByUsername(user.getUsername()).orElse(null); - if (existingUsername != null && StringUtils.isBlank(existingUsername.getUsername())) { - result.rejectValue("username", "There is already a user with this username"); - } - - if (result.hasErrors()) { - model.addAttribute("user", user); - return "register"; - } - userService.saveUser(user); - return "redirect:/articles?success"; - } -} diff --git a/src/main/java/com/blog/web/dto/ArticleDto.java b/src/main/java/com/blog/web/dto/ArticleDto.java deleted file mode 100644 index d275f3b..0000000 --- a/src/main/java/com/blog/web/dto/ArticleDto.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.blog.web.dto; - -import com.blog.web.models.Article; -import com.blog.web.models.UserEntity; -import jakarta.persistence.*; -import jakarta.validation.constraints.NotEmpty; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; -import org.hibernate.validator.constraints.URL; - -import java.time.LocalDateTime; - -public class ArticleDto { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - @NotEmpty(message = "Article title should not be empty") - private String title; - @NotEmpty(message = "Article Photo URL should not be empty") - @URL(message = "Article Photo URL should be a URL") - private String photoUrl; - @NotEmpty(message = "Article Content should not be empty") - private String content; - @CreationTimestamp - private LocalDateTime createdOn; - @UpdateTimestamp - private LocalDateTime updatedOn; - @ManyToOne - @JoinColumn(name = "created_by", nullable = false) - private UserEntity createdBy; - - public ArticleDto(long id, String title, String photoUrl, String content, UserEntity createdBy, LocalDateTime createdOn, LocalDateTime updatedOn) { - this.id = id; - this.title = title; - this.photoUrl = photoUrl; - this.content = content; - this.createdBy = createdBy; - this.createdOn = createdOn; - this.updatedOn = updatedOn; - } - - public ArticleDto() { - } - - ; - - public ArticleDto(Article article) { - this.id = article.getId(); - this.title = article.getTitle(); - this.photoUrl = article.getPhotoUrl(); - this.content = article.getContent(); - this.createdBy = article.getCreatedBy(); - this.createdOn = article.getCreatedOn(); - this.updatedOn = article.getUpdatedOn(); - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public @NotEmpty(message = "Article title should not be empty") String getTitle() { - return title; - } - - public void setTitle(@NotEmpty(message = "Article title should not be empty") String title) { - this.title = title; - } - - public @NotEmpty(message = "Article Photo URL should not be empty") @URL(message = "Article Photo URL should be a URL") String getPhotoUrl() { - return photoUrl; - } - - public void setPhotoUrl(@NotEmpty(message = "Article Photo URL should not be empty") @URL(message = "Article Photo URL should be a URL") String photoUrl) { - this.photoUrl = photoUrl; - } - - public @NotEmpty(message = "Article Content should not be empty") String getContent() { - return content; - } - - public void setContent(@NotEmpty(message = "Article Content should not be empty") String content) { - this.content = content; - } - - public LocalDateTime getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(LocalDateTime createdOn) { - this.createdOn = createdOn; - } - - public LocalDateTime getUpdatedOn() { - return updatedOn; - } - - public void setUpdatedOn(LocalDateTime updatedOn) { - this.updatedOn = updatedOn; - } - - public UserEntity getCreatedBy() { - return createdBy; - } - - public void setCreatedBy(UserEntity createdBy) { - this.createdBy = createdBy; - } - - public String getUsername() { return createdBy.getUsername(); } -} diff --git a/src/main/java/com/blog/web/dto/ArticlePublicDto.java b/src/main/java/com/blog/web/dto/ArticlePublicDto.java deleted file mode 100644 index 3ced6d2..0000000 --- a/src/main/java/com/blog/web/dto/ArticlePublicDto.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.blog.web.dto; - -import com.blog.web.models.Article; - -import java.time.LocalDateTime; - -public class ArticlePublicDto { - private Long id; - private String title; - private String photoUrl; - private String content; - private LocalDateTime createdOn; - private LocalDateTime updatedOn; - private String createdBy; - - public ArticlePublicDto(long id, String title, String photoUrl, String content, String createdBy, LocalDateTime createdOn, LocalDateTime updatedOn) { - this.id = id; - this.title = title; - this.photoUrl = photoUrl; - this.content = content; - this.createdBy = createdBy; - this.createdOn = createdOn; - this.updatedOn = updatedOn; - } - - public ArticlePublicDto() { - } - - ; - - public ArticlePublicDto(Article article) { - this.id = article.getId(); - this.title = article.getTitle(); - this.photoUrl = article.getPhotoUrl(); - this.content = article.getContent(); - this.createdBy = article.getCreatedBy().getUsername(); - this.createdOn = article.getCreatedOn(); - this.updatedOn = article.getUpdatedOn(); - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle( String title) { - this.title = title; - } - - public String getPhotoUrl() { - return photoUrl; - } - - public void setPhotoUrl(String photoUrl) { - this.photoUrl = photoUrl; - } - - public String getContent() { - return content; - } - - public void setContent( String content) { - this.content = content; - } - - public LocalDateTime getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(LocalDateTime createdOn) { - this.createdOn = createdOn; - } - - public LocalDateTime getUpdatedOn() { - return updatedOn; - } - - public void setUpdatedOn(LocalDateTime updatedOn) { - this.updatedOn = updatedOn; - } - - public String getCreatedBy() { - return createdBy; - } - - public void setCreatedBy(String createdBy) { - this.createdBy = createdBy; - } -} diff --git a/src/main/java/com/blog/web/dto/RegistrationDto.java b/src/main/java/com/blog/web/dto/RegistrationDto.java deleted file mode 100644 index 5b86557..0000000 --- a/src/main/java/com/blog/web/dto/RegistrationDto.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.blog.web.dto; - -import jakarta.validation.constraints.NotEmpty; - -public class RegistrationDto { - private Long id; - @NotEmpty - private String username; - @NotEmpty - private String email; - - @NotEmpty - private String password; - - public RegistrationDto() { - } - - public RegistrationDto(String username, String email, String password) { - this.username = username; - this.email = email; - this.password = password; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public @NotEmpty String getUsername() { - return username; - } - - public void setUsername(@NotEmpty String username) { - this.username = username; - } - - public @NotEmpty String getEmail() { - return email; - } - - public void setEmail(@NotEmpty String email) { - this.email = email; - } - - public void setPassword(@NotEmpty String password) { - this.password = password; - } - - public @NotEmpty String getPassword() { - return password; - } -} diff --git a/src/main/java/com/blog/web/mappers/ArticleMapper.java b/src/main/java/com/blog/web/mappers/ArticleMapper.java deleted file mode 100644 index 8fe729e..0000000 --- a/src/main/java/com/blog/web/mappers/ArticleMapper.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.blog.web.mappers; - -import com.blog.web.dto.ArticleDto; -import com.blog.web.dto.ArticlePublicDto; -import com.blog.web.models.Article; - -public class ArticleMapper { - private ArticleMapper() { - } - - public static Article mapToArticle(ArticleDto articleDto) { - return new Article(articleDto); - } - - public static ArticleDto mapToArticleDto(Article article) { - return new ArticleDto(article); - } - - public static ArticlePublicDto mapToArticlePublicDto(Article article) { - return new ArticlePublicDto(article); - } -} diff --git a/src/main/java/com/blog/web/models/Article.java b/src/main/java/com/blog/web/models/Article.java deleted file mode 100644 index b54907a..0000000 --- a/src/main/java/com/blog/web/models/Article.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.blog.web.models; - -import com.blog.web.dto.ArticleDto; -import jakarta.persistence.*; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.LocalDateTime; - -@Entity -public class Article { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private long id; - private String title; - private String photoUrl; - private String content; - @CreationTimestamp - private LocalDateTime createdOn; - @UpdateTimestamp - private LocalDateTime updatedOn; - @ManyToOne - @JoinColumn(name = "created_by", nullable = false) - private UserEntity createdBy; - - public Article(long id, String title, String photoUrl, String content, UserEntity createdBy, LocalDateTime createdOn, LocalDateTime updatedOn) { - this.id = id; - this.title = title; - this.photoUrl = photoUrl; - this.content = content; - this.createdBy = createdBy; - this.createdOn = createdOn; - this.updatedOn = updatedOn; - } - - public Article() { - } - - public Article(ArticleDto articleDto) { - this.id = articleDto.getId(); - this.title = articleDto.getTitle(); - this.photoUrl = articleDto.getPhotoUrl(); - this.content = articleDto.getContent(); - this.createdBy = articleDto.getCreatedBy(); - this.createdOn = articleDto.getCreatedOn(); - this.updatedOn = articleDto.getUpdatedOn(); - } - - public long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getPhotoUrl() { - return photoUrl; - } - - public void setPhotoUrl(String photoUrl) { - this.photoUrl = photoUrl; - } - - public LocalDateTime getCreatedOn() { - return createdOn; - } - - public void setCreatedOn(LocalDateTime createdOn) { - this.createdOn = createdOn; - } - - public LocalDateTime getUpdatedOn() { - return updatedOn; - } - - public void setUpdatedOn(LocalDateTime updatedOn) { - this.updatedOn = updatedOn; - } - - public UserEntity getCreatedBy() { - return createdBy; - } - - public void setCreatedBy(UserEntity createdBy) { - this.createdBy = createdBy; - } - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } -} diff --git a/src/main/java/com/blog/web/models/Role.java b/src/main/java/com/blog/web/models/Role.java deleted file mode 100644 index 2b7143f..0000000 --- a/src/main/java/com/blog/web/models/Role.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.blog.web.models; - -import jakarta.persistence.*; - -import java.util.ArrayList; -import java.util.List; - -@Entity(name = "roles") -public class Role { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private long id; - private String name; - @ManyToMany(mappedBy = "roles") - private List users = new ArrayList<>(); - - public long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public List getUsers() { - return users; - } -} diff --git a/src/main/java/com/blog/web/models/UserEntity.java b/src/main/java/com/blog/web/models/UserEntity.java deleted file mode 100644 index bf45b21..0000000 --- a/src/main/java/com/blog/web/models/UserEntity.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.blog.web.models; - -import jakarta.persistence.*; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.User; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -@Entity(name = "users") -// Named UserEntity to prevent conflicts with Java User object -public class UserEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private long id; - private String username; - private String email; - private String password; - @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) - @JoinTable(name = "user_roles", joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")}) - private final List roles = new ArrayList<>(); - - public boolean equals(UserEntity user) { - return this.id == user.getId(); - } - - public User toSecurityUser() { - return new User(this.getEmail(), this.getPassword(), this.getRoles().stream().map((role) -> new SimpleGrantedAuthority(role.getName())).collect(Collectors.toList())); - } - - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public List getRoles() { - return roles; - } - - public void setRoles(List roles) { - this.roles.clear(); - this.roles.addAll(roles); - } -} diff --git a/src/main/java/com/blog/web/repository/ArticleRepository.java b/src/main/java/com/blog/web/repository/ArticleRepository.java deleted file mode 100644 index 594cb15..0000000 --- a/src/main/java/com/blog/web/repository/ArticleRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.blog.web.repository; - -import com.blog.web.models.Article; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; - -import java.util.List; - -public interface ArticleRepository extends JpaRepository { - @Query("SELECT a from Article a WHERE a.title LIKE CONCAT('%', :search, '%')") - List
searchArticles(String search); -} diff --git a/src/main/java/com/blog/web/repository/RoleRepository.java b/src/main/java/com/blog/web/repository/RoleRepository.java deleted file mode 100644 index f271bf1..0000000 --- a/src/main/java/com/blog/web/repository/RoleRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.blog.web.repository; - -import com.blog.web.models.Role; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface RoleRepository extends JpaRepository { - Optional findByName(String name); -} diff --git a/src/main/java/com/blog/web/repository/UserRepository.java b/src/main/java/com/blog/web/repository/UserRepository.java deleted file mode 100644 index 30eefc5..0000000 --- a/src/main/java/com/blog/web/repository/UserRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.blog.web.repository; - -import com.blog.web.models.UserEntity; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface UserRepository extends JpaRepository { - Optional findByEmail(String email); - - Optional findByUsername(String username); - - Optional findFirstByUsername(String username); -} diff --git a/src/main/java/com/blog/web/security/CustomUserDetailsService.java b/src/main/java/com/blog/web/security/CustomUserDetailsService.java deleted file mode 100644 index ee3e950..0000000 --- a/src/main/java/com/blog/web/security/CustomUserDetailsService.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.blog.web.security; - -import com.blog.web.models.UserEntity; -import com.blog.web.repository.UserRepository; -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 java.util.stream.Collectors; - -@Service -public class CustomUserDetailsService implements UserDetailsService { - private final UserRepository userRepository; - - public CustomUserDetailsService(UserRepository userRepository) { - this.userRepository = userRepository; - } - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - UserEntity userEntity = userRepository.findFirstByUsername(username).orElse(new UserEntity()); - if (userEntity.getUsername() != null) { - return userEntity.toSecurityUser(); - } else { - throw new UsernameNotFoundException("Invalid username"); - } - } -} diff --git a/src/main/java/com/blog/web/security/SecurityConfig.java b/src/main/java/com/blog/web/security/SecurityConfig.java deleted file mode 100644 index 17e09c7..0000000 --- a/src/main/java/com/blog/web/security/SecurityConfig.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.blog.web.security; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -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(securedEnabled = true) -public class SecurityConfig { - private CustomUserDetailsService userDetailsService; - - public SecurityConfig(CustomUserDetailsService userDetailsService) { - this.userDetailsService = userDetailsService; - } - - @Bean - public static PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - // disabling csrf leaves us vulnerable, in a real production app do not do this - http.csrf(c -> c.disable()).cors(c -> c.disable()).authorizeHttpRequests(auths -> auths.anyRequest().permitAll()).formLogin(form -> form.loginPage("/userlogin").usernameParameter("username").passwordParameter("password").defaultSuccessUrl("/articles").loginProcessingUrl("/userlogin").failureUrl("/userlogin?error=true").permitAll()).logout(logout -> logout.logoutUrl("/logout").logoutSuccessUrl("/articles")); - return http.build(); - } - - public void configure(AuthenticationManagerBuilder builder) throws Exception { - builder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); - } -} diff --git a/src/main/java/com/blog/web/security/SecurityUtil.java b/src/main/java/com/blog/web/security/SecurityUtil.java deleted file mode 100644 index ef0b3d9..0000000 --- a/src/main/java/com/blog/web/security/SecurityUtil.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.blog.web.security; - -import org.springframework.security.authentication.AnonymousAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; - -public class SecurityUtil { - public static String getSessionUser() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - return !(authentication instanceof AnonymousAuthenticationToken) ? authentication.getName() : null; - } -} diff --git a/src/main/java/com/blog/web/services/ArticleService.java b/src/main/java/com/blog/web/services/ArticleService.java deleted file mode 100644 index 1bfe38f..0000000 --- a/src/main/java/com/blog/web/services/ArticleService.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.blog.web.services; - -import com.blog.web.dto.ArticleDto; -import com.blog.web.dto.ArticlePublicDto; -import com.blog.web.models.Article; - -import java.util.List; -import java.util.Optional; - -public interface ArticleService { - List findAllArticles(); - - Optional
saveArticle(ArticleDto article); - - ArticleDto findArticleById(long articleId); - - void updateArticle(ArticleDto articleDto); - - boolean delete(Long articleId); - - List searchArticles(String search); -} diff --git a/src/main/java/com/blog/web/services/UserService.java b/src/main/java/com/blog/web/services/UserService.java deleted file mode 100644 index b9c231b..0000000 --- a/src/main/java/com/blog/web/services/UserService.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.blog.web.services; - -import com.blog.web.dto.RegistrationDto; -import com.blog.web.models.UserEntity; - -import java.util.Optional; - -public interface UserService { - void saveUser(RegistrationDto registrationDto); - - Optional findByEmail(String email); - - Optional findByUsername(String username); - - public Optional getLoggedInUser(); -} diff --git a/src/main/java/com/blog/web/services/impl/ArticleServiceImpl.java b/src/main/java/com/blog/web/services/impl/ArticleServiceImpl.java deleted file mode 100644 index 7073073..0000000 --- a/src/main/java/com/blog/web/services/impl/ArticleServiceImpl.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.blog.web.services.impl; - -import com.blog.web.dto.ArticleDto; -import com.blog.web.dto.ArticlePublicDto; -import com.blog.web.models.Article; -import com.blog.web.models.UserEntity; -import com.blog.web.repository.ArticleRepository; -import com.blog.web.repository.UserRepository; -import com.blog.web.security.SecurityUtil; -import com.blog.web.services.ArticleService; -import com.blog.web.services.UserService; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -import com.blog.web.mappers.ArticleMapper; - -import static com.blog.web.mappers.ArticleMapper.mapToArticle; -import static com.blog.web.mappers.ArticleMapper.mapToArticleDto; - -@Service -public class ArticleServiceImpl implements ArticleService { - final private ArticleRepository articleRepository; - final private UserRepository userRepository; - final private UserService userService; - - public ArticleServiceImpl(ArticleRepository articleRepository, UserRepository userRepository, UserService userService) { - this.userRepository = userRepository; - this.articleRepository = articleRepository; - this.userService = userService; - } - - - @Override - public List findAllArticles() { - List
articles = articleRepository.findAll(); - return articles.stream().map(ArticleMapper::mapToArticlePublicDto).collect(Collectors.toList()); - } - - @Override - public Optional
saveArticle(ArticleDto articleDto) { - String username = SecurityUtil.getSessionUser(); - UserEntity user = userRepository.findByUsername(username).orElse(null); - if (user == null) { - return null; - } - Article article = mapToArticle(articleDto); - article.setCreatedBy(user); - return Optional.of(articleRepository.save(article)); - } - - @Override - public ArticleDto findArticleById(long articleId) { - Article article = articleRepository.findById(articleId).get(); - return mapToArticleDto(article); - } - - @Override - public void updateArticle(ArticleDto articleDto) { - String username = SecurityUtil.getSessionUser(); - UserEntity user = userRepository.findByUsername(username).orElse(null); - if (user == null) { - return; - } - Article article = mapToArticle(articleDto); - article.setCreatedBy(user); - articleRepository.save(article); - } - - @Override - public boolean delete(Long articleId) { - final UserEntity user = userService.getLoggedInUser().orElse(null); - if (user == null) { - return false; - } - String userId = user.getUsername(); - ArticleDto article = this.findArticleById(articleId); - String ownerId = article.getUsername(); - if (ownerId.equals(userId)) { - articleRepository.deleteById(articleId); - return true; - } else { - return false; - } - } - - @Override - public List searchArticles(String search) { - List
articles = articleRepository.searchArticles(search); - return articles.stream().map(article -> mapToArticleDto(article)).collect(Collectors.toList()); - } -} diff --git a/src/main/java/com/blog/web/services/impl/UserServiceImpl.java b/src/main/java/com/blog/web/services/impl/UserServiceImpl.java deleted file mode 100644 index 859e72c..0000000 --- a/src/main/java/com/blog/web/services/impl/UserServiceImpl.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.blog.web.services.impl; - -import com.blog.web.dto.RegistrationDto; -import com.blog.web.models.Role; -import com.blog.web.models.UserEntity; -import com.blog.web.repository.RoleRepository; -import com.blog.web.repository.UserRepository; -import com.blog.web.security.SecurityUtil; -import com.blog.web.services.UserService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.Arrays; -import java.util.Optional; - -@Service -public class UserServiceImpl implements UserService { - private final UserRepository userRepository; - private final RoleRepository roleRepository; - private final PasswordEncoder passwordEncoder; - - public UserServiceImpl(UserRepository userRepository, RoleRepository roleRepository, PasswordEncoder passwordEncoder) { - this.userRepository = userRepository; - this.roleRepository = roleRepository; - this.passwordEncoder = passwordEncoder; - } - - @Override - public void saveUser(RegistrationDto registrationDto) { - UserEntity user = new UserEntity(); - user.setUsername(registrationDto.getUsername()); - user.setEmail(registrationDto.getEmail()); - user.setPassword(passwordEncoder.encode(registrationDto.getPassword())); - - final Role role = roleRepository.findByName("User").orElse(new Role()); - user.setRoles(Arrays.asList(role)); - userRepository.save(user); - } - - @Override - public Optional findByEmail(String email) { - return userRepository.findByEmail(email); - } - - @Override - public Optional findByUsername(String username) { - return userRepository.findByUsername(username); - } - - public Optional getLoggedInUser() { - final Optional user; - String username = SecurityUtil.getSessionUser(); - if (username != null) { - user = this.findByUsername(username); - } else { - user = Optional.of(new UserEntity()); - } - return user; - } -} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 102e2c2..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1,10 +0,0 @@ -spring.application.name=web -spring.datasource.url=jdbc:postgresql://localhost:5432/spring -spring.datasource.username=tradam -spring.datasource.driver-class-name=org.postgresql.Driver - -spring.jpa.hibernate.ddl-auto=update -spring.jpa.show-sql=true - -spring.security.user.name=zxcv -spring.security.user.password=zxcv diff --git a/src/main/resources/static/abstract-polygonal-banner-background-vector.jpg b/src/main/resources/static/abstract-polygonal-banner-background-vector.jpg deleted file mode 100644 index 1c6746e..0000000 Binary files a/src/main/resources/static/abstract-polygonal-banner-background-vector.jpg and /dev/null differ diff --git a/src/main/resources/templates/articles-list.html b/src/main/resources/templates/articles-list.html deleted file mode 100644 index 799c751..0000000 --- a/src/main/resources/templates/articles-list.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - Title - - -

blah blah

-
-
my aga article
-

- ... -

-
- - \ No newline at end of file diff --git a/src/main/resources/templates/articles/edit.html b/src/main/resources/templates/articles/edit.html deleted file mode 100644 index f4faee9..0000000 --- a/src/main/resources/templates/articles/edit.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - -
-
- -
-
- - -

Please fill out this field.

-
-
- - -

Please fill out this field.

-
-
-
-
- - -

Please fill out this field.

-
-
-
-
- - -
-
- - - \ No newline at end of file diff --git a/src/main/resources/templates/articles/new.html b/src/main/resources/templates/articles/new.html deleted file mode 100644 index f5924d6..0000000 --- a/src/main/resources/templates/articles/new.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - -
-
-
-
- - -

Please fill out this field.

-
-
- - -

Please fill out this field.

-
-
-
-
- - -

Please fill out this field.

-
-
-
-
- - -
-
- - - \ No newline at end of file diff --git a/src/main/resources/templates/articles/show.html b/src/main/resources/templates/articles/show.html deleted file mode 100644 index 206aaae..0000000 --- a/src/main/resources/templates/articles/show.html +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - -
-

-

Welcome to Ghostwind CSS

-
- - -
- - -
- -
- -
- - - -
- - - -
- - - -
-

Subscribe to Ghostwind CSS

-

Get the latest posts delivered right to your inbox

-
-
-
- - -
-
-
-
- - - - -
- Avatar of Author -
-

Ghostwind CSS

-

Tailwind CSS version of Ghost's Casper theme by TailwindToolbox.com

-
-
- -
- - -
- - -
- -
- - - - - - - - \ No newline at end of file diff --git a/src/main/resources/templates/auth/login.html b/src/main/resources/templates/auth/login.html deleted file mode 100644 index 47bc63e..0000000 --- a/src/main/resources/templates/auth/login.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - -
Invalid Username/Password
-
You have been logged out
- -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
- - - \ No newline at end of file diff --git a/src/main/resources/templates/auth/register.html b/src/main/resources/templates/auth/register.html deleted file mode 100644 index dc57ca4..0000000 --- a/src/main/resources/templates/auth/register.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - -
-
Username or Email already exists
-
-
-
- - -

Please fill out this field.

-
-
- - -

Please fill out this field.

-
-
-
-
- - -

Please fill out this field.

-
-
-
-
- - -
-
- - - \ No newline at end of file diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html deleted file mode 100644 index 0c42c21..0000000 --- a/src/main/resources/templates/index.html +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/templates/layout.html b/src/main/resources/templates/layout.html deleted file mode 100644 index f3e0278..0000000 --- a/src/main/resources/templates/layout.html +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - Title Layout - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -

- โ˜• Spring! -

-

Welcome to my Blog

-
-
- - - -
- -
-
- -
-
- - - -
-
-
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
-
-
- - - -
-
- - - - - - \ No newline at end of file diff --git a/src/main/resources/templates/post.html b/src/main/resources/templates/post.html deleted file mode 100644 index a775ff2..0000000 --- a/src/main/resources/templates/post.html +++ /dev/null @@ -1,266 +0,0 @@ - - - - - - - Tailwind Starter Template - Ghostwind CSS : Tailwind Toolbox - - - - - - - - - - - - - - -
-

04 JULY 2023 / GETTING STARTED

-

Welcome to Ghostwind CSS

-
- - -
- - -
- -
- -
- - - - - -

- ๐Ÿ‘‹ Welcome fellow Tailwind CSS and Ghost fan. This starter template is an attempt to replicate the default Ghost theme "Casper" using Tailwind CSS and vanilla Javascript. -

- -

The basic blog page layout is available and all using the default Tailwind CSS classes (although there are a few hardcoded style tags). If you are going to use this in your project, you will want to convert the classes into components.

- -

Sed dignissim lectus ut tincidunt vulputate. Fusce tincidunt lacus purus, in mattis tortor sollicitudin pretium. Phasellus at diam posuere, scelerisque nisl sit amet, tincidunt urna. Cras nisi diam, pulvinar ut molestie eget, eleifend ac magna. Sed at lorem condimentum, dignissim lorem eu, blandit massa. Phasellus eleifend turpis vel erat bibendum scelerisque. Maecenas id risus dictum, rhoncus odio vitae, maximus purus. Etiam efficitur dolor in dolor molestie ornare. Aenean pulvinar diam nec neque tincidunt, vitae molestie quam fermentum. Donec ac pretium diam. Suspendisse sed odio risus. Nunc nec luctus nisi. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Duis nec nulla eget sem dictum elementum.

- -
    -
  1. Maecenas accumsan lacus sit amet elementum porta. Aliquam eu libero lectus. Fusce vehicula dictum mi. In non dolor at sem ullamcorper venenatis ut sed dui. Ut ut est quam. Suspendisse quam quam, commodo sit amet placerat in, interdum a ipsum. Morbi sit amet tellus scelerisque tortor semper posuere.
  2. -
  3. Morbi varius posuere blandit. Praesent gravida bibendum neque eget commodo. Duis auctor ornare mauris, eu accumsan odio viverra in. Proin sagittis maximus pharetra. Nullam lorem mauris, faucibus ut odio tempus, ultrices aliquet ex. Nam id quam eget ipsum luctus hendrerit. Ut eros magna, eleifend ac ornare vulputate, pretium nec felis.
  4. -
  5. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nunc vitae pretium elit. Cras leo mauris, tristique in risus ac, tristique rutrum velit. Mauris accumsan tempor felis vitae gravida. Cras egestas convallis malesuada. Etiam ac ante id tortor vulputate pretium. Maecenas vel sapien suscipit, elementum odio et, consequat tellus.
  6. -
- -
Example of blockquote - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam at ipsum eu nunc commodo posuere et sit amet ligula.
- -

Example code block:

-
-					
-<header class="site-header outer">
-  <div class="inner">
-    {{> "site-nav"}}
-  </div>
-</header>
-					
-				
- - - - -
- - - -
-

Subscribe to Ghostwind CSS

-

Get the latest posts delivered right to your inbox

-
-
-
- - -
-
-
-
- - - - -
- Avatar of Author -
-

Ghostwind CSS

-

Tailwind CSS version of Ghost's Casper theme by TailwindToolbox.com

-
-
- -
- - -
- - -
- -
- - - - - - - - - - - - - \ No newline at end of file diff --git a/src/test/java/com/blog/web/WebApplicationTests.java b/src/test/java/com/blog/web/WebApplicationTests.java deleted file mode 100644 index c3a5ef9..0000000 --- a/src/test/java/com/blog/web/WebApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.blog.web; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class WebApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/src/test/java/com/blog/web/repository/ArticleRepositoryTests.java b/src/test/java/com/blog/web/repository/ArticleRepositoryTests.java deleted file mode 100644 index 96c5f5b..0000000 --- a/src/test/java/com/blog/web/repository/ArticleRepositoryTests.java +++ /dev/null @@ -1,81 +0,0 @@ - -package com.blog.web.repository; - -import com.blog.web.dto.ArticleDto; -import com.blog.web.dto.RegistrationDto; -import com.blog.web.models.Article; -import com.blog.web.models.UserEntity; -import com.blog.web.services.ArticleService; -import com.blog.web.services.UserService; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.security.test.context.support.WithMockUser; - -import java.time.LocalDateTime; -import java.util.List; - -@SpringBootTest -@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2) -public class ArticleRepositoryTests { - - @Autowired - private ArticleRepository articleRepository; - @Autowired - private UserService userService; - @Autowired - private ArticleService articleService; - - - @Test - @WithMockUser(username="test", password="blah") - public void ArticleRepository_Search_ReturnSearch() { - // Arrange - RegistrationDto userDto = new RegistrationDto( - "test", - "test@example.com", - "blah" - ); - userService.saveUser(userDto); - UserEntity user = userService.findByUsername("test").orElseThrow(); - final ArticleDto articleDto1 = new ArticleDto( - 1, - "Title", - "https://duckduckgo.com", - "Content", - user, - LocalDateTime.now(), - LocalDateTime.now() - ); - articleService.saveArticle(articleDto1); - final ArticleDto articleDto2 = new ArticleDto( - 2, - "itleblah", - "https://duckduckgo.com", - "Content", - user, - LocalDateTime.now(), - LocalDateTime.now() - ); - articleService.saveArticle(articleDto2); - final ArticleDto articleDto3 = new ArticleDto( - 3, - "dontfindme", - "https://duckduckgo.com", - "Content", - user, - LocalDateTime.now(), - LocalDateTime.now() - ); - articleService.saveArticle(articleDto3); - - // Act - List
list = articleRepository.searchArticles("itle"); // partial string should find 2 articles - - // Assert - Assertions.assertEquals(2, list.size()); - } -} diff --git a/src/test/java/com/blog/web/services/ArticleServiceTests.java b/src/test/java/com/blog/web/services/ArticleServiceTests.java deleted file mode 100644 index d55516a..0000000 --- a/src/test/java/com/blog/web/services/ArticleServiceTests.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.blog.web.services; - -import com.blog.web.dto.ArticleDto; -import com.blog.web.models.Article; -import com.blog.web.models.UserEntity; -import com.blog.web.repository.ArticleRepository; -import com.blog.web.repository.UserRepository; -import com.blog.web.security.SecurityUtil; -import com.blog.web.services.impl.ArticleServiceImpl; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.security.test.context.support.WithMockUser; - -import java.time.LocalDateTime; -import java.util.Optional; - -import static org.mockito.AdditionalAnswers.returnsFirstArg; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.when; - -//@SpringBootTest -@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2) -@ExtendWith(MockitoExtension.class) -public class ArticleServiceTests { - - @Mock - private ArticleRepository articleRepository; - @Mock - private UserRepository userRepository; - @InjectMocks - private ArticleServiceImpl articleService; - - @Test - @WithMockUser(username="test", password="blah") - public void ArticleService_SaveArticle_ReturnsArticle() { - //RegistrationDto userDto = new RegistrationDto( - // "test", - // "test@example.com", - // "blah" - //); - //userService.saveUser(userDto); - UserEntity user = new UserEntity(); - user.setUsername("test"); - user.setEmail("test@example.com"); - user.setPassword("blah"); - //UserEntity user = userService.findByUsername("test"); - final ArticleDto articleDto = new ArticleDto( - 1, - "Title", - "https://duckduckgo.com", - "Content", - user, - LocalDateTime.now(), - LocalDateTime.now() - ); - - when(userRepository.findByUsername(anyString())).thenReturn(Optional.of(user)); - when(articleRepository.save(any(Article.class))).then(returnsFirstArg()); - //when(SecurityUtil.getSessionUser()).thenReturn(user.getUsername()); - try(MockedStatic utilities = Mockito.mockStatic(SecurityUtil.class)) { - utilities.when(SecurityUtil::getSessionUser).thenReturn(user.getUsername()); - Assertions.assertEquals(user.getUsername(), SecurityUtil.getSessionUser()); - Article article = articleService.saveArticle(articleDto).orElse(null); - - Assertions.assertNotNull(article); - } - } -} -- cgit v1.2.3