├── .dockerignore ├── .env.local ├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── LICENSE ├── README.md ├── compose.yaml ├── mvnw ├── mvnw.cmd ├── pom.xml ├── src ├── main │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── springbootblogapplication │ │ │ ├── SpringBootBlogApplication.java │ │ │ ├── config │ │ │ ├── ApplicationConfig.java │ │ │ ├── SeedData.java │ │ │ └── WebSecurityConfig.java │ │ │ ├── controllers │ │ │ ├── HomeController.java │ │ │ ├── ImageController.java │ │ │ ├── LoginController.java │ │ │ ├── PostController.java │ │ │ ├── RegisterController.java │ │ │ └── RssFeedController.java │ │ │ ├── models │ │ │ ├── Account.java │ │ │ ├── Authority.java │ │ │ └── Post.java │ │ │ ├── repositories │ │ │ ├── AccountRepository.java │ │ │ ├── AuthorityRepository.java │ │ │ └── PostRepository.java │ │ │ └── services │ │ │ ├── AccountService.java │ │ │ ├── FileService.java │ │ │ ├── MyUserDetailsService.java │ │ │ └── PostService.java │ └── resources │ │ ├── application.properties │ │ ├── db │ │ └── migration │ │ │ ├── V1__initial_migration.sql │ │ │ └── V2__add_imageFilePathToPost.sql │ │ └── templates │ │ ├── 404.html │ │ ├── error.html │ │ ├── home.html │ │ ├── login.html │ │ ├── post.html │ │ ├── post_edit.html │ │ ├── post_new.html │ │ └── register.html └── test │ └── java │ └── com │ └── example │ └── springbootblogapplication │ └── SpringBootBlogApplicationTests.java └── uploads └── pexels-adrien-olichon-16059681.jpg /.dockerignore: -------------------------------------------------------------------------------- 1 | .env 2 | -------------------------------------------------------------------------------- /.env.local: -------------------------------------------------------------------------------- 1 | # rename or copy this file to .env 2 | # then populate with your own values 3 | MYSQL_DATABASE= 4 | MYSQL_USER= 5 | MYSQL_PASSWORD= 6 | MYSQL_ROOT_PASSWORD= 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | 35 | data/ 36 | .env 37 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wazooinc/spring-boot-blog-application/cb88d96ed8984afa7ea9b6cfbbbd889165705071/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.5/apache-maven-3.8.5-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Wazoo Enterprises Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Boot Blog Application 2 | 3 | This is an ENTIRE application for Java Spring Boot using: 4 | - Spring Data JPA 5 | - H2 Database 6 | - Thymeleaf 7 | - Spring Security 8 | - Model View Controller (MVC) architecture 9 | 10 | **I walk through how to build this yourself on YouTube**: https://youtu.be/7iWlCl35p9o 11 | 12 | ## Development Instructions 13 | 14 | - `git clone https://github.com/wazooinc/spring-boot-blog-application.git` 15 | - `cd spring-boot-blog-application` 16 | - open in favorite editor, or 17 | - `mvnw spring-boot:run` 18 | - open http://localhost:3000 and Blog away! 19 | 20 | ## Built-in Account and Constraints 21 | 22 | - Login as a User with `user.user@domain.com` and password `password` 23 | - Login as an Admin with `admin.admin@domain.com` and password `password` 24 | - An **Anonymous** account can only `READ` Posts 25 | - A **User** account can `CREATE, READ, UPDATE` Posts 26 | - An **Admin** account can `CREATE, READ, UPDATE, DELETE` Posts 27 | 28 | ## LICENSE 29 | 30 | MIT License 31 | 32 | Copyright (c) 2022 Wazoo Enterprises Inc. 33 | 34 | Permission is hereby granted, free of charge, to any person obtaining a copy 35 | of this software and associated documentation files (the "Software"), to deal 36 | in the Software without restriction, including without limitation the rights 37 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 38 | copies of the Software, and to permit persons to whom the Software is 39 | furnished to do so, subject to the following conditions: 40 | 41 | The above copyright notice and this permission notice shall be included in all 42 | copies or substantial portions of the Software. 43 | 44 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 45 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 46 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 47 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 48 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 49 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 50 | SOFTWARE. 51 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | database: 5 | image: mysql:8.0 6 | restart: unless-stopped 7 | env_file: .env 8 | environment: 9 | MYSQL_DATABASE: ${MYSQL_DATABASE} 10 | MYSQL_PASSWORD: ${MYSQL_PASSWORD} 11 | MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} 12 | MYSQL_USER: ${MYSQL_USER} 13 | healthcheck: 14 | test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD" ] 15 | timeout: 20s 16 | retries: 10 17 | ports: 18 | - "3306:3306" 19 | volumes: 20 | - db-data:/var/lib/db 21 | networks: 22 | - spring-network 23 | 24 | phpmyadmin: 25 | depends_on: 26 | database: 27 | condition: service_healthy 28 | image: phpmyadmin/phpmyadmin 29 | restart: unless-stopped 30 | ports: 31 | - "8081:80" 32 | env_file: .env 33 | environment: 34 | PMA_HOST: database 35 | MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} 36 | networks: 37 | - spring-network 38 | 39 | volumes: 40 | db-data: {} 41 | 42 | networks: 43 | spring-network: 44 | driver: bridge 45 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /usr/local/etc/mavenrc ] ; then 40 | . /usr/local/etc/mavenrc 41 | fi 42 | 43 | if [ -f /etc/mavenrc ] ; then 44 | . /etc/mavenrc 45 | fi 46 | 47 | if [ -f "$HOME/.mavenrc" ] ; then 48 | . "$HOME/.mavenrc" 49 | fi 50 | 51 | fi 52 | 53 | # OS specific support. $var _must_ be set to either true or false. 54 | cygwin=false; 55 | darwin=false; 56 | mingw=false 57 | case "`uname`" in 58 | CYGWIN*) cygwin=true ;; 59 | MINGW*) mingw=true;; 60 | Darwin*) darwin=true 61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 63 | if [ -z "$JAVA_HOME" ]; then 64 | if [ -x "/usr/libexec/java_home" ]; then 65 | export JAVA_HOME="`/usr/libexec/java_home`" 66 | else 67 | export JAVA_HOME="/Library/Java/Home" 68 | fi 69 | fi 70 | ;; 71 | esac 72 | 73 | if [ -z "$JAVA_HOME" ] ; then 74 | if [ -r /etc/gentoo-release ] ; then 75 | JAVA_HOME=`java-config --jre-home` 76 | fi 77 | fi 78 | 79 | if [ -z "$M2_HOME" ] ; then 80 | ## resolve links - $0 may be a link to maven's home 81 | PRG="$0" 82 | 83 | # need this for relative symlinks 84 | while [ -h "$PRG" ] ; do 85 | ls=`ls -ld "$PRG"` 86 | link=`expr "$ls" : '.*-> \(.*\)$'` 87 | if expr "$link" : '/.*' > /dev/null; then 88 | PRG="$link" 89 | else 90 | PRG="`dirname "$PRG"`/$link" 91 | fi 92 | done 93 | 94 | saveddir=`pwd` 95 | 96 | M2_HOME=`dirname "$PRG"`/.. 97 | 98 | # make it fully qualified 99 | M2_HOME=`cd "$M2_HOME" && pwd` 100 | 101 | cd "$saveddir" 102 | # echo Using m2 at $M2_HOME 103 | fi 104 | 105 | # For Cygwin, ensure paths are in UNIX format before anything is touched 106 | if $cygwin ; then 107 | [ -n "$M2_HOME" ] && 108 | M2_HOME=`cygpath --unix "$M2_HOME"` 109 | [ -n "$JAVA_HOME" ] && 110 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 111 | [ -n "$CLASSPATH" ] && 112 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 113 | fi 114 | 115 | # For Mingw, ensure paths are in UNIX format before anything is touched 116 | if $mingw ; then 117 | [ -n "$M2_HOME" ] && 118 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 119 | [ -n "$JAVA_HOME" ] && 120 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 121 | fi 122 | 123 | if [ -z "$JAVA_HOME" ]; then 124 | javaExecutable="`which javac`" 125 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 126 | # readlink(1) is not available as standard on Solaris 10. 127 | readLink=`which readlink` 128 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 129 | if $darwin ; then 130 | javaHome="`dirname \"$javaExecutable\"`" 131 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 132 | else 133 | javaExecutable="`readlink -f \"$javaExecutable\"`" 134 | fi 135 | javaHome="`dirname \"$javaExecutable\"`" 136 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 137 | JAVA_HOME="$javaHome" 138 | export JAVA_HOME 139 | fi 140 | fi 141 | fi 142 | 143 | if [ -z "$JAVACMD" ] ; then 144 | if [ -n "$JAVA_HOME" ] ; then 145 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 146 | # IBM's JDK on AIX uses strange locations for the executables 147 | JAVACMD="$JAVA_HOME/jre/sh/java" 148 | else 149 | JAVACMD="$JAVA_HOME/bin/java" 150 | fi 151 | else 152 | JAVACMD="`\\unset -f command; \\command -v java`" 153 | fi 154 | fi 155 | 156 | if [ ! -x "$JAVACMD" ] ; then 157 | echo "Error: JAVA_HOME is not defined correctly." >&2 158 | echo " We cannot execute $JAVACMD" >&2 159 | exit 1 160 | fi 161 | 162 | if [ -z "$JAVA_HOME" ] ; then 163 | echo "Warning: JAVA_HOME environment variable is not set." 164 | fi 165 | 166 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 167 | 168 | # traverses directory structure from process work directory to filesystem root 169 | # first directory with .mvn subdirectory is considered project base directory 170 | find_maven_basedir() { 171 | 172 | if [ -z "$1" ] 173 | then 174 | echo "Path not specified to find_maven_basedir" 175 | return 1 176 | fi 177 | 178 | basedir="$1" 179 | wdir="$1" 180 | while [ "$wdir" != '/' ] ; do 181 | if [ -d "$wdir"/.mvn ] ; then 182 | basedir=$wdir 183 | break 184 | fi 185 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 186 | if [ -d "${wdir}" ]; then 187 | wdir=`cd "$wdir/.."; pwd` 188 | fi 189 | # end of workaround 190 | done 191 | echo "${basedir}" 192 | } 193 | 194 | # concatenates all lines of a file 195 | concat_lines() { 196 | if [ -f "$1" ]; then 197 | echo "$(tr -s '\n' ' ' < "$1")" 198 | fi 199 | } 200 | 201 | BASE_DIR=`find_maven_basedir "$(pwd)"` 202 | if [ -z "$BASE_DIR" ]; then 203 | exit 1; 204 | fi 205 | 206 | ########################################################################################## 207 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 208 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 209 | ########################################################################################## 210 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Found .mvn/wrapper/maven-wrapper.jar" 213 | fi 214 | else 215 | if [ "$MVNW_VERBOSE" = true ]; then 216 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 217 | fi 218 | if [ -n "$MVNW_REPOURL" ]; then 219 | jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 220 | else 221 | jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 222 | fi 223 | while IFS="=" read key value; do 224 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 225 | esac 226 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 227 | if [ "$MVNW_VERBOSE" = true ]; then 228 | echo "Downloading from: $jarUrl" 229 | fi 230 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 231 | if $cygwin; then 232 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 233 | fi 234 | 235 | if command -v wget > /dev/null; then 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Found wget ... using wget" 238 | fi 239 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 240 | wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 241 | else 242 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 243 | fi 244 | elif command -v curl > /dev/null; then 245 | if [ "$MVNW_VERBOSE" = true ]; then 246 | echo "Found curl ... using curl" 247 | fi 248 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 249 | curl -o "$wrapperJarPath" "$jarUrl" -f 250 | else 251 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 252 | fi 253 | 254 | else 255 | if [ "$MVNW_VERBOSE" = true ]; then 256 | echo "Falling back to using Java to download" 257 | fi 258 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 259 | # For Cygwin, switch paths to Windows format before running javac 260 | if $cygwin; then 261 | javaClass=`cygpath --path --windows "$javaClass"` 262 | fi 263 | if [ -e "$javaClass" ]; then 264 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 265 | if [ "$MVNW_VERBOSE" = true ]; then 266 | echo " - Compiling MavenWrapperDownloader.java ..." 267 | fi 268 | # Compiling the Java class 269 | ("$JAVA_HOME/bin/javac" "$javaClass") 270 | fi 271 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 272 | # Running the downloader 273 | if [ "$MVNW_VERBOSE" = true ]; then 274 | echo " - Running MavenWrapperDownloader.java ..." 275 | fi 276 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 277 | fi 278 | fi 279 | fi 280 | fi 281 | ########################################################################################## 282 | # End of extension 283 | ########################################################################################## 284 | 285 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 286 | if [ "$MVNW_VERBOSE" = true ]; then 287 | echo $MAVEN_PROJECTBASEDIR 288 | fi 289 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 290 | 291 | # For Cygwin, switch paths to Windows format before running java 292 | if $cygwin; then 293 | [ -n "$M2_HOME" ] && 294 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 295 | [ -n "$JAVA_HOME" ] && 296 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 297 | [ -n "$CLASSPATH" ] && 298 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 299 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 300 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 301 | fi 302 | 303 | # Provide a "standardized" way to retrieve the CLI args that will 304 | # work with both Windows and non-Windows executions. 305 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 306 | export MAVEN_CMD_LINE_ARGS 307 | 308 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 309 | 310 | exec "$JAVACMD" \ 311 | $MAVEN_OPTS \ 312 | $MAVEN_DEBUG_OPTS \ 313 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 314 | "-Dmaven.home=${M2_HOME}" \ 315 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 316 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 317 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 50 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 124 | 125 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% ^ 162 | %JVM_CONFIG_MAVEN_PROPS% ^ 163 | %MAVEN_OPTS% ^ 164 | %MAVEN_DEBUG_OPTS% ^ 165 | -classpath %WRAPPER_JAR% ^ 166 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 167 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 168 | if ERRORLEVEL 1 goto error 169 | goto end 170 | 171 | :error 172 | set ERROR_CODE=1 173 | 174 | :end 175 | @endlocal & set ERROR_CODE=%ERROR_CODE% 176 | 177 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 178 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 179 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 180 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 181 | :skipRcPost 182 | 183 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 184 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 185 | 186 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 187 | 188 | cmd /C exit /B %ERROR_CODE% 189 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.1.5 9 | 10 | 11 | com.example 12 | spring-boot-blog-application 13 | 1.4.0 14 | spring-boot-blog-application 15 | Demo project for Spring Boot 16 | 17 | 17 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-data-jpa 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-thymeleaf 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-web 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-devtools 35 | runtime 36 | true 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-docker-compose 41 | runtime 42 | true 43 | 44 | 45 | com.mysql 46 | mysql-connector-j 47 | runtime 48 | 49 | 50 | me.paulschwarz 51 | spring-dotenv 52 | 4.0.0 53 | 54 | 55 | org.projectlombok 56 | lombok 57 | true 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-starter-test 62 | test 63 | 64 | 65 | 66 | org.springframework.boot 67 | spring-boot-starter-security 68 | 69 | 70 | org.thymeleaf.extras 71 | thymeleaf-extras-springsecurity6 72 | 73 | 74 | org.springframework.boot 75 | spring-boot-starter-validation 76 | 77 | 78 | org.webjars 79 | bootstrap 80 | 5.3.0 81 | 82 | 83 | com.rometools 84 | rome 85 | 1.18.0 86 | 87 | 88 | org.flywaydb 89 | flyway-core 90 | 91 | 92 | org.flywaydb 93 | flyway-mysql 94 | 95 | 96 | org.springframework.boot 97 | spring-boot-properties-migrator 98 | runtime 99 | 100 | 101 | 102 | 103 | 104 | 105 | org.springframework.boot 106 | spring-boot-maven-plugin 107 | 108 | 109 | 110 | org.projectlombok 111 | lombok 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /src/main/java/com/example/springbootblogapplication/SpringBootBlogApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.springbootblogapplication; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class SpringBootBlogApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(SpringBootBlogApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/example/springbootblogapplication/config/ApplicationConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.springbootblogapplication.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 6 | import org.springframework.security.crypto.password.PasswordEncoder; 7 | 8 | @Configuration 9 | public class ApplicationConfig { 10 | 11 | @Bean 12 | public static PasswordEncoder passwordEncoder() { 13 | return new BCryptPasswordEncoder(); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/example/springbootblogapplication/config/SeedData.java: -------------------------------------------------------------------------------- 1 | package com.example.springbootblogapplication.config; 2 | 3 | import com.example.springbootblogapplication.models.Account; 4 | import com.example.springbootblogapplication.models.Authority; 5 | import com.example.springbootblogapplication.models.Post; 6 | import com.example.springbootblogapplication.repositories.AuthorityRepository; 7 | import com.example.springbootblogapplication.services.AccountService; 8 | import com.example.springbootblogapplication.services.FileService; 9 | import com.example.springbootblogapplication.services.PostService; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.CommandLineRunner; 12 | import org.springframework.stereotype.Component; 13 | 14 | import java.util.HashSet; 15 | import java.util.List; 16 | import java.util.Set; 17 | 18 | @Component 19 | public class SeedData implements CommandLineRunner { 20 | 21 | @Autowired 22 | private FileService fileService; 23 | 24 | @Autowired 25 | private PostService postService; 26 | 27 | @Autowired 28 | private AccountService accountService; 29 | 30 | @Autowired 31 | private AuthorityRepository authorityRepository; 32 | 33 | @Override 34 | public void run(String... args) throws Exception { 35 | 36 | fileService.init(); 37 | 38 | List posts = postService.getAll(); 39 | 40 | if (posts.size() == 0) { 41 | 42 | Authority user = new Authority(); 43 | user.setName("ROLE_USER"); 44 | authorityRepository.save(user); 45 | 46 | Authority admin = new Authority(); 47 | admin.setName("ROLE_ADMIN"); 48 | authorityRepository.save(admin); 49 | 50 | Account account1 = Account 51 | .builder() 52 | .firstName("user_first") 53 | .lastName("user_last") 54 | .email("user.user@domain.com") 55 | .password("password") 56 | .build(); 57 | 58 | Set authorities1 = new HashSet<>(); 59 | authorityRepository.findById("ROLE_USER").ifPresent(authorities1::add); 60 | account1.setAuthorities(authorities1); 61 | 62 | Account account2 = Account 63 | .builder() 64 | .firstName("admin_first") 65 | .lastName("admin_last") 66 | .email("admin.admin@domain.com") 67 | .password("password") 68 | .build(); 69 | 70 | Set authorities2 = new HashSet<>(); 71 | authorityRepository.findById("ROLE_ADMIN").ifPresent(authorities2::add); 72 | //authorityRepository.findById("ROLE_USER").ifPresent(authorities2::add); 73 | account2.setAuthorities(authorities2); 74 | 75 | accountService.save(account1); 76 | accountService.save(account2); 77 | 78 | Post post1 = Post 79 | .builder() 80 | .title("What is Lorem Ipsum?") 81 | .body("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") 82 | .account(account1) 83 | .imageFilePath("pexels-adrien-olichon-16059681.jpg") 84 | .build(); 85 | 86 | Post post2 = Post 87 | .builder() 88 | .title("Something else Ipsum") 89 | .body("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Magna eget est lorem ipsum dolor sit amet consectetur adipiscing. Tempus quam pellentesque nec nam aliquam sem et tortor. Pellentesque sit amet porttitor eget. Sed augue lacus viverra vitae congue eu consequat. Ultrices vitae auctor eu augue. Mattis rhoncus urna neque viverra. Consectetur lorem donec massa sapien faucibus et molestie ac feugiat. Sociis natoque penatibus et magnis dis parturient montes nascetur. Cursus turpis massa tincidunt dui ut ornare lectus. Odio pellentesque diam volutpat commodo sed egestas egestas fringilla. Id cursus metus aliquam eleifend mi. Nibh nisl condimentum id venenatis a condimentum.") 90 | .account(account2) 91 | .imageFilePath("pexels-adrien-olichon-16059681.jpg") 92 | .build(); 93 | 94 | postService.save(post1); 95 | postService.save(post2); 96 | } 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/example/springbootblogapplication/config/WebSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.springbootblogapplication.config; 2 | 3 | 4 | import org.springframework.boot.autoconfigure.security.servlet.PathRequest; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 8 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 10 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 11 | import org.springframework.security.web.SecurityFilterChain; 12 | 13 | import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; 14 | 15 | @Configuration 16 | @EnableWebSecurity 17 | @EnableMethodSecurity(securedEnabled = true) 18 | public class WebSecurityConfig { 19 | 20 | 21 | @Bean 22 | public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { 23 | // Note: spring security requestMatchers updated again 24 | // https://stackoverflow.com/questions/76809698/spring-security-method-cannot-decide-pattern-is-mvc-or-not-spring-boot-applicati 25 | http 26 | .csrf(AbstractHttpConfigurer::disable) 27 | .authorizeHttpRequests(auth -> { 28 | auth.requestMatchers(antMatcher("/css/**")).permitAll(); 29 | auth.requestMatchers(antMatcher("/js/**")).permitAll(); 30 | auth.requestMatchers(antMatcher("/images/**")).permitAll(); 31 | auth.requestMatchers(antMatcher("/fonts/**")).permitAll(); 32 | auth.requestMatchers(antMatcher("/webjars/**")).permitAll(); 33 | auth.requestMatchers(antMatcher("/")).permitAll(); 34 | auth.requestMatchers(antMatcher("/rss/**")).permitAll(); 35 | auth.requestMatchers(antMatcher("/register/**")).permitAll(); 36 | auth.requestMatchers(antMatcher("/posts/**")).permitAll(); 37 | auth.requestMatchers(PathRequest.toH2Console()).permitAll(); 38 | auth.anyRequest().authenticated(); 39 | }) 40 | 41 | .formLogin(form -> form 42 | .loginPage("/login") 43 | .loginProcessingUrl("/login") 44 | .usernameParameter("email") 45 | .passwordParameter("password") 46 | .defaultSuccessUrl("/") 47 | .failureUrl("/login?error") 48 | .permitAll() 49 | ); 50 | 51 | 52 | return http.build(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/example/springbootblogapplication/controllers/HomeController.java: -------------------------------------------------------------------------------- 1 | package com.example.springbootblogapplication.controllers; 2 | 3 | import com.example.springbootblogapplication.models.Post; 4 | import com.example.springbootblogapplication.services.PostService; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.ui.Model; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | 10 | import java.util.List; 11 | 12 | @Controller 13 | @RequiredArgsConstructor 14 | public class HomeController { 15 | 16 | private final PostService postService; 17 | 18 | @GetMapping("/") 19 | public String home(Model model) { 20 | List posts = postService.getAll(); 21 | model.addAttribute("posts", posts); 22 | return "home"; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/example/springbootblogapplication/controllers/ImageController.java: -------------------------------------------------------------------------------- 1 | package com.example.springbootblogapplication.controllers; 2 | 3 | import com.example.springbootblogapplication.services.FileService; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.core.io.Resource; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | @RestController 11 | @RequiredArgsConstructor 12 | public class ImageController { 13 | private final FileService fileService; 14 | 15 | @GetMapping("/images/{id}") 16 | public Resource getImage(@PathVariable("id") String imageUri) { 17 | return fileService.load(imageUri); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/example/springbootblogapplication/controllers/LoginController.java: -------------------------------------------------------------------------------- 1 | package com.example.springbootblogapplication.controllers; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | @Controller 7 | public class LoginController { 8 | 9 | @GetMapping("/login") 10 | public String getLogin() { 11 | 12 | return "login"; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/example/springbootblogapplication/controllers/PostController.java: -------------------------------------------------------------------------------- 1 | package com.example.springbootblogapplication.controllers; 2 | 3 | import com.example.springbootblogapplication.models.Account; 4 | import com.example.springbootblogapplication.models.Post; 5 | import com.example.springbootblogapplication.services.AccountService; 6 | import com.example.springbootblogapplication.services.FileService; 7 | import com.example.springbootblogapplication.services.PostService; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.security.access.prepost.PreAuthorize; 11 | import org.springframework.stereotype.Controller; 12 | import org.springframework.ui.Model; 13 | import org.springframework.validation.BindingResult; 14 | import org.springframework.web.bind.annotation.*; 15 | import org.springframework.web.bind.annotation.*; 16 | import org.springframework.web.multipart.MultipartFile; 17 | 18 | import java.security.Principal; 19 | import java.util.Optional; 20 | 21 | @Controller 22 | @RequiredArgsConstructor 23 | @Slf4j 24 | public class PostController { 25 | 26 | private final PostService postService; 27 | private final AccountService accountService; 28 | private final FileService fileService; 29 | 30 | @GetMapping("/posts/{id}") 31 | public String getPost(@PathVariable Long id, Model model) { 32 | 33 | // find post by id 34 | Optional optionalPost = this.postService.getById(id); 35 | 36 | // if post exists put it in model 37 | if (optionalPost.isPresent()) { 38 | Post post = optionalPost.get(); 39 | model.addAttribute("post", post); 40 | return "post"; 41 | } else { 42 | return "404"; 43 | } 44 | } 45 | 46 | @PostMapping("/posts/{id}") 47 | @PreAuthorize("isAuthenticated()") 48 | public String updatePost(@PathVariable Long id, Post post, @RequestParam("file") MultipartFile file) { 49 | 50 | Optional optionalPost = postService.getById(id); 51 | if (optionalPost.isPresent()) { 52 | Post existingPost = optionalPost.get(); 53 | 54 | existingPost.setTitle(post.getTitle()); 55 | existingPost.setBody(post.getBody()); 56 | 57 | try { 58 | fileService.save(file); 59 | existingPost.setImageFilePath(file.getOriginalFilename()); 60 | } catch (Exception e) { 61 | log.error("Error processing file: {}", file.getOriginalFilename()); 62 | } 63 | 64 | postService.save(existingPost); 65 | } 66 | 67 | return "redirect:/posts/" + post.getId(); 68 | } 69 | 70 | @GetMapping("/posts/new") 71 | @PreAuthorize("isAuthenticated()") 72 | public String createNewPost(Model model) { 73 | 74 | Post post = new Post(); 75 | model.addAttribute("post", post); 76 | return "post_new"; 77 | } 78 | 79 | @PostMapping("/posts/new") 80 | @PreAuthorize("isAuthenticated()") 81 | public String createNewPost(@ModelAttribute Post post, @RequestParam("file") MultipartFile file, Principal principal) { 82 | String authUsername = "anonymousUser"; 83 | if (principal != null) { 84 | authUsername = principal.getName(); 85 | } 86 | 87 | Account account = accountService.findOneByEmail(authUsername).orElseThrow(() -> new IllegalArgumentException("Account not found")); 88 | 89 | try { 90 | fileService.save(file); 91 | post.setImageFilePath(file.getOriginalFilename()); 92 | } catch (Exception e) { 93 | log.error("Error processing file: {}", file.getOriginalFilename()); 94 | } 95 | 96 | post.setAccount(account); 97 | postService.save(post); 98 | return "redirect:/"; 99 | } 100 | 101 | @GetMapping("/posts/{id}/edit") 102 | @PreAuthorize("isAuthenticated()") 103 | public String getPostForEdit(@PathVariable Long id, Model model) { 104 | 105 | // find post by id 106 | Optional optionalPost = postService.getById(id); 107 | // if post exist put it in model 108 | if (optionalPost.isPresent()) { 109 | Post post = optionalPost.get(); 110 | model.addAttribute("post", post); 111 | return "post_edit"; 112 | } else { 113 | return "404"; 114 | } 115 | } 116 | 117 | @GetMapping("/posts/{id}/delete") 118 | @PreAuthorize("hasRole('ROLE_ADMIN')") 119 | public String deletePost(@PathVariable Long id) { 120 | 121 | // find post by id 122 | Optional optionalPost = postService.getById(id); 123 | if (optionalPost.isPresent()) { 124 | Post post = optionalPost.get(); 125 | 126 | postService.delete(post); 127 | return "redirect:/"; 128 | } else { 129 | return "404"; 130 | } 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/com/example/springbootblogapplication/controllers/RegisterController.java: -------------------------------------------------------------------------------- 1 | package com.example.springbootblogapplication.controllers; 2 | 3 | import com.example.springbootblogapplication.models.Account; 4 | import com.example.springbootblogapplication.services.AccountService; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.ui.Model; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.ModelAttribute; 10 | import org.springframework.web.bind.annotation.PostMapping; 11 | 12 | @Controller 13 | @RequiredArgsConstructor 14 | public class RegisterController { 15 | 16 | private final AccountService accountService; 17 | 18 | @GetMapping("/register") 19 | public String getRegisterForm(Model model) { 20 | 21 | Account account = new Account(); 22 | model.addAttribute("account", account); 23 | return "register"; 24 | } 25 | 26 | @PostMapping("/register") 27 | public String registerNewUser(@ModelAttribute Account account) { 28 | accountService.save(account); 29 | return "redirect:/"; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/example/springbootblogapplication/controllers/RssFeedController.java: -------------------------------------------------------------------------------- 1 | package com.example.springbootblogapplication.controllers; 2 | 3 | import java.time.ZoneId; 4 | import java.util.ArrayList; 5 | import java.util.Date; 6 | import java.util.List; 7 | 8 | import jakarta.servlet.http.HttpServletRequest; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import com.example.springbootblogapplication.models.Post; 13 | import com.example.springbootblogapplication.services.PostService; 14 | import com.rometools.rome.feed.rss.Channel; 15 | import com.rometools.rome.feed.rss.Description; 16 | import com.rometools.rome.feed.rss.Item; 17 | import org.springframework.web.servlet.support.ServletUriComponentsBuilder; 18 | import lombok.RequiredArgsConstructor; 19 | 20 | @RestController 21 | @RequiredArgsConstructor 22 | public class RssFeedController { 23 | 24 | private final PostService postService; 25 | 26 | @GetMapping(path = "/rss") 27 | public Channel rssFeed(HttpServletRequest request) { 28 | 29 | String baseUrl = ServletUriComponentsBuilder.fromRequestUri(request) 30 | .replacePath(null) 31 | .build() 32 | .toUriString(); 33 | 34 | Channel channel = new Channel(); 35 | channel.setFeedType("rss_2.0"); 36 | channel.setTitle("Spring Boot Blog Application"); 37 | channel.setDescription("My Spring Boot Blog Demo App"); 38 | channel.setLink(baseUrl); 39 | channel.setUri(baseUrl); 40 | channel.setGenerator("Custom Sauce"); 41 | 42 | Date postDate = new Date(); 43 | channel.setPubDate(postDate); 44 | List feed = new ArrayList<>(); 45 | List posts = postService.getAll(); 46 | for(Post post: posts) { 47 | Item item = new Item(); 48 | item.setAuthor("Foo"); 49 | item.setLink(baseUrl + "/posts/" + post.getId()); 50 | item.setTitle(post.getTitle()); 51 | item.setUri(baseUrl + "/posts/" + post.getId()); 52 | 53 | Description descr = new Description(); 54 | descr.setValue(post.getBody()); 55 | item.setDescription(descr); 56 | item.setPubDate(Date.from(post.getUpdatedAt().atZone(ZoneId.systemDefault()).toInstant())); 57 | 58 | //channel.setItems(Collections.singletonList(item)); 59 | feed.add(item); 60 | } 61 | 62 | channel.setItems(feed); 63 | 64 | return channel; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/example/springbootblogapplication/models/Account.java: -------------------------------------------------------------------------------- 1 | package com.example.springbootblogapplication.models; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import lombok.*; 5 | 6 | import jakarta.persistence.*; 7 | 8 | import java.time.LocalDateTime; 9 | import java.util.HashSet; 10 | import java.util.List; 11 | import java.util.Set; 12 | 13 | @Entity 14 | @Getter 15 | @Setter 16 | @NoArgsConstructor 17 | @ToString 18 | @Builder 19 | @AllArgsConstructor 20 | public class Account { 21 | 22 | @Id 23 | @GeneratedValue(strategy = GenerationType.SEQUENCE) 24 | Long id; 25 | 26 | String email; 27 | 28 | @JsonIgnore 29 | String password; 30 | 31 | String firstName; 32 | 33 | String lastName; 34 | 35 | LocalDateTime createdAt; 36 | 37 | LocalDateTime updatedAt; 38 | 39 | @JsonIgnore 40 | @OneToMany(mappedBy = "account") 41 | List posts; 42 | 43 | @ManyToMany(fetch = FetchType.EAGER) 44 | @JoinTable( 45 | name = "account_authority", 46 | joinColumns = {@JoinColumn(name = "account_id", referencedColumnName = "id")}, 47 | inverseJoinColumns = {@JoinColumn(name = "authority_name", referencedColumnName = "name")}) 48 | Set authorities = new HashSet<>(); 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/example/springbootblogapplication/models/Authority.java: -------------------------------------------------------------------------------- 1 | package com.example.springbootblogapplication.models; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | import jakarta.persistence.*; 7 | import lombok.ToString; 8 | 9 | @Entity 10 | @Setter 11 | @Getter 12 | @NoArgsConstructor 13 | @ToString 14 | public class Authority { 15 | 16 | @Id 17 | @Column(length = 16) 18 | String name; 19 | 20 | @Override 21 | public boolean equals(Object o) { 22 | if (this == o) { 23 | return true; 24 | } 25 | 26 | if (o == null || getClass() != o.getClass()) { 27 | return false; 28 | } 29 | 30 | Authority authority1 = (Authority) o; 31 | 32 | return name.equals(authority1.name); 33 | } 34 | 35 | @Override 36 | public int hashCode() { 37 | return name.hashCode(); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/example/springbootblogapplication/models/Post.java: -------------------------------------------------------------------------------- 1 | package com.example.springbootblogapplication.models; 2 | 3 | import jakarta.validation.constraints.NotNull; 4 | import lombok.*; 5 | 6 | import jakarta.persistence.*; 7 | 8 | import java.time.LocalDateTime; 9 | 10 | @Entity 11 | @Getter 12 | @Setter 13 | @NoArgsConstructor 14 | @ToString 15 | @Builder 16 | @AllArgsConstructor 17 | public class Post { 18 | 19 | @Id 20 | @GeneratedValue(strategy = GenerationType.SEQUENCE) 21 | Long id; 22 | 23 | String title; 24 | 25 | @Column(columnDefinition = "TEXT") 26 | String body; 27 | 28 | LocalDateTime createdAt; 29 | 30 | LocalDateTime updatedAt; 31 | 32 | String imageFilePath; 33 | 34 | @NotNull 35 | @ManyToOne 36 | @JoinColumn(name = "account_id", referencedColumnName = "id", nullable = false) 37 | Account account; 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/example/springbootblogapplication/repositories/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.springbootblogapplication.repositories; 2 | 3 | import com.example.springbootblogapplication.models.Account; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import java.util.Optional; 8 | 9 | @Repository 10 | public interface AccountRepository extends JpaRepository { 11 | 12 | Optional findOneByEmailIgnoreCase(String email); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/example/springbootblogapplication/repositories/AuthorityRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.springbootblogapplication.repositories; 2 | 3 | import com.example.springbootblogapplication.models.Authority; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface AuthorityRepository extends JpaRepository {} 9 | -------------------------------------------------------------------------------- /src/main/java/com/example/springbootblogapplication/repositories/PostRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.springbootblogapplication.repositories; 2 | 3 | import com.example.springbootblogapplication.models.Post; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface PostRepository extends JpaRepository { 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/main/java/com/example/springbootblogapplication/services/AccountService.java: -------------------------------------------------------------------------------- 1 | package com.example.springbootblogapplication.services; 2 | 3 | import com.example.springbootblogapplication.models.Account; 4 | import com.example.springbootblogapplication.models.Authority; 5 | import com.example.springbootblogapplication.repositories.AccountRepository; 6 | import com.example.springbootblogapplication.repositories.AuthorityRepository; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.security.crypto.password.PasswordEncoder; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.time.LocalDateTime; 12 | import java.util.HashSet; 13 | import java.util.Optional; 14 | import java.util.Set; 15 | 16 | @Service 17 | @RequiredArgsConstructor 18 | public class AccountService { 19 | 20 | private final PasswordEncoder passwordEncoder; 21 | private final AccountRepository accountRepository; 22 | private final AuthorityRepository authorityRepository; 23 | 24 | public Account save(Account account) { 25 | 26 | if (account.getId() == null) { 27 | if (account.getAuthorities().isEmpty()) { 28 | Set authorities = new HashSet<>(); 29 | authorityRepository.findById("ROLE_USER").ifPresent(authorities::add); 30 | account.setAuthorities(authorities); 31 | } 32 | account.setCreatedAt(LocalDateTime.now()); 33 | } 34 | account.setUpdatedAt(LocalDateTime.now()); 35 | account.setPassword(passwordEncoder.encode(account.getPassword())); 36 | return accountRepository.save(account); 37 | } 38 | 39 | public Optional findOneByEmail(String email) { 40 | return accountRepository.findOneByEmailIgnoreCase(email); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/example/springbootblogapplication/services/FileService.java: -------------------------------------------------------------------------------- 1 | package com.example.springbootblogapplication.services; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.stereotype.Service; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.core.io.Resource; 7 | import org.springframework.core.io.UrlResource; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.util.FileSystemUtils; 10 | import org.springframework.web.multipart.MultipartFile; 11 | 12 | import java.io.IOException; 13 | import java.net.MalformedURLException; 14 | import java.nio.file.FileAlreadyExistsException; 15 | import java.nio.file.Files; 16 | import java.nio.file.Path; 17 | import java.nio.file.Paths; 18 | import java.util.stream.Stream; 19 | 20 | @Service 21 | @RequiredArgsConstructor 22 | public class FileService { 23 | private final Path root = Paths.get("./uploads"); 24 | 25 | public void init() { 26 | try { 27 | Files.createDirectories(root); 28 | } catch (IOException ex) { 29 | throw new RuntimeException("Could not initialize root folder"); 30 | } 31 | } 32 | 33 | public void save(MultipartFile file) { 34 | try { 35 | Files.copy(file.getInputStream(), this.root.resolve(file.getOriginalFilename())); 36 | } catch (Exception ex) { 37 | throw new RuntimeException(ex.getMessage()); 38 | } 39 | } 40 | 41 | public Resource load(String filename) { 42 | if (filename == null) return null; 43 | try { 44 | Path file = root.resolve(filename); 45 | Resource resource = new UrlResource(file.toUri()); 46 | 47 | if (resource.exists() || resource.isReadable()) { 48 | return resource; 49 | } else { 50 | throw new RuntimeException("Could not read the file!"); 51 | } 52 | } catch (MalformedURLException mex) { 53 | throw new RuntimeException("Error: " + mex.getMessage()); 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/example/springbootblogapplication/services/MyUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.example.springbootblogapplication.services; 2 | 3 | import com.example.springbootblogapplication.models.Account; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.security.core.GrantedAuthority; 6 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 7 | import org.springframework.security.core.userdetails.UserDetails; 8 | import org.springframework.security.core.userdetails.UserDetailsService; 9 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 10 | import org.springframework.stereotype.Component; 11 | 12 | import java.util.List; 13 | import java.util.Optional; 14 | import java.util.stream.Collectors; 15 | 16 | @Component("userDetailsService") 17 | @RequiredArgsConstructor 18 | public class MyUserDetailsService implements UserDetailsService { 19 | 20 | private final AccountService accountService; 21 | 22 | @Override 23 | public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { 24 | Optional optionalAccount = accountService.findOneByEmail(email); 25 | if (!optionalAccount.isPresent()) { 26 | throw new UsernameNotFoundException("Account not found"); 27 | } 28 | Account account = optionalAccount.get(); 29 | List grantedAuthorities = account 30 | .getAuthorities() 31 | .stream() 32 | .map(authority -> new SimpleGrantedAuthority(authority.getName())) 33 | .collect(Collectors.toList()); 34 | 35 | return new org.springframework.security.core.userdetails.User(account.getEmail(), account.getPassword(), grantedAuthorities); // (2) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/example/springbootblogapplication/services/PostService.java: -------------------------------------------------------------------------------- 1 | package com.example.springbootblogapplication.services; 2 | 3 | import com.example.springbootblogapplication.models.Post; 4 | import com.example.springbootblogapplication.repositories.PostRepository; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.time.LocalDateTime; 9 | import java.util.List; 10 | import java.util.Optional; 11 | 12 | @Service 13 | @RequiredArgsConstructor 14 | public class PostService { 15 | 16 | private final PostRepository postRepository; 17 | 18 | public Optional getById(Long id) { 19 | return postRepository.findById(id); 20 | } 21 | 22 | public List getAll() { 23 | return postRepository.findAll(); 24 | } 25 | 26 | public Post save(Post post) { 27 | if (post.getId() == null) { 28 | post.setCreatedAt(LocalDateTime.now()); 29 | } 30 | post.setUpdatedAt(LocalDateTime.now()); 31 | return postRepository.save(post); 32 | } 33 | 34 | public void delete(Post post) { 35 | postRepository.delete(post); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # use the default server port 2 | server.port=8080 3 | 4 | # enabling flyway to manage migrations 5 | spring.flyway.enabled=true 6 | 7 | spring.jpa.hibernate.ddl-auto=update 8 | spring.jpa.show-sql=true 9 | spring.jpa.open-in-view=false 10 | 11 | # setup some mysql database configs from .env variables 12 | spring.datasource.url=jdbc:mysql://localhost:3306/${MYSQL_DATABASE} 13 | spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver 14 | spring.datasource.username=${MYSQL_USER} 15 | spring.datasource.password=${MYSQL_PASSWORD} 16 | 17 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V1__initial_migration.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS post 2 | ( 3 | id BIGINT NOT NULL AUTO_INCREMENT, 4 | PRIMARY KEY (id), 5 | title VARCHAR(255) NOT NULL, 6 | body VARCHAR(5000), 7 | created_at TIMESTAMP, 8 | updated_at TIMESTAMP, 9 | account_id BIGINT NOT NULL 10 | ); 11 | 12 | CREATE TABLE IF NOT EXISTS authority 13 | ( 14 | name VARCHAR(16) PRIMARY KEY 15 | ); 16 | 17 | CREATE TABLE IF NOT EXISTS account 18 | ( 19 | id BIGINT NOT NULL AUTO_INCREMENT, 20 | PRIMARY KEY(id), 21 | email VARCHAR(255) NOT NULL, 22 | password VARCHAR(255) NOT NULL, 23 | first_name VARCHAR(255), 24 | last_name VARCHAR(255), 25 | created_at TIMESTAMP, 26 | updated_at TIMESTAMP 27 | ); 28 | 29 | CREATE TABLE IF NOT EXISTS account_authority 30 | ( 31 | account_id BIGINT, 32 | PRIMARY KEY(account_id), 33 | authority_name VARCHAR(16) 34 | ); 35 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V2__add_imageFilePathToPost.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE post ADD COLUMN image_file_path VARCHAR(255) AFTER body; 2 | 3 | -- backfill our sample posts in the db with updated column value 4 | UPDATE post SET image_file_path='pexels-adrien-olichon-16059681.jpg' WHERE id=1; 5 | UPDATE post SET image_file_path='pexels-adrien-olichon-16059681.jpg' WHERE id=2; 6 | -------------------------------------------------------------------------------- /src/main/resources/templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Blog :: 404 8 | 9 | 10 | 11 |
12 |

