├── README.md └── demo ├── HELP.md ├── build.gradle ├── build ├── classes │ └── java │ │ └── main │ │ └── com │ │ └── example │ │ └── demo │ │ ├── DemoApplication.class │ │ ├── config │ │ ├── ApplicationConfiguration.class │ │ ├── EmailConfiguration.class │ │ ├── JwtAuthenticationFilter.class │ │ └── SecurityConfiguration.class │ │ ├── controller │ │ ├── AuthenticationController.class │ │ └── UserController.class │ │ ├── dto │ │ ├── LoginUserDto.class │ │ ├── RegisterUserDto.class │ │ └── VerifyUserDto.class │ │ ├── model │ │ └── User.class │ │ ├── repository │ │ └── UserRepository.class │ │ ├── responses │ │ └── LoginResponse.class │ │ └── service │ │ ├── AuthenticationService.class │ │ ├── EmailService.class │ │ ├── JwtService.class │ │ └── UserService.class ├── resources │ └── main │ │ └── application.properties └── tmp │ └── compileJava │ ├── compileTransaction │ └── stash-dir │ │ ├── AuthenticationController.class.uniqueId0 │ │ └── AuthenticationService.class.uniqueId1 │ └── previous-compilation-data.bin ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── java │ └── com │ │ └── example │ │ └── demo │ │ ├── DemoApplication.java │ │ ├── config │ │ ├── ApplicationConfiguration.java │ │ ├── EmailConfiguration.java │ │ ├── JwtAuthenticationFilter.java │ │ └── SecurityConfiguration.java │ │ ├── controller │ │ ├── AuthenticationController.java │ │ └── UserController.java │ │ ├── dto │ │ ├── LoginUserDto.java │ │ ├── RegisterUserDto.java │ │ └── VerifyUserDto.java │ │ ├── model │ │ └── User.java │ │ ├── repository │ │ └── UserRepository.java │ │ ├── responses │ │ └── LoginResponse.java │ │ └── service │ │ ├── AuthenticationService.java │ │ ├── EmailService.java │ │ ├── JwtService.java │ │ └── UserService.java └── resources │ └── application.properties └── test └── java └── com └── example └── demo └── DemoApplicationTests.java /README.md: -------------------------------------------------------------------------------- 1 | # Signup, Login with Email Verification Tutorial 2 | 3 | Full video tutorial [here](https://youtu.be/uZGuwX3St_c?si=jIITQc20dXuGoOAP) 4 | 5 | This repository contains a tutorial on implementing user signup, login, and email verification using Spring Boot. 6 | 7 | ## Overview 8 | 9 | This tutorial demonstrates how to build a Spring Boot application for user authentication with JWT-based security, including user signup, login, email verification, and basic error handling. 10 | 11 | ## Features 12 | 13 | - User signup with email verification 14 | - User login with JWT token generation 15 | - User email verification flow (send verification code, verify user) 16 | - Fetch authenticated user details 17 | - Basic error handling and exception logging 18 | 19 | ## Prerequisites 20 | 21 | Before running the application, ensure you have the following installed: 22 | 23 | - Java Development Kit (JDK) 8 or newer 24 | - Maven or Gradle 25 | - Supabase database 26 | - Postman (for testing endpoints) 27 | -------------------------------------------------------------------------------- /demo/HELP.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ### Reference Documentation 4 | For further reference, please consider the following sections: 5 | 6 | * [Official Gradle documentation](https://docs.gradle.org) 7 | * [Spring Boot Gradle Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/3.3.1/gradle-plugin/reference/html/) 8 | * [Create an OCI image](https://docs.spring.io/spring-boot/docs/3.3.1/gradle-plugin/reference/html/#build-image) 9 | * [Spring Security](https://docs.spring.io/spring-boot/docs/3.3.1/reference/htmlsingle/index.html#web.security) 10 | * [Spring Web](https://docs.spring.io/spring-boot/docs/3.3.1/reference/htmlsingle/index.html#web) 11 | * [Spring Data JPA](https://docs.spring.io/spring-boot/docs/3.3.1/reference/htmlsingle/index.html#data.sql.jpa-and-spring-data) 12 | * [Java Mail Sender](https://docs.spring.io/spring-boot/docs/3.3.1/reference/htmlsingle/index.html#io.email) 13 | 14 | ### Guides 15 | The following guides illustrate how to use some features concretely: 16 | 17 | * [Securing a Web Application](https://spring.io/guides/gs/securing-web/) 18 | * [Spring Boot and OAuth2](https://spring.io/guides/tutorials/spring-boot-oauth2/) 19 | * [Authenticating a User with LDAP](https://spring.io/guides/gs/authenticating-ldap/) 20 | * [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) 21 | * [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) 22 | * [Building REST services with Spring](https://spring.io/guides/tutorials/rest/) 23 | * [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/) 24 | 25 | ### Additional Links 26 | These additional references should also help you: 27 | 28 | * [Gradle Build Scans – insights for your project's build](https://scans.gradle.com#gradle) 29 | 30 | -------------------------------------------------------------------------------- /demo/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.3.1' 4 | id 'io.spring.dependency-management' version '1.1.5' 5 | } 6 | 7 | group = 'com.example' 8 | version = '0.0.1-SNAPSHOT' 9 | 10 | java { 11 | toolchain { 12 | languageVersion = JavaLanguageVersion.of(22) 13 | } 14 | } 15 | 16 | configurations { 17 | compileOnly { 18 | extendsFrom annotationProcessor 19 | } 20 | } 21 | 22 | repositories { 23 | mavenCentral() 24 | } 25 | 26 | dependencies { 27 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 28 | implementation 'org.springframework.boot:spring-boot-starter-mail' 29 | implementation 'org.springframework.boot:spring-boot-starter-security' 30 | implementation 'org.springframework.boot:spring-boot-starter-web' 31 | compileOnly 'org.projectlombok:lombok' 32 | runtimeOnly 'org.postgresql:postgresql' 33 | annotationProcessor 'org.projectlombok:lombok' 34 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 35 | testImplementation 'org.springframework.security:spring-security-test' 36 | testRuntimeOnly 'org.junit.platform:junit-platform-launcher' 37 | implementation 'io.jsonwebtoken:jjwt-api:0.11.5' 38 | implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' 39 | implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' 40 | } 41 | 42 | tasks.named('test') { 43 | useJUnitPlatform() 44 | } 45 | -------------------------------------------------------------------------------- /demo/build/classes/java/main/com/example/demo/DemoApplication.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erik-Cupsa/Spring-Security-Tutorial/72ab4f1c582da4f5cb4b3d334d36f022a42df1d2/demo/build/classes/java/main/com/example/demo/DemoApplication.class -------------------------------------------------------------------------------- /demo/build/classes/java/main/com/example/demo/config/ApplicationConfiguration.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erik-Cupsa/Spring-Security-Tutorial/72ab4f1c582da4f5cb4b3d334d36f022a42df1d2/demo/build/classes/java/main/com/example/demo/config/ApplicationConfiguration.class -------------------------------------------------------------------------------- /demo/build/classes/java/main/com/example/demo/config/EmailConfiguration.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erik-Cupsa/Spring-Security-Tutorial/72ab4f1c582da4f5cb4b3d334d36f022a42df1d2/demo/build/classes/java/main/com/example/demo/config/EmailConfiguration.class -------------------------------------------------------------------------------- /demo/build/classes/java/main/com/example/demo/config/JwtAuthenticationFilter.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erik-Cupsa/Spring-Security-Tutorial/72ab4f1c582da4f5cb4b3d334d36f022a42df1d2/demo/build/classes/java/main/com/example/demo/config/JwtAuthenticationFilter.class -------------------------------------------------------------------------------- /demo/build/classes/java/main/com/example/demo/config/SecurityConfiguration.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erik-Cupsa/Spring-Security-Tutorial/72ab4f1c582da4f5cb4b3d334d36f022a42df1d2/demo/build/classes/java/main/com/example/demo/config/SecurityConfiguration.class -------------------------------------------------------------------------------- /demo/build/classes/java/main/com/example/demo/controller/AuthenticationController.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erik-Cupsa/Spring-Security-Tutorial/72ab4f1c582da4f5cb4b3d334d36f022a42df1d2/demo/build/classes/java/main/com/example/demo/controller/AuthenticationController.class -------------------------------------------------------------------------------- /demo/build/classes/java/main/com/example/demo/controller/UserController.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erik-Cupsa/Spring-Security-Tutorial/72ab4f1c582da4f5cb4b3d334d36f022a42df1d2/demo/build/classes/java/main/com/example/demo/controller/UserController.class -------------------------------------------------------------------------------- /demo/build/classes/java/main/com/example/demo/dto/LoginUserDto.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erik-Cupsa/Spring-Security-Tutorial/72ab4f1c582da4f5cb4b3d334d36f022a42df1d2/demo/build/classes/java/main/com/example/demo/dto/LoginUserDto.class -------------------------------------------------------------------------------- /demo/build/classes/java/main/com/example/demo/dto/RegisterUserDto.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erik-Cupsa/Spring-Security-Tutorial/72ab4f1c582da4f5cb4b3d334d36f022a42df1d2/demo/build/classes/java/main/com/example/demo/dto/RegisterUserDto.class -------------------------------------------------------------------------------- /demo/build/classes/java/main/com/example/demo/dto/VerifyUserDto.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erik-Cupsa/Spring-Security-Tutorial/72ab4f1c582da4f5cb4b3d334d36f022a42df1d2/demo/build/classes/java/main/com/example/demo/dto/VerifyUserDto.class -------------------------------------------------------------------------------- /demo/build/classes/java/main/com/example/demo/model/User.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erik-Cupsa/Spring-Security-Tutorial/72ab4f1c582da4f5cb4b3d334d36f022a42df1d2/demo/build/classes/java/main/com/example/demo/model/User.class -------------------------------------------------------------------------------- /demo/build/classes/java/main/com/example/demo/repository/UserRepository.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erik-Cupsa/Spring-Security-Tutorial/72ab4f1c582da4f5cb4b3d334d36f022a42df1d2/demo/build/classes/java/main/com/example/demo/repository/UserRepository.class -------------------------------------------------------------------------------- /demo/build/classes/java/main/com/example/demo/responses/LoginResponse.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erik-Cupsa/Spring-Security-Tutorial/72ab4f1c582da4f5cb4b3d334d36f022a42df1d2/demo/build/classes/java/main/com/example/demo/responses/LoginResponse.class -------------------------------------------------------------------------------- /demo/build/classes/java/main/com/example/demo/service/AuthenticationService.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erik-Cupsa/Spring-Security-Tutorial/72ab4f1c582da4f5cb4b3d334d36f022a42df1d2/demo/build/classes/java/main/com/example/demo/service/AuthenticationService.class -------------------------------------------------------------------------------- /demo/build/classes/java/main/com/example/demo/service/EmailService.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erik-Cupsa/Spring-Security-Tutorial/72ab4f1c582da4f5cb4b3d334d36f022a42df1d2/demo/build/classes/java/main/com/example/demo/service/EmailService.class -------------------------------------------------------------------------------- /demo/build/classes/java/main/com/example/demo/service/JwtService.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erik-Cupsa/Spring-Security-Tutorial/72ab4f1c582da4f5cb4b3d334d36f022a42df1d2/demo/build/classes/java/main/com/example/demo/service/JwtService.class -------------------------------------------------------------------------------- /demo/build/classes/java/main/com/example/demo/service/UserService.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erik-Cupsa/Spring-Security-Tutorial/72ab4f1c582da4f5cb4b3d334d36f022a42df1d2/demo/build/classes/java/main/com/example/demo/service/UserService.class -------------------------------------------------------------------------------- /demo/build/resources/main/application.properties: -------------------------------------------------------------------------------- 1 | # Database configuration 2 | spring.datasource.url=${SPRING_DATASOURCE_URL} 3 | spring.datasource.username=${SPRING_DATASOURCE_USERNAME} 4 | spring.datasource.password=${SPRING_DATASOURCE_PASSWORD} 5 | spring.jpa.hibernate.ddl-auto=update 6 | spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect 7 | 8 | # JWT secret key 9 | security.jwt.secret-key =${JWT_SECRET_KEY} 10 | security.jwt.expiration-time=3600000 11 | # 12 | # Mail properties 13 | spring.mail.host=smtp.gmail.com 14 | spring.mail.port=587 15 | spring.mail.username=${SUPPORT_EMAIL} 16 | spring.mail.password=${APP_PASSWORD} 17 | spring.mail.properties.mail.smtp.auth=true 18 | spring.mail.properties.mail.smtp.starttls.enable=true 19 | 20 | spring.config.import=optional:file:.env[.properties] -------------------------------------------------------------------------------- /demo/build/tmp/compileJava/compileTransaction/stash-dir/AuthenticationController.class.uniqueId0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erik-Cupsa/Spring-Security-Tutorial/72ab4f1c582da4f5cb4b3d334d36f022a42df1d2/demo/build/tmp/compileJava/compileTransaction/stash-dir/AuthenticationController.class.uniqueId0 -------------------------------------------------------------------------------- /demo/build/tmp/compileJava/compileTransaction/stash-dir/AuthenticationService.class.uniqueId1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erik-Cupsa/Spring-Security-Tutorial/72ab4f1c582da4f5cb4b3d334d36f022a42df1d2/demo/build/tmp/compileJava/compileTransaction/stash-dir/AuthenticationService.class.uniqueId1 -------------------------------------------------------------------------------- /demo/build/tmp/compileJava/previous-compilation-data.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erik-Cupsa/Spring-Security-Tutorial/72ab4f1c582da4f5cb4b3d334d36f022a42df1d2/demo/build/tmp/compileJava/previous-compilation-data.bin -------------------------------------------------------------------------------- /demo/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erik-Cupsa/Spring-Security-Tutorial/72ab4f1c582da4f5cb4b3d334d36f022a42df1d2/demo/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /demo/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /demo/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /demo/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /demo/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'demo' 2 | -------------------------------------------------------------------------------- /demo/src/main/java/com/example/demo/DemoApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.demo; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class DemoApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(DemoApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /demo/src/main/java/com/example/demo/config/ApplicationConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.config; 2 | 3 | 4 | import com.example.demo.repository.UserRepository; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.security.authentication.AuthenticationManager; 8 | import org.springframework.security.authentication.AuthenticationProvider; 9 | import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 10 | import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; 11 | import org.springframework.security.core.userdetails.UserDetailsService; 12 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 13 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 14 | 15 | @Configuration 16 | public class ApplicationConfiguration { 17 | private final UserRepository userRepository; 18 | public ApplicationConfiguration(UserRepository userRepository) { 19 | this.userRepository = userRepository; 20 | } 21 | 22 | @Bean 23 | UserDetailsService userDetailsService() { 24 | return username -> userRepository.findByEmail(username) 25 | .orElseThrow(() -> new UsernameNotFoundException("User not found")); 26 | } 27 | 28 | @Bean 29 | BCryptPasswordEncoder passwordEncoder() { 30 | return new BCryptPasswordEncoder(); 31 | } 32 | 33 | @Bean 34 | public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { 35 | return config.getAuthenticationManager(); 36 | } 37 | 38 | @Bean 39 | AuthenticationProvider authenticationProvider() { 40 | DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); 41 | 42 | authProvider.setUserDetailsService(userDetailsService()); 43 | authProvider.setPasswordEncoder(passwordEncoder()); 44 | 45 | return authProvider; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /demo/src/main/java/com/example/demo/config/EmailConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.config; 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.mail.javamail.JavaMailSender; 7 | import org.springframework.mail.javamail.JavaMailSenderImpl; 8 | 9 | import java.util.Properties; 10 | 11 | @Configuration 12 | public class EmailConfiguration { 13 | 14 | @Value("${spring.mail.username}") 15 | private String emailUsername; 16 | 17 | @Value("${spring.mail.password}") 18 | private String emailPassword; 19 | 20 | @Bean 21 | public JavaMailSender javaMailSender() { 22 | JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); 23 | mailSender.setHost("smtp.gmail.com"); 24 | mailSender.setPort(587); 25 | mailSender.setUsername(emailUsername); 26 | mailSender.setPassword(emailPassword); 27 | 28 | Properties props = mailSender.getJavaMailProperties(); 29 | props.put("mail.transport.protocol", "smtp"); 30 | props.put("mail.smtp.auth", "true"); 31 | props.put("mail.smtp.starttls.enable", "true"); 32 | props.put("mail.debug", "true"); 33 | 34 | return mailSender; 35 | } 36 | } -------------------------------------------------------------------------------- /demo/src/main/java/com/example/demo/config/JwtAuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.config; 2 | 3 | import com.example.demo.service.JwtService; 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.springframework.lang.NonNull; 9 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 10 | import org.springframework.security.core.Authentication; 11 | import org.springframework.security.core.context.SecurityContextHolder; 12 | import org.springframework.security.core.userdetails.UserDetails; 13 | import org.springframework.security.core.userdetails.UserDetailsService; 14 | import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; 15 | import org.springframework.stereotype.Component; 16 | import org.springframework.web.filter.OncePerRequestFilter; 17 | import org.springframework.web.servlet.HandlerExceptionResolver; 18 | 19 | import java.io.IOException; 20 | 21 | @Component 22 | public class JwtAuthenticationFilter extends OncePerRequestFilter { 23 | private final HandlerExceptionResolver handlerExceptionResolver; 24 | 25 | private final JwtService jwtService; 26 | private final UserDetailsService userDetailsService; 27 | 28 | public JwtAuthenticationFilter( 29 | JwtService jwtService, 30 | UserDetailsService userDetailsService, 31 | HandlerExceptionResolver handlerExceptionResolver 32 | ) { 33 | this.jwtService = jwtService; 34 | this.userDetailsService = userDetailsService; 35 | this.handlerExceptionResolver = handlerExceptionResolver; 36 | } 37 | 38 | @Override 39 | protected void doFilterInternal( 40 | @NonNull HttpServletRequest request, 41 | @NonNull HttpServletResponse response, 42 | @NonNull FilterChain filterChain 43 | ) throws ServletException, IOException { 44 | final String authHeader = request.getHeader("Authorization"); 45 | 46 | if (authHeader == null || !authHeader.startsWith("Bearer ")) { 47 | filterChain.doFilter(request, response); 48 | return; 49 | } 50 | 51 | try { 52 | final String jwt = authHeader.substring(7); 53 | final String userEmail = jwtService.extractUsername(jwt); 54 | 55 | Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 56 | 57 | if (userEmail != null && authentication == null) { 58 | UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail); 59 | 60 | if (jwtService.isTokenValid(jwt, userDetails)) { 61 | UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( 62 | userDetails, 63 | null, 64 | userDetails.getAuthorities() 65 | ); 66 | 67 | authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); 68 | SecurityContextHolder.getContext().setAuthentication(authToken); 69 | } 70 | } 71 | 72 | filterChain.doFilter(request, response); 73 | } catch (Exception exception) { 74 | handlerExceptionResolver.resolveException(request, response, null, exception); 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /demo/src/main/java/com/example/demo/config/SecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.authentication.AuthenticationProvider; 6 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 7 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 8 | import org.springframework.security.config.http.SessionCreationPolicy; 9 | import org.springframework.security.web.SecurityFilterChain; 10 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 11 | import org.springframework.web.cors.CorsConfiguration; 12 | import org.springframework.web.cors.CorsConfigurationSource; 13 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 14 | 15 | import java.util.List; 16 | 17 | @Configuration 18 | @EnableWebSecurity 19 | public class SecurityConfiguration { 20 | private final AuthenticationProvider authenticationProvider; 21 | private final JwtAuthenticationFilter jwtAuthenticationFilter; 22 | 23 | public SecurityConfiguration( 24 | JwtAuthenticationFilter jwtAuthenticationFilter, 25 | AuthenticationProvider authenticationProvider //ignore Bean warning we will get to that 26 | ) { 27 | this.authenticationProvider = authenticationProvider; 28 | this.jwtAuthenticationFilter = jwtAuthenticationFilter; 29 | } 30 | 31 | @Bean 32 | public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 33 | http 34 | .csrf(csrf -> csrf.disable()) 35 | .authorizeHttpRequests(authorize -> authorize 36 | .requestMatchers("/auth/**").permitAll() 37 | .anyRequest().authenticated() 38 | ) 39 | .sessionManagement(session -> session 40 | .sessionCreationPolicy(SessionCreationPolicy.STATELESS) 41 | ) 42 | .authenticationProvider(authenticationProvider) 43 | .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); 44 | 45 | return http.build(); 46 | } 47 | 48 | @Bean 49 | public CorsConfigurationSource corsConfigurationSource() { 50 | CorsConfiguration configuration = new CorsConfiguration(); 51 | configuration.setAllowedOrigins(List.of("https://app-backend.com", "http://localhost:8080")); //TODO: update backend url 52 | configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE")); 53 | configuration.setAllowedHeaders(List.of("Authorization", "Content-Type")); 54 | 55 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 56 | source.registerCorsConfiguration("/**", configuration); 57 | return source; 58 | } 59 | } -------------------------------------------------------------------------------- /demo/src/main/java/com/example/demo/controller/AuthenticationController.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.controller; 2 | 3 | 4 | import com.example.demo.dto.LoginUserDto; 5 | import com.example.demo.dto.RegisterUserDto; 6 | import com.example.demo.dto.VerifyUserDto; 7 | import com.example.demo.model.User; 8 | import com.example.demo.responses.LoginResponse; 9 | import com.example.demo.service.AuthenticationService; 10 | import com.example.demo.service.JwtService; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.web.bind.annotation.*; 13 | 14 | @RequestMapping("/auth") 15 | @RestController 16 | public class AuthenticationController { 17 | private final JwtService jwtService; 18 | 19 | private final AuthenticationService authenticationService; 20 | 21 | public AuthenticationController(JwtService jwtService, AuthenticationService authenticationService) { 22 | this.jwtService = jwtService; 23 | this.authenticationService = authenticationService; 24 | } 25 | 26 | @PostMapping("/signup") 27 | public ResponseEntity register(@RequestBody RegisterUserDto registerUserDto) { 28 | User registeredUser = authenticationService.signup(registerUserDto); 29 | return ResponseEntity.ok(registeredUser); 30 | } 31 | 32 | @PostMapping("/login") 33 | public ResponseEntity authenticate(@RequestBody LoginUserDto loginUserDto){ 34 | User authenticatedUser = authenticationService.authenticate(loginUserDto); 35 | String jwtToken = jwtService.generateToken(authenticatedUser); 36 | LoginResponse loginResponse = new LoginResponse(jwtToken, jwtService.getExpirationTime()); 37 | return ResponseEntity.ok(loginResponse); 38 | } 39 | 40 | @PostMapping("/verify") 41 | public ResponseEntity verifyUser(@RequestBody VerifyUserDto verifyUserDto) { 42 | try { 43 | authenticationService.verifyUser(verifyUserDto); 44 | return ResponseEntity.ok("Account verified successfully"); 45 | } catch (RuntimeException e) { 46 | return ResponseEntity.badRequest().body(e.getMessage()); 47 | } 48 | } 49 | 50 | @PostMapping("/resend") 51 | public ResponseEntity resendVerificationCode(@RequestParam String email) { 52 | try { 53 | authenticationService.resendVerificationCode(email); 54 | return ResponseEntity.ok("Verification code sent"); 55 | } catch (RuntimeException e) { 56 | return ResponseEntity.badRequest().body(e.getMessage()); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /demo/src/main/java/com/example/demo/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.controller; 2 | 3 | import com.example.demo.model.User; 4 | import com.example.demo.service.UserService; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.security.core.Authentication; 7 | import org.springframework.security.core.context.SecurityContextHolder; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import java.util.List; 13 | 14 | @RequestMapping("/users") 15 | @RestController 16 | public class UserController { 17 | private final UserService userService; 18 | public UserController(UserService userService) { 19 | this.userService = userService; 20 | } 21 | 22 | @GetMapping("/me") 23 | public ResponseEntity authenticatedUser() { 24 | Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 25 | User currentUser = (User) authentication.getPrincipal(); 26 | return ResponseEntity.ok(currentUser); 27 | } 28 | 29 | @GetMapping("/") 30 | public ResponseEntity> allUsers() { 31 | List users = userService.allUsers(); 32 | return ResponseEntity.ok(users); 33 | } 34 | } -------------------------------------------------------------------------------- /demo/src/main/java/com/example/demo/dto/LoginUserDto.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class LoginUserDto { 9 | private String email; 10 | private String password; 11 | } -------------------------------------------------------------------------------- /demo/src/main/java/com/example/demo/dto/RegisterUserDto.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class RegisterUserDto { 9 | private String email; 10 | private String password; 11 | private String username; 12 | } -------------------------------------------------------------------------------- /demo/src/main/java/com/example/demo/dto/VerifyUserDto.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class VerifyUserDto { 9 | private String email; 10 | private String verificationCode; 11 | } -------------------------------------------------------------------------------- /demo/src/main/java/com/example/demo/model/User.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.model; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.springframework.security.core.GrantedAuthority; 7 | import org.springframework.security.core.userdetails.UserDetails; 8 | 9 | import java.time.LocalDateTime; 10 | import java.util.Collection; 11 | import java.util.List; 12 | 13 | @Entity 14 | @Table(name = "users") 15 | @Getter 16 | @Setter 17 | public class User implements UserDetails { 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.AUTO) 20 | private Long id; 21 | @Column(unique = true, nullable = false) 22 | private String username; 23 | @Column(unique = true, nullable = false) 24 | private String email; 25 | @Column(nullable = false) 26 | private String password; 27 | 28 | @Column(name = "verification_code") 29 | private String verificationCode; 30 | @Column(name = "verification_expiration") 31 | private LocalDateTime verificationCodeExpiresAt; 32 | private boolean enabled; 33 | 34 | //constructor for creating an unverified user 35 | public User(String username, String email, String password) { 36 | this.username = username; 37 | this.email = email; 38 | this.password = password; 39 | } 40 | //default constructor 41 | public User(){ 42 | } 43 | 44 | @Override 45 | public Collection getAuthorities() { 46 | return List.of(); 47 | } 48 | 49 | //TODO: add proper boolean checks 50 | @Override 51 | public boolean isAccountNonExpired() { 52 | return true; 53 | } 54 | 55 | @Override 56 | public boolean isAccountNonLocked() { 57 | return true; 58 | } 59 | 60 | @Override 61 | public boolean isCredentialsNonExpired() { 62 | return true; 63 | } 64 | 65 | @Override 66 | public boolean isEnabled() { 67 | return enabled; 68 | } 69 | } -------------------------------------------------------------------------------- /demo/src/main/java/com/example/demo/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.repository; 2 | 3 | 4 | import com.example.demo.model.User; 5 | import org.springframework.data.repository.CrudRepository; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import java.util.Optional; 9 | 10 | @Repository 11 | public interface UserRepository extends CrudRepository { 12 | Optional findByEmail(String email); 13 | Optional findByVerificationCode(String verificationCode); 14 | } -------------------------------------------------------------------------------- /demo/src/main/java/com/example/demo/responses/LoginResponse.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.responses; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class LoginResponse { 9 | private String token; 10 | private long expiresIn; 11 | 12 | public LoginResponse(String token, long expiresIn) { 13 | this.token = token; 14 | this.expiresIn = expiresIn; 15 | } 16 | } -------------------------------------------------------------------------------- /demo/src/main/java/com/example/demo/service/AuthenticationService.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.service; 2 | 3 | import com.example.demo.dto.LoginUserDto; 4 | import com.example.demo.dto.RegisterUserDto; 5 | import com.example.demo.dto.VerifyUserDto; 6 | import com.example.demo.model.User; 7 | import com.example.demo.repository.UserRepository; 8 | import jakarta.mail.MessagingException; 9 | import org.springframework.security.authentication.AuthenticationManager; 10 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 11 | import org.springframework.security.crypto.password.PasswordEncoder; 12 | import org.springframework.stereotype.Service; 13 | 14 | import java.time.LocalDateTime; 15 | import java.util.Optional; 16 | import java.util.Random; 17 | 18 | @Service 19 | public class AuthenticationService { 20 | private final UserRepository userRepository; 21 | private final PasswordEncoder passwordEncoder; 22 | private final AuthenticationManager authenticationManager; 23 | private final EmailService emailService; 24 | 25 | public AuthenticationService( 26 | UserRepository userRepository, 27 | AuthenticationManager authenticationManager, 28 | PasswordEncoder passwordEncoder, 29 | EmailService emailService 30 | ) { 31 | this.authenticationManager = authenticationManager; 32 | this.userRepository = userRepository; 33 | this.passwordEncoder = passwordEncoder; 34 | this.emailService = emailService; 35 | } 36 | 37 | public User signup(RegisterUserDto input) { 38 | User user = new User(input.getUsername(), input.getEmail(), passwordEncoder.encode(input.getPassword())); 39 | user.setVerificationCode(generateVerificationCode()); 40 | user.setVerificationCodeExpiresAt(LocalDateTime.now().plusMinutes(15)); 41 | user.setEnabled(false); 42 | sendVerificationEmail(user); 43 | return userRepository.save(user); 44 | } 45 | 46 | public User authenticate(LoginUserDto input) { 47 | User user = userRepository.findByEmail(input.getEmail()) 48 | .orElseThrow(() -> new RuntimeException("User not found")); 49 | 50 | if (!user.isEnabled()) { 51 | throw new RuntimeException("Account not verified. Please verify your account."); 52 | } 53 | authenticationManager.authenticate( 54 | new UsernamePasswordAuthenticationToken( 55 | input.getEmail(), 56 | input.getPassword() 57 | ) 58 | ); 59 | 60 | return user; 61 | } 62 | 63 | public void verifyUser(VerifyUserDto input) { 64 | Optional optionalUser = userRepository.findByEmail(input.getEmail()); 65 | if (optionalUser.isPresent()) { 66 | User user = optionalUser.get(); 67 | if (user.getVerificationCodeExpiresAt().isBefore(LocalDateTime.now())) { 68 | throw new RuntimeException("Verification code has expired"); 69 | } 70 | if (user.getVerificationCode().equals(input.getVerificationCode())) { 71 | user.setEnabled(true); 72 | user.setVerificationCode(null); 73 | user.setVerificationCodeExpiresAt(null); 74 | userRepository.save(user); 75 | } else { 76 | throw new RuntimeException("Invalid verification code"); 77 | } 78 | } else { 79 | throw new RuntimeException("User not found"); 80 | } 81 | } 82 | 83 | public void resendVerificationCode(String email) { 84 | Optional optionalUser = userRepository.findByEmail(email); 85 | if (optionalUser.isPresent()) { 86 | User user = optionalUser.get(); 87 | if (user.isEnabled()) { 88 | throw new RuntimeException("Account is already verified"); 89 | } 90 | user.setVerificationCode(generateVerificationCode()); 91 | user.setVerificationCodeExpiresAt(LocalDateTime.now().plusHours(1)); 92 | sendVerificationEmail(user); 93 | userRepository.save(user); 94 | } else { 95 | throw new RuntimeException("User not found"); 96 | } 97 | } 98 | 99 | private void sendVerificationEmail(User user) { //TODO: Update with company logo 100 | String subject = "Account Verification"; 101 | String verificationCode = "VERIFICATION CODE " + user.getVerificationCode(); 102 | String htmlMessage = "" 103 | + "" 104 | + "
" 105 | + "

