├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── secure │ │ └── notes │ │ ├── HelloController.java │ │ ├── NotesApplication.java │ │ ├── config │ │ └── OAuth2LoginSuccessHandler.java │ │ ├── controllers │ │ ├── AdminController.java │ │ ├── AuditLogController.java │ │ ├── AuthController.java │ │ ├── CsrfController.java │ │ └── NoteController.java │ │ ├── dtos │ │ └── UserDTO.java │ │ ├── models │ │ ├── AppRole.java │ │ ├── AuditLog.java │ │ ├── Note.java │ │ ├── PasswordResetToken.java │ │ ├── Role.java │ │ └── User.java │ │ ├── repositories │ │ ├── AuditLogRepository.java │ │ ├── NoteRepository.java │ │ ├── PasswordResetTokenRepository.java │ │ ├── RoleRepository.java │ │ └── UserRepository.java │ │ ├── security │ │ ├── SecurityConfig.java │ │ ├── WebConfig.java │ │ ├── jwt │ │ │ ├── AuthEntryPointJwt.java │ │ │ ├── AuthTokenFilter.java │ │ │ └── JwtUtils.java │ │ ├── request │ │ │ ├── LoginRequest.java │ │ │ └── SignupRequest.java │ │ ├── response │ │ │ ├── LoginResponse.java │ │ │ ├── MessageResponse.java │ │ │ └── UserInfoResponse.java │ │ └── services │ │ │ ├── UserDetailsImpl.java │ │ │ └── UserDetailsServiceImpl.java │ │ ├── services │ │ ├── AuditLogService.java │ │ ├── NoteService.java │ │ ├── TotpService.java │ │ ├── UserService.java │ │ └── impl │ │ │ ├── AuditLogServiceImpl.java │ │ │ ├── NoteServiceImpl.java │ │ │ ├── TotpServiceImpl.java │ │ │ └── UserServiceImpl.java │ │ └── util │ │ ├── AuthUtil.java │ │ └── EmailService.java └── resources │ ├── application-dev.properties │ ├── application-prod.properties │ └── application.properties └── test └── java └── com └── secure └── notes └── NotesApplicationTests.java /.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 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.1 18 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Master Spring Security with the Secure Notes Application 2 | 3 | Welcome to the **Spring Security Masterclass**, where you'll not only learn the fundamentals of Spring Security but also build a fully-functional **Secure Notes Application** hands-on. This course is your gateway to mastering security in Java applications, but the journey doesn’t end here! 4 | 5 | ## 🚀 Continue Your Learning Journey 6 | 7 | Now that you've built a secure application, it's time to expand your skills further. Below are some advanced courses that will help you take your Spring Boot and Java knowledge to the next level. 8 | 9 | ### 🎓 The Ultimate Java and Spring Boot Mastery 10 | 11 | **[Spring Boot By Building Complex Projects Step by Step](https://link.embarkx.com/spring-boot) (47+ Hours of Content)** 12 | After mastering Spring Security, dive into complex projects that will challenge and expand your understanding of Spring Boot. This course is perfect for those looking to build production-grade applications. 13 | 14 | **[Master Spring Boot Microservices](https://link.embarkx.com/microservices) (23+ Hours of Content)** 15 | Transition from monolithic applications to microservices with this comprehensive course. Learn how to design, build, and deploy microservices using Spring Boot and related technologies. 16 | 17 | **[Learn Java with 60+ Hours of Content](http://link.embarkx.com/java) (60+ Hours of Content)** 18 | Strengthen your core Java skills, which are essential for mastering any Java-based framework like Spring. This extensive course covers everything from basics to advanced concepts in Java. 19 | 20 | **[Master Spring Security with React JS + OAuth2](https://link.embarkx.com/spring-security) (23+ Hours of Content)** 21 | Already comfortable with Spring Security? Take it to the next level by integrating it with React and OAuth2. Learn to build modern, secure, full-stack applications. 22 | 23 | **[Master IntelliJ IDEA](http://link.embarkx.com/intellij) (3+ Hours of Content)** 24 | Enhance your productivity by mastering IntelliJ IDEA, the IDE of choice for Java developers. This course will teach you how to maximize your efficiency when developing Java applications. 25 | 26 | ## 🌟 With All Our Courses You Gain Access To 27 | 28 | - 📝 **Notes:** Detailed and downloadable notes to accompany each lesson. 29 | - 💻 **Source Code:** Full access to the source code used in the tutorials. 30 | - 🤔 **Doubt Solving:** Responsive instructor and community support. 31 | - 🎥 **High-Quality HD Videos:** Easy to understand, high-definition video tutorials. 32 | - 🔄 **Free Lifetime Updates:** Continuous updates to course content at no extra cost. 33 | 34 | ## 📚 Why Choose This Mastery Series? 35 | 36 | By enrolling in these courses, you're not just acquiring knowledge; you're preparing to dominate the field of Java and Spring Boot development. Our structured learning path ensures that you build your skills progressively, with each course designed to build on the knowledge gained from the previous one. 37 | 38 | Most of these courses are available on **Udemy For Business**, so if you have a subscription, you can access them for free! 39 | 40 | ## 🔗 Join Us Now! 41 | 42 | Don’t stop at mastering Spring Security—expand your expertise across the entire Spring ecosystem and Java development. **[Enroll in these courses](https://link.embarkx.com/spring-boot)** today and start building your future! 43 | 44 | --- 45 | 46 | **Happy Learning!** 47 | *Faisal Memon & the EmbarkX Team* 48 | 49 | --- 50 | 51 | ### 📬 Contact Information 52 | 53 | - **Website:** [www.embarkx.com](http://www.embarkx.com) 54 | - **Email:** [embarkxofficial@gmail.com](mailto:embarkxofficial@gmail.com) 55 | 56 | 57 | 58 | 59 | # Usage Policy for Course Materials 60 | 61 | ## Instructor Information 62 | 63 | **Instructor:** Faisal Memon 64 | **Company:** [EmbarkX.com](http://www.embarkx.com) 65 | 66 | ## Policy Overview 67 | 68 | This document outlines the guidelines and restrictions concerning the use of course materials provided by EmbarkX, including but not limited to PDF presentations, code samples, and video tutorials. 69 | 70 | ### 1. Personal Use Only 71 | 72 | The materials provided in this course are intended for **your personal use only**. They are to be used solely for the purpose of learning and completing this course. 73 | 74 | ### 2. No Unauthorized Sharing or Distribution 75 | 76 | You are **not permitted** to share, distribute, or publicly post any course materials on any websites, social media platforms, or other public forums without prior written consent from the instructor. 77 | 78 | ### 3. Intellectual Property 79 | 80 | All course materials are protected by copyright laws and are the intellectual property of Faisal Memon and EmbarkX. Unauthorized use, reproduction, or distribution of these materials is **strictly prohibited**. 81 | 82 | ### 4. Reporting Violations 83 | 84 | If you become aware of any unauthorized sharing or distribution of course materials, please report it immediately to [embarkxofficial@gmail.com](mailto:embarkxofficial@gmail.com). 85 | 86 | ### 5. Legal Action 87 | 88 | We reserve the right to take legal action against individuals or entities found to be violating this usage policy. 89 | 90 | ## Thank You 91 | 92 | Thank you for respecting these guidelines and helping us maintain the integrity of our course materials. 93 | 94 | ## Contact Information 95 | 96 | - **Email:** [embarkxofficial@gmail.com](mailto:embarkxofficial@gmail.com) 97 | - **Website:** [www.embarkx.com](http://www.embarkx.com) 98 | -------------------------------------------------------------------------------- /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 | # Apache Maven Wrapper startup batch script, version 3.3.1 23 | # 24 | # Optional ENV vars 25 | # ----------------- 26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source 27 | # MVNW_REPOURL - repo url base for downloading maven distribution 28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output 30 | # ---------------------------------------------------------------------------- 31 | 32 | set -euf 33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x 34 | 35 | # OS specific support. 36 | native_path() { printf %s\\n "$1"; } 37 | case "$(uname)" in 38 | CYGWIN* | MINGW*) 39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" 40 | native_path() { cygpath --path --windows "$1"; } 41 | ;; 42 | esac 43 | 44 | # set JAVACMD and JAVACCMD 45 | set_java_home() { 46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched 47 | if [ -n "${JAVA_HOME-}" ]; then 48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 49 | # IBM's JDK on AIX uses strange locations for the executables 50 | JAVACMD="$JAVA_HOME/jre/sh/java" 51 | JAVACCMD="$JAVA_HOME/jre/sh/javac" 52 | else 53 | JAVACMD="$JAVA_HOME/bin/java" 54 | JAVACCMD="$JAVA_HOME/bin/javac" 55 | 56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then 57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 59 | return 1 60 | fi 61 | fi 62 | else 63 | JAVACMD="$( 64 | 'set' +e 65 | 'unset' -f command 2>/dev/null 66 | 'command' -v java 67 | )" || : 68 | JAVACCMD="$( 69 | 'set' +e 70 | 'unset' -f command 2>/dev/null 71 | 'command' -v javac 72 | )" || : 73 | 74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then 75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 76 | return 1 77 | fi 78 | fi 79 | } 80 | 81 | # hash string like Java String::hashCode 82 | hash_string() { 83 | str="${1:-}" h=0 84 | while [ -n "$str" ]; do 85 | char="${str%"${str#?}"}" 86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) 87 | str="${str#?}" 88 | done 89 | printf %x\\n $h 90 | } 91 | 92 | verbose() { :; } 93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } 94 | 95 | die() { 96 | printf %s\\n "$1" >&2 97 | exit 1 98 | } 99 | 100 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties 101 | while IFS="=" read -r key value; do 102 | case "${key-}" in 103 | distributionUrl) distributionUrl="${value-}" ;; 104 | distributionSha256Sum) distributionSha256Sum="${value-}" ;; 105 | esac 106 | done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" 107 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" 108 | 109 | case "${distributionUrl##*/}" in 110 | maven-mvnd-*bin.*) 111 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ 112 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in 113 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; 114 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; 115 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; 116 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;; 117 | *) 118 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 119 | distributionPlatform=linux-amd64 120 | ;; 121 | esac 122 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" 123 | ;; 124 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; 125 | *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; 126 | esac 127 | 128 | # apply MVNW_REPOURL and calculate MAVEN_HOME 129 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 130 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" 131 | distributionUrlName="${distributionUrl##*/}" 132 | distributionUrlNameMain="${distributionUrlName%.*}" 133 | distributionUrlNameMain="${distributionUrlNameMain%-bin}" 134 | MAVEN_HOME="$HOME/.m2/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" 135 | 136 | exec_maven() { 137 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : 138 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" 139 | } 140 | 141 | if [ -d "$MAVEN_HOME" ]; then 142 | verbose "found existing MAVEN_HOME at $MAVEN_HOME" 143 | exec_maven "$@" 144 | fi 145 | 146 | case "${distributionUrl-}" in 147 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; 148 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; 149 | esac 150 | 151 | # prepare tmp dir 152 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then 153 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } 154 | trap clean HUP INT TERM EXIT 155 | else 156 | die "cannot create temp dir" 157 | fi 158 | 159 | mkdir -p -- "${MAVEN_HOME%/*}" 160 | 161 | # Download and Install Apache Maven 162 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 163 | verbose "Downloading from: $distributionUrl" 164 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 165 | 166 | # select .zip or .tar.gz 167 | if ! command -v unzip >/dev/null; then 168 | distributionUrl="${distributionUrl%.zip}.tar.gz" 169 | distributionUrlName="${distributionUrl##*/}" 170 | fi 171 | 172 | # verbose opt 173 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' 174 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v 175 | 176 | # normalize http auth 177 | case "${MVNW_PASSWORD:+has-password}" in 178 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; 179 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; 180 | esac 181 | 182 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then 183 | verbose "Found wget ... using wget" 184 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" 185 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then 186 | verbose "Found curl ... using curl" 187 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" 188 | elif set_java_home; then 189 | verbose "Falling back to use Java to download" 190 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" 191 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" 192 | cat >"$javaSource" <<-END 193 | public class Downloader extends java.net.Authenticator 194 | { 195 | protected java.net.PasswordAuthentication getPasswordAuthentication() 196 | { 197 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); 198 | } 199 | public static void main( String[] args ) throws Exception 200 | { 201 | setDefault( new Downloader() ); 202 | java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); 203 | } 204 | } 205 | END 206 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java 207 | verbose " - Compiling Downloader.java ..." 208 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" 209 | verbose " - Running Downloader.java ..." 210 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" 211 | fi 212 | 213 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 214 | if [ -n "${distributionSha256Sum-}" ]; then 215 | distributionSha256Result=false 216 | if [ "$MVN_CMD" = mvnd.sh ]; then 217 | echo "Checksum validation is not supported for maven-mvnd." >&2 218 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 219 | exit 1 220 | elif command -v sha256sum >/dev/null; then 221 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then 222 | distributionSha256Result=true 223 | fi 224 | elif command -v shasum >/dev/null; then 225 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then 226 | distributionSha256Result=true 227 | fi 228 | else 229 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 230 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 231 | exit 1 232 | fi 233 | if [ $distributionSha256Result = false ]; then 234 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 235 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 236 | exit 1 237 | fi 238 | fi 239 | 240 | # unzip and move 241 | if command -v unzip >/dev/null; then 242 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" 243 | else 244 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" 245 | fi 246 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" 247 | mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" 248 | 249 | clean || : 250 | exec_maven "$@" 251 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM https://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.1 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @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 @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 83 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 84 | 85 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 86 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 87 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 88 | exit $? 89 | } 90 | 91 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 92 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 93 | } 94 | 95 | # prepare tmp dir 96 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 97 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 98 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 99 | trap { 100 | if ($TMP_DOWNLOAD_DIR.Exists) { 101 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 102 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 103 | } 104 | } 105 | 106 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 107 | 108 | # Download and Install Apache Maven 109 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 110 | Write-Verbose "Downloading from: $distributionUrl" 111 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 112 | 113 | $webclient = New-Object System.Net.WebClient 114 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 115 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 116 | } 117 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 118 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 119 | 120 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 121 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 122 | if ($distributionSha256Sum) { 123 | if ($USE_MVND) { 124 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 125 | } 126 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 127 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 128 | 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." 129 | } 130 | } 131 | 132 | # unzip and move 133 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 134 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 135 | try { 136 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 137 | } catch { 138 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 139 | Write-Error "fail to move MAVEN_HOME" 140 | } 141 | } finally { 142 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 143 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 144 | } 145 | 146 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 147 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.3.0 9 | 10 | 11 | com.secure 12 | notes 13 | 0.0.1-SNAPSHOT 14 | notes 15 | Secure Note Project 16 | 17 | 17 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-security 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-web 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-test 32 | test 33 | 34 | 35 | org.springframework.security 36 | spring-security-test 37 | test 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-data-jpa 42 | 43 | 44 | com.mysql 45 | mysql-connector-j 46 | runtime 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-validation 51 | 52 | 53 | org.projectlombok 54 | lombok 55 | true 56 | 57 | 58 | io.jsonwebtoken 59 | jjwt-api 60 | 0.12.6 61 | 62 | 63 | io.jsonwebtoken 64 | jjwt-impl 65 | 0.12.6 66 | runtime 67 | 68 | 69 | io.jsonwebtoken 70 | jjwt-jackson 71 | 0.12.6 72 | runtime 73 | 74 | 75 | org.springframework.boot 76 | spring-boot-starter-mail 77 | 78 | 79 | org.springframework.boot 80 | spring-boot-starter-oauth2-client 81 | 82 | 83 | com.warrenstrange 84 | googleauth 85 | 1.4.0 86 | 87 | 88 | 89 | 90 | 91 | 92 | org.springframework.boot 93 | spring-boot-maven-plugin 94 | 95 | 96 | 97 | org.projectlombok 98 | lombok 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/HelloController.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes; 2 | 3 | import org.springframework.web.bind.annotation.GetMapping; 4 | import org.springframework.web.bind.annotation.RestController; 5 | 6 | @RestController 7 | public class HelloController { 8 | @GetMapping("/hello") 9 | public String sayHello(){ 10 | return "Hello"; 11 | } 12 | 13 | @GetMapping("/contact") 14 | public String sayContact(){ 15 | return "Contact"; 16 | } 17 | 18 | @GetMapping("/hi") 19 | public String sayHi(){ 20 | return "Hi"; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/NotesApplication.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class NotesApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(NotesApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/config/OAuth2LoginSuccessHandler.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.config; 2 | 3 | import com.secure.notes.models.AppRole; 4 | import com.secure.notes.models.Role; 5 | import com.secure.notes.models.User; 6 | import com.secure.notes.repositories.RoleRepository; 7 | import com.secure.notes.security.jwt.JwtUtils; 8 | import com.secure.notes.security.services.UserDetailsImpl; 9 | import com.secure.notes.services.UserService; 10 | import jakarta.servlet.ServletException; 11 | import jakarta.servlet.http.HttpServletRequest; 12 | import jakarta.servlet.http.HttpServletResponse; 13 | import lombok.RequiredArgsConstructor; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.beans.factory.annotation.Value; 16 | import org.springframework.security.core.Authentication; 17 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 18 | import org.springframework.security.core.context.SecurityContextHolder; 19 | import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; 20 | import org.springframework.security.oauth2.core.user.DefaultOAuth2User; 21 | import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; 22 | import org.springframework.stereotype.Component; 23 | import org.springframework.web.util.UriComponentsBuilder; 24 | 25 | import java.io.IOException; 26 | import java.util.*; 27 | import java.util.stream.Collectors; 28 | 29 | @Component 30 | @RequiredArgsConstructor 31 | public class OAuth2LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { 32 | 33 | @Autowired 34 | private final UserService userService; 35 | 36 | @Autowired 37 | private final JwtUtils jwtUtils; 38 | 39 | @Autowired 40 | RoleRepository roleRepository; 41 | 42 | @Value("${frontend.url}") 43 | private String frontendUrl; 44 | 45 | String username; 46 | String idAttributeKey; 47 | 48 | @Override 49 | public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { 50 | OAuth2AuthenticationToken oAuth2AuthenticationToken = (OAuth2AuthenticationToken) authentication; 51 | if ("github".equals(oAuth2AuthenticationToken.getAuthorizedClientRegistrationId()) || "google".equals(oAuth2AuthenticationToken.getAuthorizedClientRegistrationId())) { 52 | DefaultOAuth2User principal = (DefaultOAuth2User) authentication.getPrincipal(); 53 | Map attributes = principal.getAttributes(); 54 | String email = attributes.getOrDefault("email", "").toString(); 55 | String name = attributes.getOrDefault("name", "").toString(); 56 | if ("github".equals(oAuth2AuthenticationToken.getAuthorizedClientRegistrationId())) { 57 | username = attributes.getOrDefault("login", "").toString(); 58 | idAttributeKey = "id"; 59 | } else if ("google".equals(oAuth2AuthenticationToken.getAuthorizedClientRegistrationId())) { 60 | username = email.split("@")[0]; 61 | idAttributeKey = "sub"; 62 | } else { 63 | username = ""; 64 | idAttributeKey = "id"; 65 | } 66 | System.out.println("HELLO OAUTH: " + email + " : " + name + " : " + username); 67 | 68 | userService.findByEmail(email) 69 | .ifPresentOrElse(user -> { 70 | DefaultOAuth2User oauthUser = new DefaultOAuth2User( 71 | List.of(new SimpleGrantedAuthority(user.getRole().getRoleName().name())), 72 | attributes, 73 | idAttributeKey 74 | ); 75 | Authentication securityAuth = new OAuth2AuthenticationToken( 76 | oauthUser, 77 | List.of(new SimpleGrantedAuthority(user.getRole().getRoleName().name())), 78 | oAuth2AuthenticationToken.getAuthorizedClientRegistrationId() 79 | ); 80 | SecurityContextHolder.getContext().setAuthentication(securityAuth); 81 | }, () -> { 82 | User newUser = new User(); 83 | Optional userRole = roleRepository.findByRoleName(AppRole.ROLE_USER); // Fetch existing role 84 | if (userRole.isPresent()) { 85 | newUser.setRole(userRole.get()); // Set existing role 86 | } else { 87 | // Handle the case where the role is not found 88 | throw new RuntimeException("Default role not found"); 89 | } 90 | newUser.setEmail(email); 91 | newUser.setUserName(username); 92 | newUser.setSignUpMethod(oAuth2AuthenticationToken.getAuthorizedClientRegistrationId()); 93 | userService.registerUser(newUser); 94 | DefaultOAuth2User oauthUser = new DefaultOAuth2User( 95 | List.of(new SimpleGrantedAuthority(newUser.getRole().getRoleName().name())), 96 | attributes, 97 | idAttributeKey 98 | ); 99 | Authentication securityAuth = new OAuth2AuthenticationToken( 100 | oauthUser, 101 | List.of(new SimpleGrantedAuthority(newUser.getRole().getRoleName().name())), 102 | oAuth2AuthenticationToken.getAuthorizedClientRegistrationId() 103 | ); 104 | SecurityContextHolder.getContext().setAuthentication(securityAuth); 105 | }); 106 | } 107 | this.setAlwaysUseDefaultTargetUrl(true); 108 | 109 | // JWT TOKEN LOGIC 110 | DefaultOAuth2User oauth2User = (DefaultOAuth2User) authentication.getPrincipal(); 111 | Map attributes = oauth2User.getAttributes(); 112 | 113 | // Extract necessary attributes 114 | String email = (String) attributes.get("email"); 115 | System.out.println("OAuth2LoginSuccessHandler: " + username + " : " + email); 116 | 117 | Set authorities = new HashSet<>(oauth2User.getAuthorities().stream() 118 | .map(authority -> new SimpleGrantedAuthority(authority.getAuthority())) 119 | .collect(Collectors.toList())); 120 | User user = userService.findByEmail(email).orElseThrow( 121 | () -> new RuntimeException("User not found")); 122 | authorities.add(new SimpleGrantedAuthority(user.getRole().getRoleName().name())); 123 | 124 | // Create UserDetailsImpl instance 125 | UserDetailsImpl userDetails = new UserDetailsImpl( 126 | null, 127 | username, 128 | email, 129 | null, 130 | false, 131 | authorities 132 | ); 133 | 134 | // Generate JWT token 135 | String jwtToken = jwtUtils.generateTokenFromUsername(userDetails); 136 | 137 | // Redirect to the frontend with the JWT token 138 | String targetUrl = UriComponentsBuilder.fromUriString(frontendUrl + "/oauth2/redirect") 139 | .queryParam("token", jwtToken) 140 | .build().toUriString(); 141 | this.setDefaultTargetUrl(targetUrl); 142 | super.onAuthenticationSuccess(request, response, authentication); 143 | } 144 | } -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/controllers/AdminController.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.controllers; 2 | 3 | import com.secure.notes.dtos.UserDTO; 4 | import com.secure.notes.models.Role; 5 | import com.secure.notes.models.User; 6 | import com.secure.notes.services.UserService; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.security.access.prepost.PreAuthorize; 11 | import org.springframework.web.bind.annotation.*; 12 | 13 | import java.util.List; 14 | 15 | @RestController 16 | @RequestMapping("/api/admin") 17 | //@PreAuthorize("hasRole('ROLE_ADMIN')") 18 | public class AdminController { 19 | 20 | @Autowired 21 | UserService userService; 22 | 23 | @GetMapping("/getusers") 24 | public ResponseEntity> getAllUsers() { 25 | return new ResponseEntity<>(userService.getAllUsers(), 26 | HttpStatus.OK); 27 | } 28 | 29 | @PutMapping("/update-role") 30 | public ResponseEntity updateUserRole(@RequestParam Long userId, 31 | @RequestParam String roleName) { 32 | userService.updateUserRole(userId, roleName); 33 | return ResponseEntity.ok("User role updated"); 34 | } 35 | 36 | @GetMapping("/user/{id}") 37 | public ResponseEntity getUser(@PathVariable Long id) { 38 | return new ResponseEntity<>(userService.getUserById(id), 39 | HttpStatus.OK); 40 | } 41 | 42 | @PutMapping("/update-lock-status") 43 | public ResponseEntity updateAccountLockStatus(@RequestParam Long userId, 44 | @RequestParam boolean lock) { 45 | userService.updateAccountLockStatus(userId, lock); 46 | return ResponseEntity.ok("Account lock status updated"); 47 | } 48 | 49 | @GetMapping("/roles") 50 | public List getAllRoles() { 51 | return userService.getAllRoles(); 52 | } 53 | 54 | @PutMapping("/update-expiry-status") 55 | public ResponseEntity updateAccountExpiryStatus(@RequestParam Long userId, 56 | @RequestParam boolean expire) { 57 | userService.updateAccountExpiryStatus(userId, expire); 58 | return ResponseEntity.ok("Account expiry status updated"); 59 | } 60 | 61 | @PutMapping("/update-enabled-status") 62 | public ResponseEntity updateAccountEnabledStatus(@RequestParam Long userId, 63 | @RequestParam boolean enabled) { 64 | userService.updateAccountEnabledStatus(userId, enabled); 65 | return ResponseEntity.ok("Account enabled status updated"); 66 | } 67 | 68 | @PutMapping("/update-credentials-expiry-status") 69 | public ResponseEntity updateCredentialsExpiryStatus(@RequestParam Long userId, @RequestParam boolean expire) { 70 | userService.updateCredentialsExpiryStatus(userId, expire); 71 | return ResponseEntity.ok("Credentials expiry status updated"); 72 | } 73 | 74 | @PutMapping("/update-password") 75 | public ResponseEntity updatePassword(@RequestParam Long userId, 76 | @RequestParam String password) { 77 | try { 78 | userService.updatePassword(userId, password); 79 | return ResponseEntity.ok("Password updated"); 80 | } catch (RuntimeException e) { 81 | return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); 82 | } 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/controllers/AuditLogController.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.controllers; 2 | 3 | import com.secure.notes.models.AuditLog; 4 | import com.secure.notes.services.AuditLogService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.security.access.prepost.PreAuthorize; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import java.util.List; 13 | 14 | @RestController 15 | @RequestMapping("/api/audit") 16 | public class AuditLogController { 17 | @Autowired 18 | AuditLogService auditLogService; 19 | 20 | @GetMapping 21 | @PreAuthorize("hasRole('ROLE_ADMIN')") 22 | public List getAuditLogs(){ 23 | return auditLogService.getAllAuditLogs(); 24 | } 25 | 26 | @GetMapping("/note/{id}") 27 | @PreAuthorize("hasRole('ROLE_ADMIN')") 28 | public List getNoteAuditLogs(@PathVariable Long id){ 29 | return auditLogService.getAuditLogsForNoteId(id); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/controllers/AuthController.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.controllers; 2 | 3 | import com.secure.notes.models.AppRole; 4 | import com.secure.notes.models.Role; 5 | import com.secure.notes.models.User; 6 | import com.secure.notes.repositories.RoleRepository; 7 | import com.secure.notes.repositories.UserRepository; 8 | import com.secure.notes.security.jwt.JwtUtils; 9 | import com.secure.notes.security.request.LoginRequest; 10 | import com.secure.notes.security.request.SignupRequest; 11 | import com.secure.notes.security.response.LoginResponse; 12 | import com.secure.notes.security.response.MessageResponse; 13 | import com.secure.notes.security.response.UserInfoResponse; 14 | import com.secure.notes.security.services.UserDetailsImpl; 15 | import com.secure.notes.services.TotpService; 16 | import com.secure.notes.services.UserService; 17 | import com.secure.notes.util.AuthUtil; 18 | import com.warrenstrange.googleauth.GoogleAuthenticatorKey; 19 | import jakarta.validation.Valid; 20 | import org.springframework.beans.factory.annotation.Autowired; 21 | import org.springframework.http.HttpStatus; 22 | import org.springframework.http.ResponseEntity; 23 | import org.springframework.security.authentication.AuthenticationManager; 24 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 25 | import org.springframework.security.core.Authentication; 26 | import org.springframework.security.core.AuthenticationException; 27 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 28 | import org.springframework.security.core.context.SecurityContextHolder; 29 | import org.springframework.security.core.userdetails.UserDetails; 30 | import org.springframework.security.crypto.password.PasswordEncoder; 31 | import org.springframework.web.bind.annotation.*; 32 | 33 | import java.time.LocalDate; 34 | import java.util.HashMap; 35 | import java.util.List; 36 | import java.util.Map; 37 | import java.util.Set; 38 | import java.util.stream.Collectors; 39 | 40 | @RestController 41 | @RequestMapping("/api/auth") 42 | public class AuthController { 43 | 44 | @Autowired 45 | JwtUtils jwtUtils; 46 | 47 | @Autowired 48 | AuthenticationManager authenticationManager; 49 | 50 | @Autowired 51 | UserRepository userRepository; 52 | 53 | @Autowired 54 | RoleRepository roleRepository; 55 | 56 | @Autowired 57 | PasswordEncoder encoder; 58 | 59 | @Autowired 60 | UserService userService; 61 | 62 | @Autowired 63 | AuthUtil authUtil; 64 | 65 | @Autowired 66 | TotpService totpService; 67 | 68 | @PostMapping("/public/signin") 69 | public ResponseEntity authenticateUser(@RequestBody LoginRequest loginRequest) { 70 | Authentication authentication; 71 | try { 72 | authentication = authenticationManager 73 | .authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())); 74 | } catch (AuthenticationException exception) { 75 | Map map = new HashMap<>(); 76 | map.put("message", "Bad credentials"); 77 | map.put("status", false); 78 | return new ResponseEntity(map, HttpStatus.NOT_FOUND); 79 | } 80 | 81 | // Set the authentication 82 | SecurityContextHolder.getContext().setAuthentication(authentication); 83 | 84 | UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); 85 | 86 | String jwtToken = jwtUtils.generateTokenFromUsername(userDetails); 87 | 88 | // Collect roles from the UserDetails 89 | List roles = userDetails.getAuthorities().stream() 90 | .map(item -> item.getAuthority()) 91 | .collect(Collectors.toList()); 92 | 93 | // Prepare the response body, now including the JWT token directly in the body 94 | LoginResponse response = new LoginResponse(userDetails.getUsername(), 95 | roles, jwtToken); 96 | 97 | // Return the response entity with the JWT token included in the response body 98 | return ResponseEntity.ok(response); 99 | } 100 | 101 | 102 | @PostMapping("/public/signup") 103 | public ResponseEntity registerUser(@Valid @RequestBody SignupRequest signUpRequest) { 104 | if (userRepository.existsByUserName(signUpRequest.getUsername())) { 105 | return ResponseEntity.badRequest().body(new MessageResponse("Error: Username is already taken!")); 106 | } 107 | 108 | if (userRepository.existsByEmail(signUpRequest.getEmail())) { 109 | return ResponseEntity.badRequest().body(new MessageResponse("Error: Email is already in use!")); 110 | } 111 | 112 | // Create new user's account 113 | User user = new User(signUpRequest.getUsername(), 114 | signUpRequest.getEmail(), 115 | encoder.encode(signUpRequest.getPassword())); 116 | 117 | Set strRoles = signUpRequest.getRole(); 118 | Role role; 119 | 120 | if (strRoles == null || strRoles.isEmpty()) { 121 | role = roleRepository.findByRoleName(AppRole.ROLE_USER) 122 | .orElseThrow(() -> new RuntimeException("Error: Role is not found.")); 123 | } else { 124 | String roleStr = strRoles.iterator().next(); 125 | if (roleStr.equals("admin")) { 126 | role = roleRepository.findByRoleName(AppRole.ROLE_ADMIN) 127 | .orElseThrow(() -> new RuntimeException("Error: Role is not found.")); 128 | } else { 129 | role = roleRepository.findByRoleName(AppRole.ROLE_USER) 130 | .orElseThrow(() -> new RuntimeException("Error: Role is not found.")); 131 | } 132 | 133 | user.setAccountNonLocked(true); 134 | user.setAccountNonExpired(true); 135 | user.setCredentialsNonExpired(true); 136 | user.setEnabled(true); 137 | user.setCredentialsExpiryDate(LocalDate.now().plusYears(1)); 138 | user.setAccountExpiryDate(LocalDate.now().plusYears(1)); 139 | user.setTwoFactorEnabled(false); 140 | user.setSignUpMethod("email"); 141 | } 142 | user.setRole(role); 143 | userRepository.save(user); 144 | 145 | return ResponseEntity.ok(new MessageResponse("User registered successfully!")); 146 | } 147 | 148 | 149 | @GetMapping("/user") 150 | public ResponseEntity getUserDetails(@AuthenticationPrincipal UserDetails userDetails) { 151 | User user = userService.findByUsername(userDetails.getUsername()); 152 | 153 | List roles = userDetails.getAuthorities().stream() 154 | .map(item -> item.getAuthority()) 155 | .collect(Collectors.toList()); 156 | 157 | UserInfoResponse response = new UserInfoResponse( 158 | user.getUserId(), 159 | user.getUserName(), 160 | user.getEmail(), 161 | user.isAccountNonLocked(), 162 | user.isAccountNonExpired(), 163 | user.isCredentialsNonExpired(), 164 | user.isEnabled(), 165 | user.getCredentialsExpiryDate(), 166 | user.getAccountExpiryDate(), 167 | user.isTwoFactorEnabled(), 168 | roles 169 | ); 170 | 171 | return ResponseEntity.ok().body(response); 172 | } 173 | 174 | @GetMapping("/username") 175 | public String currentUserName(@AuthenticationPrincipal UserDetails userDetails) { 176 | return (userDetails != null) ? userDetails.getUsername() : ""; 177 | } 178 | 179 | @PostMapping("/public/forgot-password") 180 | public ResponseEntity forgotPassword(@RequestParam String email) { 181 | try { 182 | userService.generatePasswordResetToken(email); 183 | return ResponseEntity.ok(new MessageResponse("Password reset email sent!")); 184 | } catch (Exception e) { 185 | e.printStackTrace(); 186 | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) 187 | .body(new MessageResponse("Error sending password reset email")); 188 | } 189 | 190 | } 191 | 192 | @PostMapping("/public/reset-password") 193 | public ResponseEntity resetPassword(@RequestParam String token, 194 | @RequestParam String newPassword) { 195 | 196 | try { 197 | userService.resetPassword(token, newPassword); 198 | return ResponseEntity.ok(new MessageResponse("Password reset successful")); 199 | } catch (RuntimeException e) { 200 | return ResponseEntity.status(HttpStatus.BAD_REQUEST) 201 | .body(new MessageResponse(e.getMessage())); 202 | } 203 | } 204 | 205 | // 2FA Authentication 206 | @PostMapping("/enable-2fa") 207 | public ResponseEntity enable2FA() { 208 | Long userId = authUtil.loggedInUserId(); 209 | GoogleAuthenticatorKey secret = userService.generate2FASecret(userId); 210 | String qrCodeUrl = totpService.getQrCodeUrl(secret, 211 | userService.getUserById(userId).getUserName()); 212 | return ResponseEntity.ok(qrCodeUrl); 213 | } 214 | 215 | @PostMapping("/disable-2fa") 216 | public ResponseEntity disable2FA() { 217 | Long userId = authUtil.loggedInUserId(); 218 | userService.disable2FA(userId); 219 | return ResponseEntity.ok("2FA disabled"); 220 | } 221 | 222 | 223 | @PostMapping("/verify-2fa") 224 | public ResponseEntity verify2FA(@RequestParam int code) { 225 | Long userId = authUtil.loggedInUserId(); 226 | boolean isValid = userService.validate2FACode(userId, code); 227 | if (isValid) { 228 | userService.enable2FA(userId); 229 | return ResponseEntity.ok("2FA Verified"); 230 | } else { 231 | return ResponseEntity.status(HttpStatus.UNAUTHORIZED) 232 | .body("Invalid 2FA Code"); 233 | } 234 | } 235 | 236 | 237 | @GetMapping("/user/2fa-status") 238 | public ResponseEntity get2FAStatus() { 239 | User user = authUtil.loggedInUser(); 240 | if (user != null){ 241 | return ResponseEntity.ok().body(Map.of("is2faEnabled", user.isTwoFactorEnabled())); 242 | } else { 243 | return ResponseEntity.status(HttpStatus.NOT_FOUND) 244 | .body("User not found"); 245 | } 246 | } 247 | 248 | 249 | @PostMapping("/public/verify-2fa-login") 250 | public ResponseEntity verify2FALogin(@RequestParam int code, 251 | @RequestParam String jwtToken) { 252 | String username = jwtUtils.getUserNameFromJwtToken(jwtToken); 253 | User user = userService.findByUsername(username); 254 | boolean isValid = userService.validate2FACode(user.getUserId(), code); 255 | if (isValid) { 256 | return ResponseEntity.ok("2FA Verified"); 257 | } else { 258 | return ResponseEntity.status(HttpStatus.UNAUTHORIZED) 259 | .body("Invalid 2FA Code"); 260 | } 261 | } 262 | 263 | } -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/controllers/CsrfController.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.controllers; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import org.springframework.security.web.csrf.CsrfToken; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | @RestController 9 | public class CsrfController { 10 | 11 | @GetMapping("/api/csrf-token") 12 | public CsrfToken csrfToken(HttpServletRequest request) { 13 | return (CsrfToken) request.getAttribute(CsrfToken.class.getName()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/controllers/NoteController.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.controllers; 2 | 3 | import com.secure.notes.models.Note; 4 | import com.secure.notes.services.NoteService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 7 | import org.springframework.security.core.userdetails.UserDetails; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | import java.util.List; 11 | 12 | @RestController 13 | @RequestMapping("/api/notes") 14 | public class NoteController { 15 | 16 | @Autowired 17 | private NoteService noteService; 18 | 19 | @PostMapping 20 | public Note createNote(@RequestBody String content, 21 | @AuthenticationPrincipal UserDetails userDetails) { 22 | String username = userDetails.getUsername(); 23 | System.out.println("USER DETAILS: " + username); 24 | return noteService.createNoteForUser(username, content); 25 | } 26 | 27 | @GetMapping 28 | public List getUserNotes(@AuthenticationPrincipal UserDetails userDetails) { 29 | String username = userDetails.getUsername(); 30 | System.out.println("USER DETAILS: " + username); 31 | return noteService.getNotesForUser(username); 32 | } 33 | 34 | @PutMapping("/{noteId}") 35 | public Note updateNote(@PathVariable Long noteId, 36 | @RequestBody String content, 37 | @AuthenticationPrincipal UserDetails userDetails) { 38 | String username = userDetails.getUsername(); 39 | return noteService.updateNoteForUser(noteId, content, username); 40 | } 41 | 42 | @DeleteMapping("/{noteId}") 43 | public void deleteNote(@PathVariable Long noteId, 44 | @AuthenticationPrincipal UserDetails userDetails) { 45 | String username = userDetails.getUsername(); 46 | noteService.deleteNoteForUser(noteId, username); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/dtos/UserDTO.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.dtos; 2 | 3 | import com.secure.notes.models.Role; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.time.LocalDate; 9 | import java.time.LocalDateTime; 10 | 11 | 12 | @Data 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class UserDTO { 16 | private Long userId; 17 | private String userName; 18 | private String email; 19 | private boolean accountNonLocked; 20 | private boolean accountNonExpired; 21 | private boolean credentialsNonExpired; 22 | private boolean enabled; 23 | private LocalDate credentialsExpiryDate; 24 | private LocalDate accountExpiryDate; 25 | private String twoFactorSecret; 26 | private boolean isTwoFactorEnabled; 27 | private String signUpMethod; 28 | private Role role; 29 | private LocalDateTime createdDate; 30 | private LocalDateTime updatedDate; 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/models/AppRole.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.models; 2 | 3 | public enum AppRole { 4 | ROLE_USER, 5 | ROLE_ADMIN 6 | } 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/models/AuditLog.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.models; 2 | 3 | import jakarta.persistence.Entity; 4 | import jakarta.persistence.GeneratedValue; 5 | import jakarta.persistence.GenerationType; 6 | import jakarta.persistence.Id; 7 | import lombok.Data; 8 | 9 | import java.time.LocalDateTime; 10 | 11 | @Entity 12 | @Data 13 | public class AuditLog { 14 | @Id 15 | @GeneratedValue(strategy = GenerationType.IDENTITY) 16 | private Long id; 17 | private String action; 18 | private String username; 19 | private Long noteId; 20 | private String noteContent; 21 | private LocalDateTime timestamp; 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/models/Note.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.models; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.Data; 5 | 6 | @Entity 7 | @Data 8 | public class Note { 9 | @Id 10 | @GeneratedValue(strategy = GenerationType.IDENTITY) 11 | private Long id; 12 | 13 | @Lob 14 | private String content; 15 | 16 | private String ownerUsername; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/models/PasswordResetToken.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.models; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.time.Instant; 8 | 9 | @Entity 10 | @Data 11 | @NoArgsConstructor 12 | public class PasswordResetToken { 13 | @Id 14 | @GeneratedValue(strategy = GenerationType.IDENTITY) 15 | private Long id; 16 | 17 | @Column(nullable = false, unique = true) 18 | private String token; 19 | 20 | @Column(nullable = false) 21 | private Instant expiryDate; 22 | 23 | private boolean used; 24 | 25 | @ManyToOne(fetch = FetchType.LAZY) 26 | @JoinColumn(name = "user_id", nullable = false) 27 | private User user; 28 | 29 | public PasswordResetToken(String token, Instant expiryDate, User user) { 30 | this.token = token; 31 | this.expiryDate = expiryDate; 32 | this.user = user; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/models/Role.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.models; 2 | 3 | import com.fasterxml.jackson.annotation.JsonBackReference; 4 | import jakarta.persistence.*; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | import lombok.ToString; 9 | 10 | import java.util.HashSet; 11 | import java.util.Set; 12 | 13 | @Entity 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | @Data 17 | @Table(name = "roles") 18 | public class Role{ 19 | 20 | @Id 21 | @GeneratedValue(strategy = GenerationType.IDENTITY) 22 | @Column(name = "role_id") 23 | private Integer roleId; 24 | 25 | @ToString.Exclude 26 | @Enumerated(EnumType.STRING) 27 | @Column(length = 20, name = "role_name") 28 | private AppRole roleName; 29 | 30 | @OneToMany(mappedBy = "role", fetch = FetchType.LAZY, cascade = {CascadeType.MERGE}) 31 | @JsonBackReference 32 | @ToString.Exclude 33 | private Set users = new HashSet<>(); 34 | 35 | public Role(AppRole roleName) { 36 | this.roleName = roleName; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/models/User.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.models; 2 | 3 | import com.fasterxml.jackson.annotation.JsonBackReference; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import jakarta.persistence.*; 6 | import jakarta.validation.constraints.Email; 7 | import jakarta.validation.constraints.NotBlank; 8 | import jakarta.validation.constraints.Size; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | import lombok.ToString; 12 | import org.hibernate.annotations.CreationTimestamp; 13 | import org.hibernate.annotations.UpdateTimestamp; 14 | 15 | import java.time.LocalDate; 16 | import java.time.LocalDateTime; 17 | 18 | @Entity 19 | @Data 20 | @NoArgsConstructor 21 | @Table(name = "users", 22 | uniqueConstraints = { 23 | @UniqueConstraint(columnNames = "username"), 24 | @UniqueConstraint(columnNames = "email") 25 | }) 26 | public class User{ 27 | @Id 28 | @GeneratedValue(strategy = GenerationType.IDENTITY) 29 | @Column(name = "user_id") 30 | private Long userId; 31 | 32 | @NotBlank 33 | @Size(max = 20) 34 | @Column(name = "username") 35 | private String userName; 36 | 37 | @NotBlank 38 | @Size(max = 50) 39 | @Email 40 | @Column(name = "email") 41 | private String email; 42 | 43 | @Size(max = 120) 44 | @Column(name = "password") 45 | @JsonIgnore 46 | private String password; 47 | 48 | private boolean accountNonLocked = true; 49 | private boolean accountNonExpired = true; 50 | private boolean credentialsNonExpired = true; 51 | private boolean enabled = true; 52 | 53 | private LocalDate credentialsExpiryDate; 54 | private LocalDate accountExpiryDate; 55 | 56 | private String twoFactorSecret; 57 | private boolean isTwoFactorEnabled = false; 58 | private String signUpMethod; 59 | 60 | @ManyToOne(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE}) 61 | @JoinColumn(name = "role_id", referencedColumnName = "role_id") 62 | @JsonBackReference 63 | @ToString.Exclude 64 | private Role role; 65 | 66 | @CreationTimestamp 67 | @Column(updatable = false) 68 | private LocalDateTime createdDate; 69 | 70 | @UpdateTimestamp 71 | private LocalDateTime updatedDate; 72 | 73 | public User(String userName, String email, String password) { 74 | this.userName = userName; 75 | this.email = email; 76 | this.password = password; 77 | } 78 | 79 | public User(String userName, String email) { 80 | this.userName = userName; 81 | this.email = email; 82 | } 83 | 84 | @Override 85 | public boolean equals(Object o) { 86 | if (this == o) return true; 87 | if (!(o instanceof User)) return false; 88 | return userId != null && userId.equals(((User) o).getUserId()); 89 | } 90 | 91 | @Override 92 | public int hashCode() { 93 | return getClass().hashCode(); 94 | } 95 | } 96 | 97 | 98 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/repositories/AuditLogRepository.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.repositories; 2 | 3 | import com.secure.notes.models.AuditLog; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.List; 7 | 8 | public interface AuditLogRepository extends JpaRepository { 9 | List findByNoteId(Long noteId); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/repositories/NoteRepository.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.repositories; 2 | 3 | import com.secure.notes.models.Note; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import java.util.List; 6 | 7 | public interface NoteRepository extends JpaRepository { 8 | List findByOwnerUsername(String ownerUsername); 9 | } -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/repositories/PasswordResetTokenRepository.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.repositories; 2 | 3 | import com.secure.notes.models.PasswordResetToken; 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 PasswordResetTokenRepository extends JpaRepository { 11 | Optional findByToken(String token); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/repositories/RoleRepository.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.repositories; 2 | 3 | import com.secure.notes.models.AppRole; 4 | import com.secure.notes.models.Role; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | import java.util.Optional; 8 | 9 | public interface RoleRepository extends JpaRepository { 10 | Optional findByRoleName(AppRole appRole); 11 | 12 | } -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/repositories/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.repositories; 2 | 3 | import com.secure.notes.models.User; 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 UserRepository extends JpaRepository { 11 | Optional findByUserName(String username); 12 | 13 | Boolean existsByUserName(String username); 14 | Boolean existsByEmail(String email); 15 | 16 | Optional findByEmail(String email); 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/security/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.security; 2 | 3 | import com.secure.notes.config.OAuth2LoginSuccessHandler; 4 | import com.secure.notes.models.AppRole; 5 | import com.secure.notes.models.Role; 6 | import com.secure.notes.models.User; 7 | import com.secure.notes.repositories.RoleRepository; 8 | import com.secure.notes.repositories.UserRepository; 9 | import com.secure.notes.security.jwt.AuthEntryPointJwt; 10 | import com.secure.notes.security.jwt.AuthTokenFilter; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.CommandLineRunner; 13 | import org.springframework.context.annotation.Bean; 14 | import org.springframework.context.annotation.Configuration; 15 | import org.springframework.context.annotation.Lazy; 16 | import org.springframework.security.authentication.AuthenticationManager; 17 | import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; 18 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 19 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 20 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 21 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 22 | import org.springframework.security.crypto.password.PasswordEncoder; 23 | import org.springframework.security.web.SecurityFilterChain; 24 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 25 | import org.springframework.security.web.csrf.CookieCsrfTokenRepository; 26 | 27 | import java.time.LocalDate; 28 | 29 | import static org.springframework.security.config.Customizer.withDefaults; 30 | 31 | @Configuration 32 | @EnableWebSecurity 33 | @EnableMethodSecurity(prePostEnabled = true, 34 | securedEnabled = true, 35 | jsr250Enabled = true) 36 | public class SecurityConfig { 37 | @Autowired 38 | private AuthEntryPointJwt unauthorizedHandler; 39 | 40 | @Autowired 41 | @Lazy 42 | OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler; 43 | 44 | @Bean 45 | public AuthTokenFilter authenticationJwtTokenFilter() { 46 | return new AuthTokenFilter(); 47 | } 48 | 49 | @Bean 50 | SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { 51 | http.csrf(csrf -> 52 | csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) 53 | .ignoringRequestMatchers("/api/auth/public/**") 54 | ); 55 | //http.csrf(AbstractHttpConfigurer::disable); 56 | http.authorizeHttpRequests((requests) 57 | -> requests 58 | .requestMatchers("/api/admin/**").hasRole("ADMIN") 59 | .requestMatchers("/api/csrf-token").permitAll() 60 | .requestMatchers("/api/auth/public/**").permitAll() 61 | .requestMatchers("/oauth2/**").permitAll() 62 | .anyRequest().authenticated()) 63 | .oauth2Login(oauth2 -> { 64 | oauth2.successHandler(oAuth2LoginSuccessHandler); 65 | }); 66 | http.exceptionHandling(exception 67 | -> exception.authenticationEntryPoint(unauthorizedHandler)); 68 | http.addFilterBefore(authenticationJwtTokenFilter(), 69 | UsernamePasswordAuthenticationFilter.class); 70 | http.formLogin(withDefaults()); 71 | http.httpBasic(withDefaults()); 72 | return http.build(); 73 | } 74 | 75 | @Bean 76 | public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { 77 | return authenticationConfiguration.getAuthenticationManager(); 78 | } 79 | 80 | @Bean 81 | public PasswordEncoder passwordEncoder() { 82 | return new BCryptPasswordEncoder(); 83 | } 84 | 85 | @Bean 86 | public CommandLineRunner initData(RoleRepository roleRepository, 87 | UserRepository userRepository, 88 | PasswordEncoder passwordEncoder) { 89 | return args -> { 90 | Role userRole = roleRepository.findByRoleName(AppRole.ROLE_USER) 91 | .orElseGet(() -> roleRepository.save(new Role(AppRole.ROLE_USER))); 92 | 93 | Role adminRole = roleRepository.findByRoleName(AppRole.ROLE_ADMIN) 94 | .orElseGet(() -> roleRepository.save(new Role(AppRole.ROLE_ADMIN))); 95 | 96 | if (!userRepository.existsByUserName("user1")) { 97 | User user1 = new User("user1", "user1@example.com", 98 | passwordEncoder.encode("password1")); 99 | user1.setAccountNonLocked(false); 100 | user1.setAccountNonExpired(true); 101 | user1.setCredentialsNonExpired(true); 102 | user1.setEnabled(true); 103 | user1.setCredentialsExpiryDate(LocalDate.now().plusYears(1)); 104 | user1.setAccountExpiryDate(LocalDate.now().plusYears(1)); 105 | user1.setTwoFactorEnabled(false); 106 | user1.setSignUpMethod("email"); 107 | user1.setRole(userRole); 108 | userRepository.save(user1); 109 | } 110 | 111 | if (!userRepository.existsByUserName("admin")) { 112 | User admin = new User("admin", "admin@example.com", 113 | passwordEncoder.encode("adminPass")); 114 | admin.setAccountNonLocked(true); 115 | admin.setAccountNonExpired(true); 116 | admin.setCredentialsNonExpired(true); 117 | admin.setEnabled(true); 118 | admin.setCredentialsExpiryDate(LocalDate.now().plusYears(1)); 119 | admin.setAccountExpiryDate(LocalDate.now().plusYears(1)); 120 | admin.setTwoFactorEnabled(false); 121 | admin.setSignUpMethod("email"); 122 | admin.setRole(adminRole); 123 | userRepository.save(admin); 124 | } 125 | }; 126 | } 127 | } 128 | 129 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/security/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.security; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 7 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 8 | 9 | @Configuration 10 | public class WebConfig implements WebMvcConfigurer { 11 | 12 | @Value("${frontend.url}") 13 | private String frontendUrl; 14 | 15 | @Bean 16 | public WebMvcConfigurer corsConfigurer() { 17 | return new WebMvcConfigurer() { 18 | @Override 19 | public void addCorsMappings(CorsRegistry registry) { 20 | registry.addMapping("/**") 21 | .allowedOrigins(frontendUrl) 22 | .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") 23 | .allowedHeaders("*") 24 | .allowCredentials(true) 25 | .maxAge(3600); 26 | } 27 | }; 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/security/jwt/AuthEntryPointJwt.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.security.jwt; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import jakarta.servlet.ServletException; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.http.MediaType; 10 | import org.springframework.security.core.AuthenticationException; 11 | import org.springframework.security.web.AuthenticationEntryPoint; 12 | import org.springframework.stereotype.Component; 13 | 14 | import java.io.IOException; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | 18 | @Component 19 | public class AuthEntryPointJwt implements AuthenticationEntryPoint { 20 | 21 | private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class); 22 | 23 | @Override 24 | public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) 25 | throws IOException, ServletException { 26 | logger.error("Unauthorized error: {}", authException.getMessage()); 27 | System.out.println(authException); 28 | 29 | response.setContentType(MediaType.APPLICATION_JSON_VALUE); 30 | response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 31 | 32 | final Map body = new HashMap<>(); 33 | body.put("status", HttpServletResponse.SC_UNAUTHORIZED); 34 | body.put("error", "Unauthorized"); 35 | body.put("message", authException.getMessage()); 36 | body.put("path", request.getServletPath()); 37 | 38 | final ObjectMapper mapper = new ObjectMapper(); 39 | mapper.writeValue(response.getOutputStream(), body); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/security/jwt/AuthTokenFilter.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.security.jwt; 2 | 3 | import com.secure.notes.security.services.UserDetailsServiceImpl; 4 | import jakarta.servlet.FilterChain; 5 | import jakarta.servlet.ServletException; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 12 | import org.springframework.security.core.context.SecurityContextHolder; 13 | import org.springframework.security.core.userdetails.UserDetails; 14 | import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; 15 | import org.springframework.stereotype.Component; 16 | import org.springframework.web.filter.OncePerRequestFilter; 17 | 18 | import java.io.IOException; 19 | 20 | @Component 21 | public class AuthTokenFilter extends OncePerRequestFilter { 22 | @Autowired 23 | private JwtUtils jwtUtils; 24 | 25 | @Autowired 26 | private UserDetailsServiceImpl userDetailsService; 27 | 28 | private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class); 29 | 30 | @Override 31 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 32 | throws ServletException, IOException { 33 | logger.debug("AuthTokenFilter called for URI: {}", request.getRequestURI()); 34 | try { 35 | String jwt = parseJwt(request); 36 | if (jwt != null && jwtUtils.validateJwtToken(jwt)) { 37 | String username = jwtUtils.getUserNameFromJwtToken(jwt); 38 | 39 | UserDetails userDetails = userDetailsService.loadUserByUsername(username); 40 | 41 | UsernamePasswordAuthenticationToken authentication = 42 | new UsernamePasswordAuthenticationToken(userDetails, 43 | null, 44 | userDetails.getAuthorities()); 45 | logger.debug("Roles from JWT: {}", userDetails.getAuthorities()); 46 | 47 | authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); 48 | 49 | SecurityContextHolder.getContext().setAuthentication(authentication); 50 | } 51 | } catch (Exception e) { 52 | logger.error("Cannot set user authentication: {}", e); 53 | } 54 | 55 | filterChain.doFilter(request, response); 56 | } 57 | 58 | private String parseJwt(HttpServletRequest request) { 59 | String jwt = jwtUtils.getJwtFromHeader(request); 60 | logger.debug("AuthTokenFilter.java: {}", jwt); 61 | return jwt; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/security/jwt/JwtUtils.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.security.jwt; 2 | 3 | import com.secure.notes.security.services.UserDetailsImpl; 4 | import io.jsonwebtoken.*; 5 | import io.jsonwebtoken.io.Decoders; 6 | import io.jsonwebtoken.security.Keys; 7 | import jakarta.servlet.http.HttpServletRequest; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.security.core.userdetails.UserDetails; 12 | import org.springframework.stereotype.Component; 13 | 14 | import javax.crypto.SecretKey; 15 | import java.security.Key; 16 | import java.util.Date; 17 | import java.util.stream.Collectors; 18 | 19 | @Component 20 | public class JwtUtils { 21 | private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class); 22 | 23 | @Value("${spring.app.jwtSecret}") 24 | private String jwtSecret; 25 | 26 | @Value("${spring.app.jwtExpirationMs}") 27 | private int jwtExpirationMs; 28 | 29 | public String getJwtFromHeader(HttpServletRequest request) { 30 | String bearerToken = request.getHeader("Authorization"); 31 | logger.debug("Authorization Header: {}", bearerToken); 32 | if (bearerToken != null && bearerToken.startsWith("Bearer ")) { 33 | return bearerToken.substring(7); // Remove Bearer prefix 34 | } 35 | return null; 36 | } 37 | 38 | public String generateTokenFromUsername(UserDetailsImpl userDetails) { 39 | String username = userDetails.getUsername(); 40 | String roles = userDetails.getAuthorities().stream() 41 | .map(authority -> authority.getAuthority()) 42 | .collect(Collectors.joining(",")); 43 | return Jwts.builder() 44 | .subject(username) 45 | .claim("roles", roles) 46 | .claim("is2faEnabled", userDetails.is2faEnabled()) 47 | .issuedAt(new Date()) 48 | .expiration(new Date((new Date()).getTime() + jwtExpirationMs)) 49 | .signWith(key()) 50 | .compact(); 51 | } 52 | 53 | public String getUserNameFromJwtToken(String token) { 54 | return Jwts.parser() 55 | .verifyWith((SecretKey) key()) 56 | .build().parseSignedClaims(token) 57 | .getPayload().getSubject(); 58 | } 59 | 60 | private Key key() { 61 | return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret)); 62 | } 63 | 64 | public boolean validateJwtToken(String authToken) { 65 | try { 66 | System.out.println("Validate"); 67 | Jwts.parser().verifyWith((SecretKey) key()) 68 | .build().parseSignedClaims(authToken); 69 | return true; 70 | } catch (MalformedJwtException e) { 71 | logger.error("Invalid JWT token: {}", e.getMessage()); 72 | } catch (ExpiredJwtException e) { 73 | logger.error("JWT token is expired: {}", e.getMessage()); 74 | } catch (UnsupportedJwtException e) { 75 | logger.error("JWT token is unsupported: {}", e.getMessage()); 76 | } catch (IllegalArgumentException e) { 77 | logger.error("JWT claims string is empty: {}", e.getMessage()); 78 | } 79 | return false; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/security/request/LoginRequest.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.security.request; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Setter 7 | @Getter 8 | public class LoginRequest { 9 | private String username; 10 | private String password; 11 | } -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/security/request/SignupRequest.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.security.request; 2 | 3 | import java.util.Set; 4 | 5 | import jakarta.validation.constraints.*; 6 | import lombok.Data; 7 | import lombok.Getter; 8 | import lombok.Setter; 9 | 10 | @Data 11 | public class SignupRequest { 12 | @NotBlank 13 | @Size(min = 3, max = 20) 14 | private String username; 15 | 16 | @NotBlank 17 | @Size(max = 50) 18 | @Email 19 | private String email; 20 | 21 | @Setter 22 | @Getter 23 | private Set role; 24 | 25 | @NotBlank 26 | @Size(min = 6, max = 40) 27 | private String password; 28 | } -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/security/response/LoginResponse.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.security.response; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.util.List; 7 | 8 | @Setter 9 | @Getter 10 | public class LoginResponse { 11 | private String jwtToken; 12 | private String username; 13 | private List roles; 14 | 15 | public LoginResponse(String username, List roles, String jwtToken) { 16 | this.username = username; 17 | this.roles = roles; 18 | this.jwtToken = jwtToken; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/security/response/MessageResponse.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.security.response; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Setter 7 | @Getter 8 | public class MessageResponse { 9 | private String message; 10 | 11 | public MessageResponse(String message) { 12 | this.message = message; 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/security/response/UserInfoResponse.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.security.response; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.time.LocalDate; 7 | import java.util.List; 8 | 9 | @Setter 10 | @Getter 11 | public class UserInfoResponse { 12 | private Long id; 13 | private String username; 14 | private String email; 15 | private boolean accountNonLocked; 16 | private boolean accountNonExpired; 17 | private boolean credentialsNonExpired; 18 | private boolean enabled; 19 | private LocalDate credentialsExpiryDate; 20 | private LocalDate accountExpiryDate; 21 | private boolean isTwoFactorEnabled; 22 | private List roles; 23 | 24 | public UserInfoResponse(Long id, String username, String email, boolean accountNonLocked, boolean accountNonExpired, 25 | boolean credentialsNonExpired, boolean enabled, LocalDate credentialsExpiryDate, 26 | LocalDate accountExpiryDate, boolean isTwoFactorEnabled, List roles) { 27 | this.id = id; 28 | this.username = username; 29 | this.email = email; 30 | this.accountNonLocked = accountNonLocked; 31 | this.accountNonExpired = accountNonExpired; 32 | this.credentialsNonExpired = credentialsNonExpired; 33 | this.enabled = enabled; 34 | this.credentialsExpiryDate = credentialsExpiryDate; 35 | this.accountExpiryDate = accountExpiryDate; 36 | this.isTwoFactorEnabled = isTwoFactorEnabled; 37 | this.roles = roles; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/security/services/UserDetailsImpl.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.security.services; 2 | 3 | import java.util.Collection; 4 | import java.util.List; 5 | import java.util.Objects; 6 | 7 | import com.secure.notes.models.User; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | import org.springframework.security.core.GrantedAuthority; 11 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 12 | import org.springframework.security.core.userdetails.UserDetails; 13 | 14 | import com.fasterxml.jackson.annotation.JsonIgnore; 15 | 16 | @NoArgsConstructor 17 | @Data 18 | public class UserDetailsImpl implements UserDetails { 19 | private static final long serialVersionUID = 1L; 20 | 21 | private Long id; 22 | private String username; 23 | private String email; 24 | 25 | @JsonIgnore 26 | private String password; 27 | 28 | private boolean is2faEnabled; 29 | 30 | private Collection authorities; 31 | 32 | public UserDetailsImpl(Long id, String username, String email, String password, 33 | boolean is2faEnabled, Collection authorities) { 34 | this.id = id; 35 | this.username = username; 36 | this.email = email; 37 | this.password = password; 38 | this.is2faEnabled = is2faEnabled; 39 | this.authorities = authorities; 40 | } 41 | 42 | public static UserDetailsImpl build(User user) { 43 | GrantedAuthority authority = new SimpleGrantedAuthority(user.getRole().getRoleName().name()); 44 | 45 | return new UserDetailsImpl( 46 | user.getUserId(), 47 | user.getUserName(), 48 | user.getEmail(), 49 | user.getPassword(), 50 | user.isTwoFactorEnabled(), 51 | List.of(authority) // Wrapping the single authority in a list 52 | ); 53 | } 54 | 55 | 56 | @Override 57 | public Collection getAuthorities() { 58 | return authorities; 59 | } 60 | 61 | public Long getId() { 62 | return id; 63 | } 64 | 65 | public String getEmail() { 66 | return email; 67 | } 68 | 69 | @Override 70 | public String getPassword() { 71 | return password; 72 | } 73 | 74 | @Override 75 | public String getUsername() { 76 | return username; 77 | } 78 | 79 | @Override 80 | public boolean isAccountNonExpired() { 81 | return true; 82 | } 83 | 84 | @Override 85 | public boolean isAccountNonLocked() { 86 | return true; 87 | } 88 | 89 | @Override 90 | public boolean isCredentialsNonExpired() { 91 | return true; 92 | } 93 | 94 | @Override 95 | public boolean isEnabled() { 96 | return true; 97 | } 98 | 99 | public boolean is2faEnabled() { 100 | return is2faEnabled; 101 | } 102 | 103 | @Override 104 | public boolean equals(Object o) { 105 | if (this == o) 106 | return true; 107 | if (o == null || getClass() != o.getClass()) 108 | return false; 109 | UserDetailsImpl user = (UserDetailsImpl) o; 110 | return Objects.equals(id, user.id); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/security/services/UserDetailsServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.security.services; 2 | 3 | import com.secure.notes.models.User; 4 | import com.secure.notes.repositories.UserRepository; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.security.core.userdetails.UserDetails; 7 | import org.springframework.security.core.userdetails.UserDetailsService; 8 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 9 | import org.springframework.stereotype.Service; 10 | import org.springframework.transaction.annotation.Transactional; 11 | 12 | 13 | @Service 14 | public class UserDetailsServiceImpl implements UserDetailsService { 15 | @Autowired 16 | UserRepository userRepository; 17 | 18 | @Override 19 | @Transactional 20 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 21 | User user = userRepository.findByUserName(username) 22 | .orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username)); 23 | 24 | return UserDetailsImpl.build(user); 25 | } 26 | 27 | 28 | 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/services/AuditLogService.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.services; 2 | 3 | import com.secure.notes.models.AuditLog; 4 | import com.secure.notes.models.Note; 5 | 6 | import java.util.List; 7 | 8 | public interface AuditLogService { 9 | void logNoteCreation(String username, Note note); 10 | 11 | void logNoteUpdate(String username, Note note); 12 | 13 | void logNoteDeletion(String username, Long noteId); 14 | 15 | List getAllAuditLogs(); 16 | 17 | List getAuditLogsForNoteId(Long id); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/services/NoteService.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.services; 2 | 3 | import com.secure.notes.models.Note; 4 | 5 | import java.util.List; 6 | 7 | public interface NoteService { 8 | Note createNoteForUser(String username, String content); 9 | 10 | Note updateNoteForUser(Long noteId, String content, String username); 11 | 12 | void deleteNoteForUser(Long noteId, String username); 13 | 14 | List getNotesForUser(String username); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/services/TotpService.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.services; 2 | 3 | import com.warrenstrange.googleauth.GoogleAuthenticatorKey; 4 | 5 | public interface TotpService { 6 | GoogleAuthenticatorKey generateSecret(); 7 | 8 | String getQrCodeUrl(GoogleAuthenticatorKey secret, String username); 9 | 10 | boolean verifyCode(String secret, int code); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/services/UserService.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.services; 2 | 3 | import com.secure.notes.dtos.UserDTO; 4 | import com.secure.notes.models.Role; 5 | import com.secure.notes.models.User; 6 | import com.warrenstrange.googleauth.GoogleAuthenticatorKey; 7 | 8 | import java.util.List; 9 | import java.util.Optional; 10 | 11 | public interface UserService { 12 | void updateUserRole(Long userId, String roleName); 13 | 14 | List getAllUsers(); 15 | 16 | UserDTO getUserById(Long id); 17 | 18 | User findByUsername(String username); 19 | 20 | void updateAccountLockStatus(Long userId, boolean lock); 21 | 22 | List getAllRoles(); 23 | 24 | void updateAccountExpiryStatus(Long userId, boolean expire); 25 | 26 | void updateAccountEnabledStatus(Long userId, boolean enabled); 27 | 28 | void updateCredentialsExpiryStatus(Long userId, boolean expire); 29 | 30 | void updatePassword(Long userId, String password); 31 | 32 | void generatePasswordResetToken(String email); 33 | 34 | void resetPassword(String token, String newPassword); 35 | 36 | Optional findByEmail(String email); 37 | 38 | User registerUser(User user); 39 | 40 | GoogleAuthenticatorKey generate2FASecret(Long userId); 41 | 42 | boolean validate2FACode(Long userId, int code); 43 | 44 | void enable2FA(Long userId); 45 | 46 | void disable2FA(Long userId); 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/services/impl/AuditLogServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.services.impl; 2 | 3 | import com.secure.notes.models.AuditLog; 4 | import com.secure.notes.models.Note; 5 | import com.secure.notes.repositories.AuditLogRepository; 6 | import com.secure.notes.services.AuditLogService; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.time.LocalDateTime; 11 | import java.util.List; 12 | 13 | @Service 14 | public class AuditLogServiceImpl implements AuditLogService { 15 | 16 | @Autowired 17 | AuditLogRepository auditLogRepository; 18 | 19 | @Override 20 | public void logNoteCreation(String username, Note note){ 21 | AuditLog log = new AuditLog(); 22 | log.setAction("CREATE"); 23 | log.setUsername(username); 24 | log.setNoteId(note.getId()); 25 | log.setNoteContent(note.getContent()); 26 | log.setTimestamp(LocalDateTime.now()); 27 | auditLogRepository.save(log); 28 | } 29 | 30 | @Override 31 | public void logNoteUpdate(String username, Note note){ 32 | AuditLog log = new AuditLog(); 33 | log.setAction("UPDATE"); 34 | log.setUsername(username); 35 | log.setNoteId(note.getId()); 36 | log.setNoteContent(note.getContent()); 37 | log.setTimestamp(LocalDateTime.now()); 38 | auditLogRepository.save(log); 39 | } 40 | 41 | @Override 42 | public void logNoteDeletion(String username, Long noteId){ 43 | AuditLog log = new AuditLog(); 44 | log.setAction("DELETE"); 45 | log.setUsername(username); 46 | log.setNoteId(noteId); 47 | log.setTimestamp(LocalDateTime.now()); 48 | auditLogRepository.save(log); 49 | } 50 | 51 | @Override 52 | public List getAllAuditLogs() { 53 | return auditLogRepository.findAll(); 54 | } 55 | 56 | @Override 57 | public List getAuditLogsForNoteId(Long id) { 58 | return auditLogRepository.findByNoteId(id); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/services/impl/NoteServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.services.impl; 2 | 3 | import com.secure.notes.models.Note; 4 | import com.secure.notes.repositories.NoteRepository; 5 | import com.secure.notes.services.AuditLogService; 6 | import com.secure.notes.services.NoteService; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.util.List; 11 | 12 | @Service 13 | public class NoteServiceImpl implements NoteService { 14 | 15 | @Autowired 16 | private NoteRepository noteRepository; 17 | 18 | @Autowired 19 | private AuditLogService auditLogService; 20 | 21 | @Override 22 | public Note createNoteForUser(String username, String content) { 23 | Note note = new Note(); 24 | note.setContent(content); 25 | note.setOwnerUsername(username); 26 | Note savedNote = noteRepository.save(note); 27 | auditLogService.logNoteCreation(username, note); 28 | return savedNote; 29 | } 30 | 31 | @Override 32 | public Note updateNoteForUser(Long noteId, String content, String username) { 33 | Note note = noteRepository.findById(noteId).orElseThrow(() 34 | -> new RuntimeException("Note not found")); 35 | note.setContent(content); 36 | Note updatedNote = noteRepository.save(note); 37 | auditLogService.logNoteUpdate(username, note); 38 | return updatedNote; 39 | } 40 | 41 | @Override 42 | public void deleteNoteForUser(Long noteId, String username) { 43 | Note note = noteRepository.findById(noteId).orElseThrow( 44 | () -> new RuntimeException("Note not found") 45 | ); 46 | auditLogService.logNoteDeletion(username, noteId); 47 | noteRepository.delete(note); 48 | } 49 | 50 | @Override 51 | public List getNotesForUser(String username) { 52 | List personalNotes = noteRepository 53 | .findByOwnerUsername(username); 54 | return personalNotes; 55 | } 56 | } 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/services/impl/TotpServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.services.impl; 2 | 3 | import com.secure.notes.services.TotpService; 4 | import com.warrenstrange.googleauth.GoogleAuthenticator; 5 | import com.warrenstrange.googleauth.GoogleAuthenticatorKey; 6 | import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator; 7 | import org.springframework.stereotype.Service; 8 | 9 | @Service 10 | public class TotpServiceImpl implements TotpService { 11 | 12 | private final GoogleAuthenticator gAuth; 13 | 14 | public TotpServiceImpl(GoogleAuthenticator gAuth) { 15 | this.gAuth = gAuth; 16 | } 17 | 18 | public TotpServiceImpl() { 19 | this.gAuth = new GoogleAuthenticator(); 20 | } 21 | 22 | @Override 23 | public GoogleAuthenticatorKey generateSecret(){ 24 | return gAuth.createCredentials(); 25 | } 26 | 27 | @Override 28 | public String getQrCodeUrl(GoogleAuthenticatorKey secret, String username){ 29 | return GoogleAuthenticatorQRGenerator.getOtpAuthURL("Secure Notes Application", username, secret); 30 | } 31 | 32 | @Override 33 | public boolean verifyCode(String secret, int code){ 34 | return gAuth.authorize(secret, code); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/services/impl/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.services.impl; 2 | 3 | import com.secure.notes.dtos.UserDTO; 4 | import com.secure.notes.models.AppRole; 5 | import com.secure.notes.models.PasswordResetToken; 6 | import com.secure.notes.models.Role; 7 | import com.secure.notes.models.User; 8 | import com.secure.notes.repositories.PasswordResetTokenRepository; 9 | import com.secure.notes.repositories.RoleRepository; 10 | import com.secure.notes.repositories.UserRepository; 11 | import com.secure.notes.services.TotpService; 12 | import com.secure.notes.services.UserService; 13 | import com.secure.notes.util.EmailService; 14 | import com.warrenstrange.googleauth.GoogleAuthenticatorKey; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.beans.factory.annotation.Value; 17 | import org.springframework.security.crypto.password.PasswordEncoder; 18 | import org.springframework.stereotype.Service; 19 | 20 | import java.time.Instant; 21 | import java.time.temporal.ChronoUnit; 22 | import java.util.List; 23 | import java.util.Optional; 24 | import java.util.UUID; 25 | 26 | @Service 27 | public class UserServiceImpl implements UserService { 28 | 29 | @Value("${frontend.url}") 30 | String frontendUrl; 31 | 32 | @Autowired 33 | PasswordEncoder passwordEncoder; 34 | 35 | @Autowired 36 | UserRepository userRepository; 37 | 38 | @Autowired 39 | RoleRepository roleRepository; 40 | 41 | @Autowired 42 | PasswordResetTokenRepository passwordResetTokenRepository; 43 | 44 | @Autowired 45 | EmailService emailService; 46 | 47 | @Autowired 48 | TotpService totpService; 49 | 50 | @Override 51 | public void updateUserRole(Long userId, String roleName) { 52 | User user = userRepository.findById(userId).orElseThrow(() 53 | -> new RuntimeException("User not found")); 54 | AppRole appRole = AppRole.valueOf(roleName); 55 | Role role = roleRepository.findByRoleName(appRole) 56 | .orElseThrow(() -> new RuntimeException("Role not found")); 57 | user.setRole(role); 58 | userRepository.save(user); 59 | } 60 | 61 | @Override 62 | public List getAllUsers() { 63 | return userRepository.findAll(); 64 | } 65 | 66 | 67 | @Override 68 | public UserDTO getUserById(Long id) { 69 | // return userRepository.findById(id).orElseThrow(); 70 | User user = userRepository.findById(id).orElseThrow(); 71 | return convertToDto(user); 72 | } 73 | 74 | private UserDTO convertToDto(User user) { 75 | return new UserDTO( 76 | user.getUserId(), 77 | user.getUserName(), 78 | user.getEmail(), 79 | user.isAccountNonLocked(), 80 | user.isAccountNonExpired(), 81 | user.isCredentialsNonExpired(), 82 | user.isEnabled(), 83 | user.getCredentialsExpiryDate(), 84 | user.getAccountExpiryDate(), 85 | user.getTwoFactorSecret(), 86 | user.isTwoFactorEnabled(), 87 | user.getSignUpMethod(), 88 | user.getRole(), 89 | user.getCreatedDate(), 90 | user.getUpdatedDate() 91 | ); 92 | } 93 | 94 | @Override 95 | public User findByUsername(String username) { 96 | Optional user = userRepository.findByUserName(username); 97 | return user.orElseThrow(() -> new RuntimeException("User not found with username: " + username)); 98 | } 99 | 100 | 101 | @Override 102 | public void updateAccountLockStatus(Long userId, boolean lock) { 103 | User user = userRepository.findById(userId).orElseThrow(() 104 | -> new RuntimeException("User not found")); 105 | user.setAccountNonLocked(!lock); 106 | userRepository.save(user); 107 | } 108 | 109 | 110 | @Override 111 | public List getAllRoles() { 112 | return roleRepository.findAll(); 113 | } 114 | 115 | @Override 116 | public void updateAccountExpiryStatus(Long userId, boolean expire) { 117 | User user = userRepository.findById(userId).orElseThrow(() 118 | -> new RuntimeException("User not found")); 119 | user.setAccountNonExpired(!expire); 120 | userRepository.save(user); 121 | } 122 | 123 | @Override 124 | public void updateAccountEnabledStatus(Long userId, boolean enabled) { 125 | User user = userRepository.findById(userId).orElseThrow(() 126 | -> new RuntimeException("User not found")); 127 | user.setEnabled(enabled); 128 | userRepository.save(user); 129 | } 130 | 131 | @Override 132 | public void updateCredentialsExpiryStatus(Long userId, boolean expire) { 133 | User user = userRepository.findById(userId).orElseThrow(() 134 | -> new RuntimeException("User not found")); 135 | user.setCredentialsNonExpired(!expire); 136 | userRepository.save(user); 137 | } 138 | 139 | 140 | @Override 141 | public void updatePassword(Long userId, String password) { 142 | try { 143 | User user = userRepository.findById(userId) 144 | .orElseThrow(() -> new RuntimeException("User not found")); 145 | user.setPassword(passwordEncoder.encode(password)); 146 | userRepository.save(user); 147 | } catch (Exception e) { 148 | throw new RuntimeException("Failed to update password"); 149 | } 150 | } 151 | 152 | @Override 153 | public void generatePasswordResetToken(String email){ 154 | User user = userRepository.findByEmail(email) 155 | .orElseThrow(() -> new RuntimeException("User not found")); 156 | 157 | String token = UUID.randomUUID().toString(); 158 | Instant expiryDate = Instant.now().plus(24, ChronoUnit.HOURS); 159 | PasswordResetToken resetToken = new PasswordResetToken(token, expiryDate, user); 160 | passwordResetTokenRepository.save(resetToken); 161 | 162 | String resetUrl = frontendUrl + "/reset-password?token=" + token; 163 | // Send email to user 164 | emailService.sendPasswordResetEmail(user.getEmail(), resetUrl); 165 | } 166 | 167 | 168 | @Override 169 | public void resetPassword(String token, String newPassword) { 170 | PasswordResetToken resetToken = passwordResetTokenRepository.findByToken(token) 171 | .orElseThrow(() -> new RuntimeException("Invalid password reset token")); 172 | 173 | if (resetToken.isUsed()) 174 | throw new RuntimeException("Password reset token has already been used"); 175 | 176 | if (resetToken.getExpiryDate().isBefore(Instant.now())) 177 | throw new RuntimeException("Password reset token has expired"); 178 | 179 | User user = resetToken.getUser(); 180 | user.setPassword(passwordEncoder.encode(newPassword)); 181 | userRepository.save(user); 182 | 183 | resetToken.setUsed(true); 184 | passwordResetTokenRepository.save(resetToken); 185 | } 186 | 187 | @Override 188 | public Optional findByEmail(String email) { 189 | return userRepository.findByEmail(email); 190 | } 191 | 192 | @Override 193 | public User registerUser(User user){ 194 | if (user.getPassword() != null) 195 | user.setPassword(passwordEncoder.encode(user.getPassword())); 196 | return userRepository.save(user); 197 | } 198 | 199 | @Override 200 | public GoogleAuthenticatorKey generate2FASecret(Long userId){ 201 | User user = userRepository.findById(userId) 202 | .orElseThrow(() -> new RuntimeException("User not found")); 203 | GoogleAuthenticatorKey key = totpService.generateSecret(); 204 | user.setTwoFactorSecret(key.getKey()); 205 | userRepository.save(user); 206 | return key; 207 | } 208 | 209 | @Override 210 | public boolean validate2FACode(Long userId, int code){ 211 | User user = userRepository.findById(userId) 212 | .orElseThrow(() -> new RuntimeException("User not found")); 213 | return totpService.verifyCode(user.getTwoFactorSecret(), code); 214 | } 215 | 216 | @Override 217 | public void enable2FA(Long userId){ 218 | User user = userRepository.findById(userId) 219 | .orElseThrow(() -> new RuntimeException("User not found")); 220 | user.setTwoFactorEnabled(true); 221 | userRepository.save(user); 222 | } 223 | 224 | @Override 225 | public void disable2FA(Long userId){ 226 | User user = userRepository.findById(userId) 227 | .orElseThrow(() -> new RuntimeException("User not found")); 228 | user.setTwoFactorEnabled(false); 229 | userRepository.save(user); 230 | } 231 | 232 | 233 | } 234 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/util/AuthUtil.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.util; 2 | 3 | import com.secure.notes.models.User; 4 | import com.secure.notes.repositories.UserRepository; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.security.core.Authentication; 7 | import org.springframework.security.core.context.SecurityContextHolder; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Component 11 | public class AuthUtil { 12 | 13 | @Autowired 14 | UserRepository userRepository; 15 | 16 | public Long loggedInUserId(){ 17 | Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 18 | User user = userRepository.findByUserName(authentication.getName()) 19 | .orElseThrow(() -> new RuntimeException("User not found")); 20 | return user.getUserId(); 21 | } 22 | 23 | public User loggedInUser(){ 24 | Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 25 | return userRepository.findByUserName(authentication.getName()) 26 | .orElseThrow(() -> new RuntimeException("User not found")); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/secure/notes/util/EmailService.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes.util; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.mail.SimpleMailMessage; 5 | import org.springframework.mail.javamail.JavaMailSender; 6 | import org.springframework.stereotype.Service; 7 | 8 | @Service 9 | public class EmailService { 10 | 11 | @Autowired 12 | private JavaMailSender mailSender; 13 | 14 | public void sendPasswordResetEmail(String to, String resetUrl){ 15 | SimpleMailMessage message = new SimpleMailMessage(); 16 | message.setTo(to); 17 | message.setSubject("Password Reset Request"); 18 | message.setText("Click the link to reset your password: " + resetUrl); 19 | mailSender.send(message); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/resources/application-dev.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=${SPRING_APPLICATION_NAME} 2 | 3 | spring.security.user.name=${SPRING_SECURITY_USER_NAME} 4 | spring.security.user.password=${SPRING_SECURITY_USER_PASSWORD} 5 | 6 | spring.datasource.url=${SPRING_DATASOURCE_URL} 7 | spring.datasource.username=${SPRING_DATASOURCE_USERNAME} 8 | spring.datasource.password=${SPRING_DATASOURCE_PASSWORD} 9 | 10 | spring.jpa.hibernate.ddl-auto=update 11 | spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect 12 | 13 | logging.level.org.springframework.security=DEBUG 14 | 15 | spring.app.jwtSecret=${SPRING_APP_JWT_SECRET} 16 | spring.app.jwtExpirationMs=${SPRING_APP_JWT_EXPIRATION_MS} 17 | 18 | # email settings 19 | spring.mail.host=smtp.gmail.com 20 | spring.mail.port=587 21 | spring.mail.username=${SPRING_MAIL_USERNAME} 22 | spring.mail.password=${SPRING_MAIL_PASSWORD} 23 | spring.mail.properties.mail.smtp.auth=true 24 | spring.mail.properties.mail.smtp.starttls.enable=true 25 | 26 | # GitHub OAuth2 configuration 27 | spring.security.oauth2.client.registration.github.client-id=${GITHUB_CLIENT_ID} 28 | spring.security.oauth2.client.registration.github.client-secret=${GITHUB_CLIENT_SECRET} 29 | spring.security.oauth2.client.registration.github.scope=read:user,user:email 30 | 31 | # Google OAuth2 configuration 32 | spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID} 33 | spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET} 34 | 35 | frontend.url=${FRONTEND_URL} 36 | -------------------------------------------------------------------------------- /src/main/resources/application-prod.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=${SPRING_APPLICATION_NAME} 2 | 3 | spring.security.user.name=${SPRING_SECURITY_USER_NAME} 4 | spring.security.user.password=${SPRING_SECURITY_USER_PASSWORD} 5 | 6 | spring.datasource.url=${SPRING_DATASOURCE_URL} 7 | spring.datasource.username=${SPRING_DATASOURCE_USERNAME} 8 | spring.datasource.password=${SPRING_DATASOURCE_PASSWORD} 9 | 10 | spring.jpa.hibernate.ddl-auto=update 11 | spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect 12 | 13 | logging.level.org.springframework.security=DEBUG 14 | 15 | spring.app.jwtSecret=${SPRING_APP_JWT_SECRET} 16 | spring.app.jwtExpirationMs=${SPRING_APP_JWT_EXPIRATION_MS} 17 | 18 | # email settings 19 | spring.mail.host=smtp.gmail.com 20 | spring.mail.port=587 21 | spring.mail.username=${SPRING_MAIL_USERNAME} 22 | spring.mail.password=${SPRING_MAIL_PASSWORD} 23 | spring.mail.properties.mail.smtp.auth=true 24 | spring.mail.properties.mail.smtp.starttls.enable=true 25 | 26 | # GitHub OAuth2 configuration 27 | spring.security.oauth2.client.registration.github.client-id=${GITHUB_CLIENT_ID} 28 | spring.security.oauth2.client.registration.github.client-secret=${GITHUB_CLIENT_SECRET} 29 | spring.security.oauth2.client.registration.github.scope=read:user,user:email 30 | 31 | # Google OAuth2 configuration 32 | spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID} 33 | spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET} 34 | 35 | frontend.url=${FRONTEND_URL} 36 | 37 | # Needed if you are using Elastic Bean Stalk 38 | server.port=5000 -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.profiles.active=prod -------------------------------------------------------------------------------- /src/test/java/com/secure/notes/NotesApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.secure.notes; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class NotesApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | --------------------------------------------------------------------------------