Sorry, Post Not Found

13 |
14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/resources/templates/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Blog :: 403 8 | 9 | 10 | 11 |
12 |

Sorry, some kind of error happened!

13 |
14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/resources/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | Blog :: Home 9 | 10 | 11 | 12 |
13 |

Spring Boot Blog Application

14 |
15 | 18 |
19 |
20 | 21 |

Title

22 |
Account First Name
23 |
Created At
24 |
Updated At
25 |

body text

26 |
27 |
28 |
29 |
30 | 34 |
35 |
37 |
38 | 39 |
40 | 41 |
42 |
43 | 44 |
45 | 46 | 47 | -------------------------------------------------------------------------------- /src/main/resources/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Blog :: Login 8 | 9 | 10 | 11 |
12 |

Spring Boot Blog Application

13 |
14 | Home 15 |

Login

16 |
19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 | 28 |
29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main/resources/templates/post.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | Blog :: Post 10 | 11 | 12 | 13 |
14 | Home 15 |
16 | 17 |

Title

18 |
Created At
19 |
Updated At
20 |

body text

21 |
22 | 26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/main/resources/templates/post_edit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Blog :: Update Post 8 | 9 | 10 | 11 |
12 | Home 13 |
18 | 19 |

Update Post

20 | 21 |
22 | 23 |
24 |
25 | 26 | 27 |
28 |
29 | 30 | 31 |
32 | 33 |
34 |
35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/main/resources/templates/post_new.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Blog :: New Post 8 | 9 | 10 | 11 |
12 | Home 13 |
18 | 19 |

Write new Post

20 | 21 |
22 | 23 |
24 |
25 | 26 | 27 |
28 |
29 | 30 | 31 |
32 | 33 |
34 |
35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/main/resources/templates/register.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Blog :: Register 8 | 9 | 10 | 11 |
12 |

Spring Boot Blog Application

13 |
14 | Home 15 |

Register New Account

16 |
20 |
21 | 22 | 23 |
24 |
25 | 26 | 27 |
28 |
29 | 30 | 31 |
32 |
33 | 34 | 35 |
36 | 37 |
38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /src/test/java/com/example/springbootblogapplication/SpringBootBlogApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example.springbootblogapplication; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class SpringBootBlogApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /uploads/pexels-adrien-olichon-16059681.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wazooinc/spring-boot-blog-application/cb88d96ed8984afa7ea9b6cfbbbd889165705071/uploads/pexels-adrien-olichon-16059681.jpg --------------------------------------------------------------------------------