Welcome to our app!

" 106 | + "

Please enter the verification code below to continue:

" 107 | + "
" 108 | + "

Verification Code:

" 109 | + "

" + verificationCode + "

" 110 | + "
" 111 | + "
" 112 | + "" 113 | + ""; 114 | 115 | try { 116 | emailService.sendVerificationEmail(user.getEmail(), subject, htmlMessage); 117 | } catch (MessagingException e) { 118 | // Handle email sending exception 119 | e.printStackTrace(); 120 | } 121 | } 122 | private String generateVerificationCode() { 123 | Random random = new Random(); 124 | int code = random.nextInt(900000) + 100000; 125 | return String.valueOf(code); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /demo/src/main/java/com/example/demo/service/EmailService.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.service; 2 | 3 | import jakarta.mail.MessagingException; 4 | import jakarta.mail.internet.MimeMessage; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.mail.javamail.JavaMailSender; 7 | import org.springframework.mail.javamail.MimeMessageHelper; 8 | import org.springframework.stereotype.Service; 9 | 10 | @Service 11 | public class EmailService { 12 | @Autowired 13 | private JavaMailSender emailSender; 14 | 15 | public void sendVerificationEmail(String to, String subject, String text) throws MessagingException { 16 | MimeMessage message = emailSender.createMimeMessage(); 17 | MimeMessageHelper helper = new MimeMessageHelper(message, true); 18 | 19 | helper.setTo(to); 20 | helper.setSubject(subject); 21 | helper.setText(text, true); 22 | 23 | emailSender.send(message); 24 | } 25 | } -------------------------------------------------------------------------------- /demo/src/main/java/com/example/demo/service/JwtService.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.service; 2 | 3 | import io.jsonwebtoken.Claims; 4 | import io.jsonwebtoken.Jwts; 5 | import io.jsonwebtoken.SignatureAlgorithm; 6 | import io.jsonwebtoken.io.Decoders; 7 | import io.jsonwebtoken.security.Keys; 8 | import java.security.Key; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.security.core.userdetails.UserDetails; 11 | import org.springframework.stereotype.Service; 12 | 13 | import java.util.Date; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | import java.util.function.Function; 17 | 18 | @Service 19 | public class JwtService { 20 | @Value("${security.jwt.secret-key}") 21 | private String secretKey; 22 | 23 | @Value("${security.jwt.expiration-time}") 24 | private long jwtExpiration; 25 | 26 | public String extractUsername(String token) { 27 | return extractClaim(token, Claims::getSubject); 28 | } 29 | 30 | public T extractClaim(String token, Function claimsResolver) { 31 | final Claims claims = extractAllClaims(token); 32 | return claimsResolver.apply(claims); 33 | } 34 | 35 | public String generateToken(UserDetails userDetails) { 36 | return generateToken(new HashMap<>(), userDetails); 37 | } 38 | 39 | public String generateToken(Map extraClaims, UserDetails userDetails) { 40 | return buildToken(extraClaims, userDetails, jwtExpiration); 41 | } 42 | 43 | public long getExpirationTime() { 44 | return jwtExpiration; 45 | } 46 | 47 | private String buildToken( 48 | Map extraClaims, 49 | UserDetails userDetails, 50 | long expiration 51 | ) { 52 | return Jwts 53 | .builder() 54 | .setClaims(extraClaims) 55 | .setSubject(userDetails.getUsername()) 56 | .setIssuedAt(new Date(System.currentTimeMillis())) 57 | .setExpiration(new Date(System.currentTimeMillis() + expiration)) 58 | .signWith(getSignInKey(), SignatureAlgorithm.HS256) 59 | .compact(); 60 | } 61 | 62 | public boolean isTokenValid(String token, UserDetails userDetails) { 63 | final String username = extractUsername(token); 64 | return (username.equals(userDetails.getUsername())) && !isTokenExpired(token); 65 | } 66 | 67 | private boolean isTokenExpired(String token) { 68 | return extractExpiration(token).before(new Date()); 69 | } 70 | 71 | private Date extractExpiration(String token) { 72 | return extractClaim(token, Claims::getExpiration); 73 | } 74 | 75 | private Claims extractAllClaims(String token) { 76 | return Jwts 77 | .parserBuilder() 78 | .setSigningKey(getSignInKey()) 79 | .build() 80 | .parseClaimsJws(token) 81 | .getBody(); 82 | } 83 | 84 | private Key getSignInKey() { 85 | byte[] keyBytes = Decoders.BASE64.decode(secretKey); 86 | return Keys.hmacShaKeyFor(keyBytes); 87 | } 88 | } -------------------------------------------------------------------------------- /demo/src/main/java/com/example/demo/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.service; 2 | 3 | import com.example.demo.model.User; 4 | import com.example.demo.repository.UserRepository; 5 | import org.springframework.stereotype.Service; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | @Service 11 | public class UserService { 12 | private final UserRepository userRepository; 13 | public UserService(UserRepository userRepository, EmailService emailService) { 14 | this.userRepository = userRepository; 15 | } 16 | 17 | public List allUsers() { 18 | List users = new ArrayList<>(); 19 | userRepository.findAll().forEach(users::add); 20 | return users; 21 | } 22 | } -------------------------------------------------------------------------------- /demo/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # Database configuration 2 | spring.datasource.url=${SPRING_DATASOURCE_URL} 3 | spring.datasource.username=${SPRING_DATASOURCE_USERNAME} 4 | spring.datasource.password=${SPRING_DATASOURCE_PASSWORD} 5 | spring.jpa.hibernate.ddl-auto=update 6 | spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect 7 | 8 | # JWT secret key 9 | security.jwt.secret-key =${JWT_SECRET_KEY} 10 | security.jwt.expiration-time=3600000 11 | # 12 | # Mail properties 13 | spring.mail.host=smtp.gmail.com 14 | spring.mail.port=587 15 | spring.mail.username=${SUPPORT_EMAIL} 16 | spring.mail.password=${APP_PASSWORD} 17 | spring.mail.properties.mail.smtp.auth=true 18 | spring.mail.properties.mail.smtp.starttls.enable=true 19 | 20 | spring.config.import=optional:file:.env[.properties] -------------------------------------------------------------------------------- /demo/src/test/java/com/example/demo/DemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example.demo; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class DemoApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | --------------------------------------------------------------------------------