diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 13f8d2d..a35c2aa 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -8,4 +8,4 @@ updates:
- package-ecosystem: "gradle" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
- interval: "daily"
+ interval: "weekly"
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
deleted file mode 100644
index 29396d3..0000000
--- a/.github/workflows/codeql-analysis.yml
+++ /dev/null
@@ -1,77 +0,0 @@
-# For most projects, this workflow file will not need changing; you simply need
-# to commit it to your repository.
-#
-# You may wish to alter this file to override the set of languages analyzed,
-# or to provide custom queries or build logic.
-#
-# ******** NOTE ********
-# We have attempted to detect the languages in your repository. Please check
-# the `language` matrix defined below to confirm you have the correct set of
-# supported CodeQL languages.
-#
-name: "CodeQL"
-
-on:
- push:
- branches: ["main"]
- pull_request:
- # The branches below must be a subset of the branches above
- branches: ["main"]
- schedule:
- - cron: "26 2 * * 3"
-
-jobs:
- analyze:
- name: Analyze
- runs-on: ubuntu-latest
- permissions:
- actions: read
- contents: read
- security-events: write
-
- strategy:
- fail-fast: false
- matrix:
- language: ["java", "javascript"]
- # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
- # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v3
-
- - name: Setup JDK 17
- uses: actions/setup-java@v3
- with:
- distribution: "zulu"
- java-version: "17"
-
- # Initializes the CodeQL tools for scanning.
- - name: Initialize CodeQL
- uses: github/codeql-action/init@v2
- with:
- languages: ${{ matrix.language }}
- # If you wish to specify custom queries, you can do so here or in a config file.
- # By default, queries listed here will override any specified in a config file.
- # Prefix the list here with "+" to use these queries and those in the config file.
-
- # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
- # queries: security-extended,security-and-quality
-
- # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
- # If this step fails, then you should remove it and run the build manually (see below)
- - name: Autobuild
- uses: github/codeql-action/autobuild@v2
-
- # âšī¸ Command-line programs to run using the OS shell.
- # đ See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
-
- # If the Autobuild fails above, remove it and uncomment the following three lines.
- # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
-
- # - run: |
- # echo "Run, Build Application using script"
- # ./location_of_script_within_repo/buildscript.sh
-
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 37a9c69..752d806 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -5,5 +5,8 @@
"java.configuration.updateBuildConfiguration": "automatic",
"java.compile.nullAnalysis.mode": "automatic",
"java.transport": "stdio",
- "java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx2G -Xms100m -javaagent:\"/Users/devon/.vscode/extensions/gabrielbb.vscode-lombok-1.0.1/server/lombok.jar\""
+ "java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx2G -Xms100m -javaagent:\"/Users/devon/.vscode/extensions/gabrielbb.vscode-lombok-1.0.1/server/lombok.jar\"",
+ "debug.javascript.defaultRuntimeExecutable": {
+ "pwa-node": "/Users/devon/.local/share/mise/shims/node"
+ }
}
diff --git a/config/README.md b/CHANGELOG.md
similarity index 100%
rename from config/README.md
rename to CHANGELOG.md
diff --git a/CONFIG.md b/CONFIG.md
index 93a78f4..3d5c712 100644
--- a/CONFIG.md
+++ b/CONFIG.md
@@ -23,9 +23,6 @@ Welcome to the User Framework SpringBoot Configuration Guide! This document outl
- **DDL Auto (`spring.jpa.hibernate.ddl-auto`)**: Hibernate schema generation strategy, defaults to `update`.
- **Dialect (`spring.jpa.properties.hibernate.dialect`)**: Set this to the appropriate dialect for your database, defaults to `org.hibernate.dialect.MariaDBDialect`.
-### Application Properties
-
-- **Name (`spring.application.name`)**: Set your application's name, defaults to `User Framework`.
## User Settings
@@ -47,18 +44,12 @@ Welcome to the User Framework SpringBoot Configuration Guide! This document outl
- **From Address (`spring.mail.fromAddress`)**: The email address used as the sender in outgoing emails.
-## Copyright
-
-- **First Year (`spring.copyrightFirstYear`)**: The starting year for the copyright notice.
## Role and Privileges
- **Roles and Privileges (`spring.roles-and-privileges`)**: Map out roles to their respective privileges.
- **Role Hierarchy (`spring.role-hierarchy`)**: Define the hierarchy and inheritance of roles.
-## New Relic Monitoring
-
-- **API Key and Account ID (`management.newrelic.metrics.export`)**: Required if you're integrating with New Relic for monitoring.
## Server and Session Settings
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 4ab0162..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,3 +0,0 @@
-FROM amazoncorretto:11-alpine-jdk
-COPY build/libs/user-1.0.0-SNAPSHOT.jar user-1.0.0-SNAPSHOT.jar
-ENTRYPOINT ["java","-jar","/user-1.0.0-SNAPSHOT.jar"]
diff --git a/PUBLISH.md b/PUBLISH.md
new file mode 100644
index 0000000..9545ebc
--- /dev/null
+++ b/PUBLISH.md
@@ -0,0 +1,34 @@
+# Maven Publishing Guide
+
+# Build and Publish Command Reference
+
+## Building the Project
+
+To build the project, run:
+
+```sh
+./gradlew build
+```
+
+
+## Publish to Local Maven
+
+```shell
+gradle publishLocal
+```
+
+## Publish to Private Maven repository
+
+```shell
+gradle publishReposilite
+```
+
+
+## Publish to Maven Central
+
+```shell
+gradle publishMavenCentral
+```
+
+
+
diff --git a/QUICKSTART.md b/QUICKSTART.md
deleted file mode 100644
index d706700..0000000
--- a/QUICKSTART.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# Quickstart Guide
-
-## Prerequisites
- - Java Development Kit (JDK) 17 or later
-
-## Quick Note
-
-This Framework is intended to be copied and used as a template for new projects. It is not intended to be used as a dependency.
-
-While it would be nice to vend this as a library through Maven or Gradle, I don't belive it's possible to do so. In order for this framework to be useful (for my needs) it needs to provide the front end pages, JS, and set Spring configurations.
-
-If anyone knows a way to do this as a dependancy, please let me knowm, or submit a PR.
-
-
-## Getting Started
-
-1. Download this project as a zip file and extract it to a new folder.
-2. Open the project in your favorite IDE. I use VSCode.
-3. Copy the `src/main/resources/application-local.yml-example` file to `src/main/resources/application-local.yml`
-4. Edit the `src/main/resources/application-local.yml` file to set your configurations for things like SMTP server, Facebook or Google OAuth information, etc. If you need to override any defaults from `application.yml` you can do so here.
-5. Create the local database: `docker run -p 127.0.0.1:3306:3306 --name springuserframework -e MARIADB_ROOT_PASSWORD=springuserroot -e MARIADB_DATABASE=springuser -e MARIADB_USER=springuser -e MARIADB_PASSWORD=springuser -d mariadb:latest`
-6. If you are using a public hostname for OAuth (Google or Facebook), you will need to setup an [ngrok tunnel](https://medium.com/@Demipo/exposing-a-local-spring-boot-app-with-ngrok-819250ef75f) or [CloudFlare tunnel](https://vitobotta.com/2022/02/27/free-ngrok-alternative-with-cloudflare-tunnels/)
-7. Run the project. You can do this from the command line with `./gradlew bootRun`
-8. Open a browser and go to `http://localhost:8080` to see the home page.
-9. If things are working, you can now develop your own application on top of this framework
-
-
-## Bugs, Gaps, Questions
-If you find any issues, gaps in documentation or features, or have any questions, please open an issue on GitHub!
-
-
-
-Back to [README.md](README.md)
diff --git a/README.md b/README.md
index 354b89b..0723547 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,24 @@
-# SpringUserFramework
+## Table of Contents
+- [SpringUserFramework](#springuserframework)
+ - [Summary](#summary)
+ - [Features](#features)
+ - [How To Get Started](#how-to-get-started)
+ - [Refer to the Demo Project](#refer-to-the-demo-project)
+ - [Configuring Your Local Environment](#configuring-your-local-environment)
+ - [Database](#database)
+ - [Mail Sending (SMTP)](#mail-sending-smtp)
+ - [SSO OAuth2 with Google and Facebook](#sso-oauth2-with-google-and-facebook)
+ - [Overriding Spring Security Messages](#overriding-spring-security-messages)
+ - [Notes](#notes)
+
+# SpringUserFramework
-> â ī¸ **WARNING:** This project is about to undergo a major change and be refactored into a library for easier use by your application. If you aren't already using this project, I suggest waiting a couple weeks for the massive new update coming soon!
+SpringUserFramework is a Java Spring Boot User Management Framework designed to simplify the implementation of user management features in your SpringBoot web application. It is built on top of [Spring Security](https://spring.io/projects/spring-security) and provides out-of-the-box support for registration, login, logout, and forgot password flows. It also supports SSO with Google and Facebook.
-SpringUserFramework is a Java Spring Boot User Management Framework designed to simplify the implementation of user management features in your Spring-based web application. It is built on top of [Spring Security](https://spring.io/projects/spring-security) and provides out-of-the-box support for registration, login, logout, and forgot password flows. The framework includes basic example pages that are unstyled, allowing for seamless integration into your application.
+The framework includes basic example pages that are unstyled, allowing for seamless integration into your application.
## Summary
@@ -26,6 +39,8 @@ The framework provides support for the following features:
- Login and logout functionality.
- Forgot password flow.
- Database-backed user store using Spring JPA.
+- SSO support for Google
+- SSO support for Facebook
- Configuration options to control anonymous access, whitelist URIs, and protect specific URIs requiring a logged-in user session.
- CSRF protection enabled by default, with example jQuery AJAX calls passing the CSRF token from the Thymeleaf page context.
- Audit event framework for recording and logging security events, customizable to store audit events in a database or publish them via a REST API.
@@ -36,85 +51,87 @@ The framework provides support for the following features:
## How To Get Started
-### Quickstart Guide
-You can jump right in and get started by following the [Quickstart Guide](QUICKSTART.md).
-
-For more information, read on.
-
-### Configuring Your Local Environment
-There is an example configuration file in /src/main/resources called application-local.yml-example. By default this project's gradle bootRun command runs Spring using the "local" profile. So you can just copy that file to application-local.yml and replace the values (keys, URLs, etc..) with your values. If you are using a different profile to run (such as default) you will just need to ensure the same configs are in place in your active configuration file(s).
-
-You can read more about the required configuration values in the [Configuration Guide](CONFIG.md).
-
-Missing or incorrect configuration values will make this framework not work correctly.
+This Framework is now available as a library on Maven Central. You can add it to your Gradle project by adding the following dependency to your `build.gradle` file:
-### Database
-This framework uses a database as a user store. By buildling on top of Spring JPA it is easy to use which ever datastore you like. The example configuration in application.yml is for a [MariaDB](https://mariadb.com) 10.5 database. You will need to create a user and a database and configure the database name, username, and password.
+```groovy
+implementation 'com.digitalsanctuary:ds-spring-user-framework:3.0.0'
+```
-You can do this using docker with a command like this:
+Or to your Maven project by adding it to your `pom.xml` file:
-```
-docker run -p 127.0.0.1:3306:3306 --name springuserframework -e MARIADB_ROOT_PASSWORD=springuserroot -e MARIADB_DATABASE=springuser -e MARIADB_USER=springuser -e MARIADB_PASSWORD=springuser -d mariadb:latest
+```xml
+
+ com.digitalsanctuary
+ ds-spring-user-framework
+ 3.0.0
+
```
-Or on Apple Silicon:
+Please check for the latest version on [Maven Central](https://central.sonatype.com/artifact/com.digitalsanctuary/ds-spring-user-framework) (this README may not always be up to date).
+When upgrading to a new version, please check the [CHANGELOG](CHANGELOG.md) for any breaking changes or new features.
-```
-docker run -p 127.0.0.1:3306:3306 --name springuserframework -e MARIADB_ROOT_PASSWORD=springuserroot -e MARIADB_DATABASE=springuser -e MARIADB_USER=springuser -e MARIADB_PASSWORD=springuser -d arm64v8/mariadb:latest
-```
-### Mail Sending (SMTP)
-The framework sends emails for verficiation links, forgot password flow, etc... so you need to configure the outbound SMTP server and authentication information.
+### Refer to the Demo Project
+I have created a demo project that uses this framework. You can find it here: [SpringUserFrameworkDemo](https://github.com/devondragon/SpringUserFrameworkDemoApp). This demo project is a full SpringBoot application that uses this framework as a library. You can use it as a reference for how to use this framework in your own project. It demonstrates all of the configuration values and how to override them in your own `application.yml` file. It also has functioning examples for all front end pages, javascript, etc...
-### SSO OAuth2 with Google and Facebook
-The framework supports SSO OAuth2 with Google and Facebook. To enable this you need to configure the client id and secret for each provider.
+In addition to being a fully functional reference, you can also use the demo project as a starting point for your own project. Just clone the repo and start building your own application on top of it.
-For public OAuth you will need a public hostname and HTTPS enabled. You can use ngrok to create a public hostname and tunnel to your local machine. You can then use the ngrok hostname in your Google and Facebook developer console configuration.
+### Configuring Your Local Environment
-### New Relic
-Out of the box the project includes the New Relic Telemetry module, and as such requires a New Relic account id, and associated API key. If you don't use New Relic you can remove the dependancy from the build.gradle file and ignore the configuration values.
+You can read more about the required configuration values in the [Configuration Guide](CONFIG.md).
-Beyond that the default configurations should be all you need, although of course you can customize things however you like.
+Missing or incorrect configuration values will make this framework not work correctly.
-## Docker
+### Database
+This framework uses a database as a user store. By building on top of Spring JPA it is easy to use whichever database you like.
-After running gradle build, you can build a simple Docker image of the application using the provided Dockerfile. Please note that this Dockerfile is basic and does not incorporate advanced features such as layering or buildpacks that you may require for production applications.
+If you set your JPA Hibernate ddl-auto property to "create" it will create the tables for you. If you set it to "update" it will update the tables for you. If you set it to "none" you will need to create the tables yourself.
-Additionally, a docker-compose file is included, which launches a stack with the Spring Boot Application, MariaDB Database, and Postfix Mail Server. The configurations in the docker-compose file are set to make everything work smoothly. However, please be aware that sending emails from your computer (via the docker Postfix Mail Server) may be blocked by email providers due to spam checks. You can use temporary email addresses from [10MinuteMail.com](https://10minutemail.com) for testing purposes, but for real use, it is recommended to configure the Spring Boot application to use a real mail server for outbound transactional emails.
+If you are not using automatic schema updates or Flyway, you can set up your database manually using the provided `schema.sql` file:
+```bash
+mysql -u username -p database_name < src/main/resources/schema.sql
+```
+Flyway support will be coming soon. This will allow you to automatically update your database schema as you deploy new versions of your application.
-## Overriding Spring Security Messages
-You may want to override the default Spring Security user facing messages. You can do this in your messages.properties file, by adding any of the message keys from Spring Security (found here: [Spring Security Messages](https://github.com/spring-projects/spring-security/blob/main/core/src/main/resources/org/springframework/security/messages.properties)) and providing your own values.
+### Mail Sending (SMTP)
+The framework sends emails for verification links, forgot password flow, etc... so you need to configure the outbound SMTP server and authentication information. This is done in the `application.yml` file. You can see the example configuration in the Demo Project's `application.yml` file. Please also refer to the [Spring Boot Mail Properties](https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#mail-properties) for more information on the available properties.
-## Dev Tools
+### SSO OAuth2 with Google and Facebook
+The framework supports SSO OAuth2 with Google and Facebook. To enable this you need to configure the client id and secret for each provider. This is done in the application.yml (or application.properties) file using the [Spring Security OAuth2 properties](https://docs.spring.io/spring-security/reference/servlet/oauth2/login/core.html). You can see the example configuration in the Demo Project's `application.yml` file.
+
+Here is a quick example for your reference:
+
+```yaml
+spring:
+ security:
+ oauth2:
+ client:
+ registration:
+ google:
+ client-id: YOUR_GOOGLE_CLIENT_ID
+ client-secret: YOUR_GOOGLE_CLIENT_SECRET
+ redirect-uri: "{baseUrl}/login/oauth2/code/google"
+ facebook:
+ client-id: YOUR_FACEBOOK_CLIENT_ID
+ client-secret: YOUR_FACEBOOK_CLIENT_SECRET
+ redirect-uri: "{baseUrl}/login/oauth2/code/facebook"
+```
-### SpringBoot DevTools Auto Restart and Live Reload
-Read the following articles:
- - https://www.digitalsanctuary.com/java/springboot-devtools-auto-restart-and-live-reload.html
- - https://www.digitalsanctuary.com/java/how-to-get-springboot-livereload-working-over-https.html
+For public OAuth you will need a public hostname and HTTPS enabled. You can use ngrok or Cloudflare tunnels to create a public hostname and tunnel to your local machine during development. You can then use the ngrok hostname in your Google and Facebook developer console configuration.
-### Live Reload over HTTPS Setup
-If you are running your local dev env using HTTPS or referencing it from a ngrok tunnel using HTTPS, you will need to make a few changes to get Live Reload to work. First you need to tell the application to use HTTPS by setting the following properties in your application.yml file:
-```
-spring.devtools.livereload.https=true
-```
-You then need to install mitmproxy and configure it to intercept the HTTPS traffic. You can do this by running the following command:
-```
-mitmproxy --mode reverse:http://localhost:35729 -p 35739
-```
+## Overriding Spring Security Messages
+
+You may want to override the default Spring Security user facing messages. You can do this in your messages.properties file, by adding any of the message keys from Spring Security (found here: [Spring Security Messages](https://github.com/spring-projects/spring-security/blob/main/core/src/main/resources/org/springframework/security/messages.properties)) and providing your own values.
-By default, mitmproxy uses self-signed SSL certificates, so you need to tell your browser to trust them before this will work. You can do this by opening https://localhost:35739/livereload.js in your browser and going through the steps to trust the server and certificate.
-Alternatively, you can configure mitmproxy to use real certificates and avoid this step. Follow these directions: https://docs.mitmproxy.org/stable/concepts-certificates/
## Notes
-Much of this is based on the [Baeldung course on Spring Security](https://www.baeldung.com/learn-spring-security-course). If you want to learn more about Spring Security or would like to add SSO integration or 2FA to your application, that guide is a great place to start.
-
Please note that there is no warranty or guarantee of functionality, quality, performance, or security made by the author. The code is available freely, but you assume all responsibility and liability for its usage in your application.
diff --git a/build.gradle b/build.gradle
index b2886ad..947614f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,29 +1,31 @@
plugins {
- id 'java'
- id 'org.springframework.boot' version '3.3.5'
- id 'io.spring.dependency-management' version '1.1.7'
- id "com.github.ben-manes.versions" version "0.51.0"
-
+ id 'org.springframework.boot' version '3.4.1'
+ id 'io.spring.dependency-management' version '1.1.7'
+ id 'com.github.ben-manes.versions' version '0.51.0'
+ id 'java-library'
+ id 'maven-publish'
+ id 'signing'
+ id 'com.vanniktech.maven.publish' version '0.30.0'
+ id 'net.researchgate.release' version '3.1.0'
}
-group = 'com.digitalsanctuary.spring'
-version = '1.0.0-SNAPSHOT'
-sourceCompatibility = '17'
-targetCompatibility = '17'
+import com.vanniktech.maven.publish.SonatypeHost
+import com.vanniktech.maven.publish.JavaLibrary
+import com.vanniktech.maven.publish.JavadocJar
-// Define the configurations used in the project
-configurations {
- developmentOnly
- runtimeOnly {
- extendsFrom developmentOnly
- }
- testImplementation {
- extendsFrom runtimeOnly
- }
- compileOnly {
- extendsFrom annotationProcessor
+group = 'com.digitalsanctuary.springuser'
+// version = '3.0.0-SNAPSHOT'
+description = 'Spring User Framework'
+
+ext {
+ springBootVersion = '3.4.1'
+ lombokVersion = '1.18.36'
+}
+
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(17)
}
- dev
}
repositories {
@@ -32,39 +34,45 @@ repositories {
dependencies {
// Spring Boot starters
- implementation 'org.springframework.boot:spring-boot-starter-actuator'
- implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
- implementation 'org.springframework.boot:spring-boot-starter-jdbc'
- implementation 'org.springframework.boot:spring-boot-starter-mail'
- implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
- implementation 'org.springframework.boot:spring-boot-starter-security'
- implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
- implementation 'org.springframework.boot:spring-boot-starter-web'
- implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.3.RELEASE'
- implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.3.0'
-
+ compileOnly 'org.springframework.boot:spring-boot-starter-actuator'
+ compileOnly 'org.springframework.boot:spring-boot-starter-data-jpa'
+ compileOnly 'org.springframework.boot:spring-boot-starter-jdbc'
+ compileOnly 'org.springframework.boot:spring-boot-starter-mail'
+ compileOnly 'org.springframework.boot:spring-boot-starter-oauth2-client'
+ compileOnly 'org.springframework.boot:spring-boot-starter-security'
+ compileOnly 'org.springframework.boot:spring-boot-starter-thymeleaf'
+ compileOnly "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
+ compileOnly 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.3.RELEASE'
+ compileOnly 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.3.0'
// Other dependencies
runtimeOnly 'org.springframework.boot:spring-boot-devtools'
- // runtimeOnly 'io.micrometer:micrometer-registry-new-relic'
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client:3.5.1'
runtimeOnly 'org.postgresql:postgresql'
implementation 'org.passay:passay:1.6.6'
implementation 'com.google.guava:guava:33.4.0-jre'
- implementation 'org.springframework.boot:spring-boot-starter-actuator'
+ compileOnly 'org.springframework.boot:spring-boot-starter-actuator'
+ compileOnly 'jakarta.validation:jakarta.validation-api:3.1.0'
+
+ // Lombok dependencies
+ compileOnly "org.projectlombok:lombok:$lombokVersion"
+ annotationProcessor "org.springframework.boot:spring-boot-configuration-processor:$springBootVersion"
+ annotationProcessor "org.projectlombok:lombok:$lombokVersion"
- compileOnly 'javax.validation:validation-api:2.0.1.Final'
- compileOnly 'org.projectlombok:lombok'
- annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
- annotationProcessor 'org.projectlombok:lombok'
+ // Lombok dependencies for test classes
+ testCompileOnly "org.projectlombok:lombok:$lombokVersion"
+ testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion"
- // Test dependencies
- testImplementation 'org.springframework.boot:spring-boot-starter-test'
+ // Test dependencies
+ testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
+ testImplementation "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
+ testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'com.h2database:h2:2.3.232'
- testImplementation group: 'com.codeborne', name: 'selenide', version: '7.7.0'
- testImplementation group: 'io.github.bonigarcia', name: 'webdrivermanager', version: '5.9.2'
+}
+tasks.named('bootJar') {
+ enabled = false
}
test {
@@ -74,21 +82,126 @@ test {
}
}
+tasks.named('jar') {
+ enabled = true
+ archiveBaseName.set('ds-spring-ai-client')
+ archiveClassifier.set('')
+}
+
+def registerJdkTestTask(name, jdkVersion) {
+ tasks.register(name, Test) {
+ javaLauncher.set(javaToolchains.launcherFor {
+ languageVersion = JavaLanguageVersion.of(jdkVersion)
+ })
+ testClassesDirs = sourceSets.test.output.classesDirs
+ classpath = sourceSets.test.runtimeClasspath
+ useJUnitPlatform()
+ testLogging {
+ events "PASSED", "FAILED", "SKIPPED"
+ }
+ doFirst {
+ println("Running tests with JDK $jdkVersion")
+ }
+ }
+}
+
+registerJdkTestTask('testJdk17', 17)
+registerJdkTestTask('testJdk21', 21)
+
+
+// Task that runs both test tasks
+tasks.register('testAll') {
+ dependsOn(tasks.named('testJdk17'), tasks.named('testJdk21'))
+ doFirst {
+ println("Running tests with both JDK 17 and JDK 21")
+ }
+}
+
+// Make the default 'test' task depend on 'testAll'
+tasks.test {
+ dependsOn(tasks.named('testAll'))
+ doFirst {
+ println("Delegating to 'testAll'")
+ }
+ // Prevent the default test behavior
+ testClassesDirs = files()
+ classpath = files()
+}
+
+
+// Maven Central Publishing Tasks
+mavenPublishing {
+ configure(new JavaLibrary(new JavadocJar.Javadoc(), true))
+ publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
+ signAllPublications()
+ coordinates("com.digitalsanctuary", "ds-spring-user-framework", project.version)
+
+ pom {
+ name = "DS Spring User Framework"
+ description = "Simple SpringBoot User Library built on top of Spring Security."
+ inceptionYear = "2024"
+ url = "https://github.com/devondragon/SpringUserFramework"
+ licenses {
+ license {
+ name = "The Apache License, Version 2.0"
+ url = "http://www.apache.org/licenses/LICENSE-2.0.txt"
+ distribution = "http://www.apache.org/licenses/LICENSE-2.0.txt"
+ }
+ }
+ developers {
+ developer {
+ id = "devondragon"
+ name = "Devon Hillard"
+ url = "https://github.com/devondragon/"
+ }
+ }
+ scm {
+ url = "https://github.com/devondragon/SpringUserFramework"
+ connection = "scm:git:git@github.com:devondragon/SpringUserFramework.git"
+ developerConnection = "scm:git:ssh://git@github.com:devondragon/SpringUserFramework.git"
+ }
+ }
+}
+
+tasks.named("publishMavenPublicationToMavenCentralRepository") {
+ dependsOn("signMavenPublication")
+}
+
+publishing {
+ repositories {
+ maven {
+ name = 'reposiliteRepository'
+ url = uri('https://reposilite.tr0n.io/private')
+ credentials(PasswordCredentials)
+ authentication {
+ basic(BasicAuthentication)
+ }
+ }
+ // more repositories can go here
+ }
+}
+
+// Maven Publishing Aliases
+
+tasks.register("publishReposilite") {
+ dependsOn("publishMavenPublicationToReposiliteRepository")
+}
+
+tasks.register("publishMavenCentral") {
+ dependsOn("publishAndReleaseToMavenCentral")
+}
+
+tasks.register("publishLocal") {
+ dependsOn("publishToMavenLocal")
+}
-bootJar {
- launchScript {
- properties 'confFolder': '/opt/app/conf/'
- }
+task generateAIChangelog(type: Exec) {
+ def newVersion = project.version
+ commandLine 'mise', 'x', '--', 'python', 'generate_changelog.py', newVersion
}
-bootRun {
- // Use Spring Boot DevTool only when we run Gradle bootRun task
- classpath = sourceSets.main.runtimeClasspath + configurations.developmentOnly
- sourceResources sourceSets.main
- if (project.hasProperty('profiles')) {
- environment SPRING_PROFILES_ACTIVE: profiles
- } else {
- def profiles = 'local'
- environment SPRING_PROFILES_ACTIVE: profiles
- }
+release {
+ beforeReleaseBuild.dependsOn generateAIChangelog
+ // afterReleaseBuild.dependsOn publishReposilite
+ afterReleaseBuild.dependsOn publishMavenCentral
}
diff --git a/config/application.properties.example b/config/application.properties.example
deleted file mode 100644
index 0e573c0..0000000
--- a/config/application.properties.example
+++ /dev/null
@@ -1,8 +0,0 @@
-user.mail.fromAddress=your_email_address
-
-spring.mail.host=email-smtp.us-west-2.amazonaws.com
-spring.mail.username=your_aws_smtp_username
-spring.mail.password=your_aws_smtp_password
-
-management.metrics.export.newrelic.apiKey=new_relic_api_key
-management.metrics.export.newrelic.accountId=new_relic_account_number
diff --git a/docker-compose.yml b/docker-compose.yml
deleted file mode 100644
index 91c1ff2..0000000
--- a/docker-compose.yml
+++ /dev/null
@@ -1,74 +0,0 @@
-version: "3"
-
-services:
- myapp-db:
- image: mariadb:10.5
- volumes:
- - userdb:/var/lib/mysql
- environment:
- - MYSQL_ROOT_PASSWORD=root
- - MYSQL_DATABASE=springuser
- - MYSQL_USER=springuser
- - MYSQL_PASSWORD=springuser
- ports:
- - 3306:3306
-
- mailserver:
- image: docker.io/mailserver/docker-mailserver:latest
- hostname: mailserver
- domainname: local
- container_name: mailserver
- env_file: mailserver.env
- ports:
- - "25:25"
- - "143:143"
- - "587:587"
- - "993:993"
- volumes:
- - maildata:/var/mail
- - mailstate:/var/mail-state
- - maillogs:/var/log/mail
- - ./config/:/tmp/docker-mailserver/${SELINUX_LABEL}
- environment:
- - PERMIT_DOCKER=connected-networks
- - ONE_DIR=1
- - DMS_DEBUG=1
- - SPOOF_PROTECTION=0
- - REPORT_RECIPIENT=1
- - ENABLE_SPAMASSASSIN=0
- - ENABLE_CLAMAV=0
- - ENABLE_FAIL2BAN=1
- - ENABLE_POSTGREY=0
- - SMTP_ONLY=1
-# restart: always
- cap_add: [ "NET_ADMIN", "SYS_PTRACE" ]
-
- myapp-main:
- image: user-framework
-# restart: always
- volumes:
- - appvol:/opt/app
- build:
- context: .
- depends_on:
- - myapp-db
- - mailserver
- ports:
- - 8080:8080
- environment:
- - spring.datasource.url=jdbc:mysql://myapp-db:3306/springuser?createDatabaseIfNotExist=true
- - spring.datasource.username=springuser
- - spring.datasource.password=springuser
- - SPRING_PROFILES_ACTIVE=dev
- - spring.mail.host=mailserver
- - spring.mail.properties.mail.smtp.port=25
- - spring.mail.properties.mail.smtp.auth=false
- - spring.mail.properties.mail.smtp.starttls.enable=false
- - spring.mail.properties.mail.smtp.starttls.required=false
-
-volumes:
- maildata:
- mailstate:
- maillogs:
- userdb:
- appvol:
diff --git a/generate_changelog.py b/generate_changelog.py
new file mode 100644
index 0000000..73f650c
--- /dev/null
+++ b/generate_changelog.py
@@ -0,0 +1,88 @@
+import os
+import sys
+import subprocess
+from openai import OpenAI
+from datetime import date
+
+def get_git_commits():
+ # Get the last tag
+ last_tag = subprocess.check_output(
+ ["git", "describe", "--tags", "--abbrev=0"], text=True
+ ).strip()
+
+ # Get commit messages since the last tag
+ commits = subprocess.check_output(
+ ["git", "log", f"{last_tag}..HEAD", "--pretty=format:%s"], text=True
+ ).strip()
+
+ return last_tag, commits.split("\n")
+
+def generate_changelog(commits):
+ if not commits:
+ return "No commits to include in the changelog."
+
+ prompt = f"""
+ You are a helpful assistant tasked with creating a changelog. Based on these Git commit messages, generate a clear, human-readable changelog:
+
+ Commit messages:
+ {commits}
+
+ Format the changelog as follows:
+ ### Features
+ - List features here
+
+ ### Fixes
+ - List fixes here
+
+ ### Breaking Changes
+ - List breaking changes here (if any)
+ """
+
+ client = OpenAI(
+ api_key=os.environ.get("OPENAI_API_TOKEN"), # This is the default and can be omitted
+ )
+ response = client.chat.completions.create(
+ model="gpt-4",
+ messages=[
+ {"role": "system", "content": "You are a helpful assistant for software development."},
+ {"role": "user", "content": prompt},
+ ],
+ )
+ return response.choices[0].message.content.strip()
+
+def update_changelog(version, changelog_content):
+ changelog_file = "CHANGELOG.md"
+ today = date.today().strftime("%Y-%m-%d")
+ new_entry = f"## [{version}] - {today}\n{changelog_content}\n\n"
+
+ if os.path.exists(changelog_file):
+ with open(changelog_file, "r+") as f:
+ old_content = f.read()
+ f.seek(0, 0)
+ f.write(new_entry + old_content)
+ else:
+ with open(changelog_file, "w") as f:
+ f.write(new_entry)
+
+if __name__ == "__main__":
+ last_tag, commits = get_git_commits()
+ if not commits:
+ print("No new commits found.")
+ exit()
+
+ print("Generating changelog...")
+ changelog_content = generate_changelog("\n".join(commits))
+
+ print("\nGenerated Changelog:")
+ print(changelog_content)
+
+ # Check if a version was passed as a command-line argument
+ if len(sys.argv) > 1:
+ new_version = sys.argv[1]
+ else:
+ # Prompt for a version if none was provided
+ new_version = input("Enter the new version (e.g., 1.0.0): ").strip()
+
+ update_changelog(new_version, changelog_content)
+
+ print(f"Changelog updated for version {new_version}!")
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..cd92d6b
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1 @@
+version=3.0.0-SNAPSHOT
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 94113f2..cea7a79 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/mailserver.env b/mailserver.env
deleted file mode 100644
index 941bfe7..0000000
--- a/mailserver.env
+++ /dev/null
@@ -1,446 +0,0 @@
-# âââââââââââââââââââââââââââââââââââââââââââââââ
-# âââ Mailserver Environment Variables ââââââââââ
-# âââââââââââââââââââââââââââââââââââââââââââââââ
-
-# empty => uses the `hostname` command to get the mail server's canonical hostname
-# => Specify a fully-qualified domainname to serve mail for. This is used for many of the config features so if you can't set your hostname (e.g. you're in a container platform that doesn't let you) specify it in this environment variable.
-OVERRIDE_HOSTNAME=
-
-# 0 => Debug disabled
-# 1 => Enables debug on startup
-DMS_DEBUG=0
-
-# critical => Only show critical messages
-# error => Only show erroneous output
-# **warn** => Show warnings
-# info => Normal informational output
-# debug => Also show debug messages
-SUPERVISOR_LOGLEVEL=
-
-# 0 => mail state in default directories
-# 1 => consolidate all states into a single directory (`/var/mail-state`) to allow persistence using docker volumes
-ONE_DIR=1
-
-# empty => postmaster@domain.com
-# => Specify the postmaster address
-POSTMASTER_ADDRESS=
-
-# Set different options for mynetworks option (can be overwrite in postfix-main.cf)
-# **WARNING**: Adding the docker network's gateway to the list of trusted hosts, e.g. using the `network` or
-# `connected-networks` option, can create an open relay
-# https://github.com/docker-mailserver/docker-mailserver/issues/1405#issuecomment-590106498
-# empty => localhost only
-# host => Add docker host (ipv4 only)
-# network => Add all docker containers (ipv4 only)
-# connected-networks => Add all connected docker networks (ipv4 only)
-PERMIT_DOCKER=
-
-# In case you network interface differs from 'eth0', e.g. when you are using HostNetworking in Kubernetes,
-# you can set NETWORK_INTERFACE to whatever interface you want. This interface will then be used.
-# - **empty** => eth0
-NETWORK_INTERFACE=
-
-# empty => modern
-# modern => Enables TLSv1.2 and modern ciphers only. (default)
-# intermediate => Enables TLSv1, TLSv1.1 and TLSv1.2 and broad compatibility ciphers.
-TLS_LEVEL=
-
-# Configures the handling of creating mails with forged sender addresses.
-#
-# empty => (not recommended, but default for backwards compatibility reasons)
-# Mail address spoofing allowed. Any logged in user may create email messages with a forged sender address.
-# See also https://en.wikipedia.org/wiki/Email_spoofing
-# 1 => (recommended) Mail spoofing denied. Each user may only send with his own or his alias addresses.
-# Addresses with extension delimiters(http://www.postfix.org/postconf.5.html#recipient_delimiter) are not able to send messages.
-SPOOF_PROTECTION=
-
-# Enables the Sender Rewriting Scheme. SRS is needed if your mail server acts as forwarder. See [postsrsd](https://github.com/roehling/postsrsd/blob/master/README.md#sender-rewriting-scheme-crash-course) for further explanation.
-# - **0** => Disabled
-# - 1 => Enabled
-ENABLE_SRS=0
-
-# 1 => Enables POP3 service
-# empty => disables POP3
-ENABLE_POP3=
-ENABLE_CLAMAV=0
-
-# If you enable Fail2Ban, don't forget to add the following lines to your `docker-compose.yml`:
-# cap_add:
-# - NET_ADMIN
-# Otherwise, `iptables` won't be able to ban IPs.
-ENABLE_FAIL2BAN=0
-
-# 1 => Enables Managesieve on port 4190
-# empty => disables Managesieve
-ENABLE_MANAGESIEVE=
-
-# **enforce** => Allow other tests to complete. Reject attempts to deliver mail with a 550 SMTP reply, and log the helo/sender/recipient information. Repeat this test the next time the client connects.
-# drop => Drop the connection immediately with a 521 SMTP reply. Repeat this test the next time the client connects.
-# ignore => Ignore the failure of this test. Allow other tests to complete. Repeat this test the next time the client connects. This option is useful for testing and collecting statistics without blocking mail.
-POSTSCREEN_ACTION=enforce
-
-# empty => all daemons start
-# 1 => only launch postfix smtp
-SMTP_ONLY=
-
-# Please read [the SSL page in the wiki](https://github.com/docker-mailserver/docker-mailserver/wiki/Configure-SSL) for more information.
-#
-# empty => SSL disabled
-# letsencrypt => Enables Let's Encrypt certificates
-# custom => Enables custom certificates
-# manual => Let's you manually specify locations of your SSL certificates for non-standard cases
-# self-signed => Enables self-signed certificates
-SSL_TYPE=
-
-# These are only supported with `SSL_TYPE=manual`.
-# Provide the path to your cert and key files that you've mounted access to within the container.
-SSL_CERT_PATH=
-SSL_KEY_PATH=
-# Optional: A 2nd certificate can be supported as fallback (dual cert support), eg ECDSA with an RSA fallback.
-# Useful for additional compatibility with older MTA and MUA (eg pre-2015).
-SSL_ALT_CERT_PATH=
-SSL_ALT_KEY_PATH=
-
-# Set how many days a virusmail will stay on the server before being deleted
-# empty => 7 days
-VIRUSMAILS_DELETE_DELAY=
-
-# This Option is activating the Usage of POSTFIX_DAGENT to specify a lmtp client different from default dovecot socket.
-# empty => disabled
-# 1 => enabled
-ENABLE_POSTFIX_VIRTUAL_TRANSPORT=
-
-# Enabled by ENABLE_POSTFIX_VIRTUAL_TRANSPORT. Specify the final delivery of postfix
-#
-# empty => fail
-# `lmtp:unix:private/dovecot-lmtp` (use socket)
-# `lmtps:inet::` (secure lmtp with starttls, take a look at https://sys4.de/en/blog/2014/11/17/sicheres-lmtp-mit-starttls-in-dovecot/)
-# `lmtp::2003` (use kopano as mailstore)
-# etc.
-POSTFIX_DAGENT=
-
-# Set the mailbox size limit for all users. If set to zero, the size will be unlimited (default).
-#
-# empty => 0
-POSTFIX_MAILBOX_SIZE_LIMIT=
-
-# Set the message size limit for all users. If set to zero, the size will be unlimited (not recommended!)
-#
-# empty => 10240000 (~10 MB)
-POSTFIX_MESSAGE_SIZE_LIMIT=
-
-# Enables regular pflogsumm mail reports.
-# This is a new option. The old REPORT options are still supported for backwards compatibility. If this is not set and reports are enabled with the old options, logrotate will be used.
-#
-# not set => No report
-# daily_cron => Daily report for the previous day
-# logrotate => Full report based on the mail log when it is rotated
-PFLOGSUMM_TRIGGER=
-
-# Recipient address for pflogsumm reports.
-#
-# not set => Use REPORT_RECIPIENT or POSTMASTER_ADDRESS
-# => Specify the recipient address(es)
-PFLOGSUMM_RECIPIENT=
-
-# From address for pflogsumm reports.
-#
-# not set => Use REPORT_SENDER or POSTMASTER_ADDRESS
-# => Specify the sender address
-PFLOGSUMM_SENDER=
-
-# Interval for logwatch report.
-#
-# none => No report is generated
-# daily => Send a daily report
-# weekly => Send a report every week
-LOGWATCH_INTERVAL=
-
-# Recipient address for logwatch reports if they are enabled.
-#
-# not set => Use REPORT_RECIPIENT or POSTMASTER_ADDRESS
-# => Specify the recipient address(es)
-LOGWATCH_RECIPIENT=
-
-# Enables a report being sent (created by pflogsumm) on a regular basis. (deprecated)
-# **0** => Report emails are disabled
-# 1 => Using POSTMASTER_ADDRESS as the recipient
-# => Specify the recipient address
-REPORT_RECIPIENT=0
-
-# Change the sending address for mail report (deprecated)
-# **empty** => mailserver-report@hostname
-# => Specify the report sender (From) address
-REPORT_SENDER=
-
-# Changes the interval in which a report is being sent. (deprecated)
-# **daily** => Send a daily report
-# weekly => Send a report every week
-# monthly => Send a report every month
-#
-# Note: This Variable actually controls logrotate inside the container and rotates the log depending on this setting. The main log output is still available in its entirety via `docker logs mail` (Or your respective container name). If you want to control logrotation for the docker generated logfile see: [Docker Logging Drivers](https://docs.docker.com/config/containers/logging/configure/)
-REPORT_INTERVAL=daily
-
-# Choose TCP/IP protocols to use
-# **all** => All possible protocols.
-# ipv4 => Use only IPv4 traffic. Most likely you want this behind Docker.
-# ipv6 => Use only IPv6 traffic.
-#
-# Note: More details in http://www.postfix.org/postconf.5.html#inet_protocols
-POSTFIX_INET_PROTOCOLS=all
-
-# âââââââââââââââââââââââââââââââââââââââââââââââ
-# âââ Spamassassin Section ââââââââââââââââââââââ
-# âââââââââââââââââââââââââââââââââââââââââââââââ
-
-ENABLE_SPAMASSASSIN=0
-
-# deliver spam messages in the inbox (eventually tagged using SA_SPAM_SUBJECT)
-SPAMASSASSIN_SPAM_TO_INBOX=1
-
-# spam messages will be moved in the Junk folder (SPAMASSASSIN_SPAM_TO_INBOX=1 required)
-MOVE_SPAM_TO_JUNK=1
-
-# add spam info headers if at, or above that level:
-SA_TAG=2.0
-
-# add 'spam detected' headers at that level
-SA_TAG2=6.31
-
-# triggers spam evasive actions
-SA_KILL=6.31
-
-# add tag to subject if spam detected
-SA_SPAM_SUBJECT=***SPAM*****
-
-# âââââââââââââââââââââââââââââââââââââââââââââââ
-# âââ Fetchmail Section âââââââââââââââââââââââââ
-# âââââââââââââââââââââââââââââââââââââââââââââââ
-
-ENABLE_FETCHMAIL=0
-
-# The interval to fetch mail in seconds
-FETCHMAIL_POLL=300
-
-# âââââââââââââââââââââââââââââââââââââââââââââââ
-# âââ LDAP Section ââââââââââââââââââââââââââââââ
-# âââââââââââââââââââââââââââââââââââââââââââââââ
-
-# A second container for the ldap service is necessary (i.e. https://github.com/osixia/docker-openldap)
-# For preparing the ldap server to use in combination with this container this article may be helpful: http://acidx.net/wordpress/2014/06/installing-a-mailserver-with-postfix-dovecot-sasl-ldap-roundcube/
-
-# empty => LDAP authentification is disabled
-# 1 => LDAP authentification is enabled
-ENABLE_LDAP=
-
-# empty => no
-# yes => LDAP over TLS enabled for Postfix
-LDAP_START_TLS=
-
-# If you going to use the mailserver in combination with docker-compose you can set the service name here
-# empty => mail.domain.com
-# Specify the dns-name/ip-address where the ldap-server
-LDAP_SERVER_HOST=
-
-# empty => ou=people,dc=domain,dc=com
-# => e.g. LDAP_SEARCH_BASE=dc=mydomain,dc=local
-LDAP_SEARCH_BASE=
-
-# empty => cn=admin,dc=domain,dc=com
-# => take a look at examples of SASL_LDAP_BIND_DN
-LDAP_BIND_DN=
-
-# empty** => admin
-# => Specify the password to bind against ldap
-LDAP_BIND_PW=
-
-# e.g. `"(&(mail=%s)(mailEnabled=TRUE))"`
-# => Specify how ldap should be asked for users
-LDAP_QUERY_FILTER_USER=
-
-# e.g. `"(&(mailGroupMember=%s)(mailEnabled=TRUE))"`
-# => Specify how ldap should be asked for groups
-LDAP_QUERY_FILTER_GROUP=
-
-# e.g. `"(&(mailAlias=%s)(mailEnabled=TRUE))"`
-# => Specify how ldap should be asked for aliases
-LDAP_QUERY_FILTER_ALIAS=
-
-# e.g. `"(&(|(mail=*@%s)(mailalias=*@%s)(mailGroupMember=*@%s))(mailEnabled=TRUE))"`
-# => Specify how ldap should be asked for domains
-LDAP_QUERY_FILTER_DOMAIN=
-
-# âââââââââââââââââââââââââââââââââââââââââââââââ
-# âââ Dovecot Section âââââââââââââââââââââââââââ
-# âââââââââââââââââââââââââââââââââââââââââââââââ
-
-# empty => no
-# yes => LDAP over TLS enabled for Dovecot
-DOVECOT_TLS=
-
-# e.g. `"(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))"`
-DOVECOT_USER_FILTER=
-
-# e.g. `"(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))"`
-DOVECOT_PASS_FILTER=
-
-# Define the mailbox format to be used
-# default is maildir, supported values are: sdbox, mdbox, maildir
-DOVECOT_MAILBOX_FORMAT=maildir
-
-# empty => no
-# yes => Allow bind authentication for LDAP
-# https://wiki.dovecot.org/AuthDatabase/LDAP/AuthBinds
-DOVECOT_AUTH_BIND=
-
-# âââââââââââââââââââââââââââââââââââââââââââââââ
-# âââ Postgrey Section ââââââââââââââââââââââââââ
-# âââââââââââââââââââââââââââââââââââââââââââââââ
-
-ENABLE_POSTGREY=0
-# greylist for N seconds
-POSTGREY_DELAY=300
-# delete entries older than N days since the last time that they have been seen
-POSTGREY_MAX_AGE=35
-# response when a mail is greylisted
-POSTGREY_TEXT=Delayed by Postgrey
-# whitelist host after N successful deliveries (N=0 to disable whitelisting)
-POSTGREY_AUTO_WHITELIST_CLIENTS=5
-
-# âââââââââââââââââââââââââââââââââââââââââââââââ
-# âââ SASL Section ââââââââââââââââââââââââââââââ
-# âââââââââââââââââââââââââââââââââââââââââââââââ
-
-ENABLE_SASLAUTHD=0
-
-# empty => pam
-# `ldap` => authenticate against ldap server
-# `shadow` => authenticate against local user db
-# `mysql` => authenticate against mysql db
-# `rimap` => authenticate against imap server
-# NOTE: can be a list of mechanisms like pam ldap shadow
-SASLAUTHD_MECHANISMS=
-
-# empty => None
-# e.g. with SASLAUTHD_MECHANISMS rimap you need to specify the ip-address/servername of the imap server ==> xxx.xxx.xxx.xxx
-SASLAUTHD_MECH_OPTIONS=
-
-# empty => localhost
-SASLAUTHD_LDAP_SERVER=
-
-# empty or 0 => `ldap://` will be used
-# 1 => `ldaps://` will be used
-SASLAUTHD_LDAP_SSL=
-
-# empty => anonymous bind
-# specify an object with priviliges to search the directory tree
-# e.g. active directory: SASLAUTHD_LDAP_BIND_DN=cn=Administrator,cn=Users,dc=mydomain,dc=net
-# e.g. openldap: SASLAUTHD_LDAP_BIND_DN=cn=admin,dc=mydomain,dc=net
-SASLAUTHD_LDAP_BIND_DN=
-
-# empty => anonymous bind
-SASLAUTHD_LDAP_PASSWORD=
-
-# empty => Reverting to SASLAUTHD_MECHANISMS pam
-# specify the search base
-SASLAUTHD_LDAP_SEARCH_BASE=
-
-# empty => default filter `(&(uniqueIdentifier=%u)(mailEnabled=TRUE))`
-# e.g. for active directory: `(&(sAMAccountName=%U)(objectClass=person))`
-# e.g. for openldap: `(&(uid=%U)(objectClass=person))`
-SASLAUTHD_LDAP_FILTER=
-
-# empty => no
-# yes => LDAP over TLS enabled for SASL
-# Must not be used together with SASLAUTHD_LDAP_SSL=1_
-SASLAUTHD_LDAP_START_TLS=
-
-# empty => no
-# yes => Require and verify server certificate
-# If yes you must/could specify SASLAUTHD_LDAP_TLS_CACERT_FILE or SASLAUTHD_LDAP_TLS_CACERT_DIR.
-SASLAUTHD_LDAP_TLS_CHECK_PEER=
-
-# File containing CA (Certificate Authority) certificate(s).
-# empty => Nothing is added to the configuration
-# Any value => Fills the `ldap_tls_cacert_file` option
-SASLAUTHD_LDAP_TLS_CACERT_FILE=
-
-# Path to directory with CA (Certificate Authority) certificates.
-# empty => Nothing is added to the configuration
-# Any value => Fills the `ldap_tls_cacert_dir` option
-SASLAUTHD_LDAP_TLS_CACERT_DIR=
-
-# Specify what password attribute to use for password verification.
-# empty => Nothing is added to the configuration but the documentation says it is `userPassword` by default.
-# Any value => Fills the `ldap_password_attr` option
-SASLAUTHD_LDAP_PASSWORD_ATTR=
-
-# empty => No sasl_passwd will be created
-# string => `/etc/postfix/sasl_passwd` will be created with the string as password
-SASL_PASSWD=
-
-# empty => `bind` will be used as a default value
-# `fastbind` => The fastbind method is used
-# `custom` => The custom method uses userPassword attribute to verify the password
-SASLAUTHD_LDAP_AUTH_METHOD=
-
-# Specify the authentication mechanism for SASL bind
-# empty => Nothing is added to the configuration
-# Any value => Fills the `ldap_mech` option
-SASLAUTHD_LDAP_MECH=
-
-# âââââââââââââââââââââââââââââââââââââââââââââââ
-# âââ SRS Section âââââââââââââââââââââââââââââââ
-# âââââââââââââââââââââââââââââââââââââââââââââââ
-
-# envelope_sender => Rewrite only envelope sender address (default)
-# header_sender => Rewrite only header sender (not recommended)
-# envelope_sender,header_sender => Rewrite both senders
-# An email has an "envelope" sender (indicating the sending server) and a
-# "header" sender (indicating who sent it). More strict SPF policies may require
-# you to replace both instead of just the envelope sender.
-SRS_SENDER_CLASSES=envelope_sender
-
-# empty => Envelope sender will be rewritten for all domains
-# provide comma separated list of domains to exclude from rewriting
-SRS_EXCLUDE_DOMAINS=
-
-# empty => generated when the image is built
-# provide a secret to use in base64
-# you may specify multiple keys, comma separated. the first one is used for
-# signing and the remaining will be used for verification. this is how you
-# rotate and expire keys
-SRS_SECRET=
-
-# âââââââââââââââââââââââââââââââââââââââââââââââ
-# âââ Default Relay Host Section ââââââââââââââââ
-# âââââââââââââââââââââââââââââââââââââââââââââââ
-
-# Setup relaying all mail through a default relay host
-#
-# empty => don't configure default relay host
-# default host and optional port to relay all mail through
-DEFAULT_RELAY_HOST=
-
-# âââââââââââââââââââââââââââââââââââââââââââââââ
-# âââ Multi-Domain Relay Section ââââââââââââââââ
-# âââââââââââââââââââââââââââââââââââââââââââââââ
-
-# Setup relaying for multiple domains based on the domain name of the sender
-# optionally uses usernames and passwords in postfix-sasl-password.cf and relay host mappings in postfix-relaymap.cf
-#
-# empty => don't configure relay host
-# default host to relay mail through
-RELAY_HOST=
-
-# empty => 25
-# default port to relay mail
-RELAY_PORT=25
-
-# empty => no default
-# default relay username (if no specific entry exists in postfix-sasl-password.cf)
-RELAY_USER=
-
-# empty => no default
-# password for default relay user
-RELAY_PASSWORD=
diff --git a/settings.gradle b/settings.gradle
index 9d37ea2..c7f1de0 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-rootProject.name = 'user'
+rootProject.name = 'ds-spring-user-framework'
diff --git a/src/main/java/com/digitalsanctuary/spring/user/UserApplication.java b/src/main/java/com/digitalsanctuary/spring/user/UserApplication.java
deleted file mode 100644
index 9f8864b..0000000
--- a/src/main/java/com/digitalsanctuary/spring/user/UserApplication.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.digitalsanctuary.spring.user;
-
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.scheduling.annotation.EnableAsync;
-import org.springframework.scheduling.annotation.EnableScheduling;
-import lombok.extern.slf4j.Slf4j;
-
-/**
- * The Class UserApplication. Basic Spring Boot Application Setup. Adds Async support and Scheduling support to the default Spring Boot stack.
- */
-@Slf4j
-@EnableAsync
-@EnableScheduling
-@SpringBootApplication
-public class UserApplication {
-
- /**
- * The main method.
- *
- * @param args the arguments
- */
- public static void main(String[] args) {
- log.info("Starting UserApplication...");
- SpringApplication.run(UserApplication.class, args);
- log.info("UserApplication started.");
- }
-
-}
diff --git a/src/main/java/com/digitalsanctuary/spring/user/UserConfiguration.java b/src/main/java/com/digitalsanctuary/spring/user/UserConfiguration.java
new file mode 100644
index 0000000..50c1c95
--- /dev/null
+++ b/src/main/java/com/digitalsanctuary/spring/user/UserConfiguration.java
@@ -0,0 +1,36 @@
+package com.digitalsanctuary.spring.user;
+
+import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import jakarta.annotation.PostConstruct;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * The UserConfiguration class is a Spring Boot configuration class that provides configuration for the DigitalSanctuary Spring Boot User Framework
+ * Library. This class is used to configure the user framework library, including enabling asynchronous processing and scheduling, and scanning for
+ * components and repositories.
+ */
+@Slf4j
+@Configuration
+@EnableAsync
+@EnableScheduling
+@ComponentScan(basePackages = "com.digitalsanctuary.spring.user")
+@EnableJpaRepositories(basePackages = "com.digitalsanctuary.spring.user.persistence.repository")
+@EntityScan(basePackages = "com.digitalsanctuary.spring.user.persistence.model")
+public class UserConfiguration {
+
+ /**
+ * Method executed after the bean initialization.
+ *
+ * This method logs a message indicating that the DigitalSanctuary Spring Boot User Framework LIbrary has been loaded.
+ *
+ */
+ @PostConstruct
+ public void onStartup() {
+ log.info("DigitalSanctuary SpringBoot User Framework Library loaded");
+ }
+}
diff --git a/src/main/java/com/digitalsanctuary/spring/user/api/UserAPI.java b/src/main/java/com/digitalsanctuary/spring/user/api/UserAPI.java
index 9413416..8eccdd5 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/api/UserAPI.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/api/UserAPI.java
@@ -1,8 +1,7 @@
package com.digitalsanctuary.spring.user.api;
import java.util.Locale;
-import java.util.Optional;
-import javax.validation.Valid;
+import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.MessageSource;
@@ -10,17 +9,19 @@
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
-import org.springframework.web.bind.annotation.*;
-import com.digitalsanctuary.spring.user.dto.PasswordDto;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.digitalsanctuary.spring.user.audit.AuditEvent;
import com.digitalsanctuary.spring.user.dto.UserDto;
-import com.digitalsanctuary.spring.user.event.AuditEvent;
import com.digitalsanctuary.spring.user.event.OnRegistrationCompleteEvent;
import com.digitalsanctuary.spring.user.exceptions.UserAlreadyExistException;
import com.digitalsanctuary.spring.user.persistence.model.User;
import com.digitalsanctuary.spring.user.service.DSUserDetails;
import com.digitalsanctuary.spring.user.service.UserEmailService;
import com.digitalsanctuary.spring.user.service.UserService;
-import com.digitalsanctuary.spring.user.service.UserService.TokenValidationResult;
import com.digitalsanctuary.spring.user.util.JSONResponse;
import com.digitalsanctuary.spring.user.util.UserUtils;
import jakarta.servlet.ServletException;
@@ -29,322 +30,254 @@
import lombok.extern.slf4j.Slf4j;
/**
- * The UserAPI is the Controller for the REST API endpoints for the user management functionality. By default these endpoints are defined under the
- * "/user" prefix.
+ * REST controller for managing user-related operations. This class handles user registration, account deletion, and other user-related endpoints.
*/
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping(path = "/user", produces = "application/json")
public class UserAPI {
+
private final UserService userService;
private final UserEmailService userEmailService;
private final MessageSource messages;
private final ApplicationEventPublisher eventPublisher;
- // URIs configured in application.properties
- /** The registration pending URI. */
@Value("${user.security.registrationPendingURI}")
private String registrationPendingURI;
- /** The registration success URI. */
@Value("${user.security.registrationSuccessURI}")
private String registrationSuccessURI;
- /** The registration new verification URI. */
- @Value("${user.security.registrationNewVerificationURI}")
- private String registrationNewVerificationURI;
-
- /** The forgot password pending URI. */
@Value("${user.security.forgotPasswordPendingURI}")
private String forgotPasswordPendingURI;
- /** The forgot password change URI. */
- @Value("${user.security.forgotPasswordChangeURI}")
- private String forgotPasswordChangeURI;
-
@Value("${user.actuallyDeleteAccount:false}")
private boolean actuallyDeleteAccount;
-
/**
- * Register a new user account.
+ * Registers a new user account.
*
- * @param userDto the userDTO object is used for passing the form data in
- * @param request the request
- * @return A JSONResponse. In addition to success status, message, and code in the response body, this method also returns a 200 status on
- * success, a 409 status if the email address is already in use, and a 502 if there is an error.
+ * @param userDto the user data transfer object containing user details
+ * @param request the HTTP servlet request
+ * @return a ResponseEntity containing a JSONResponse with the registration result
*/
@PostMapping("/registration")
- public ResponseEntity registerUserAccount(@Valid final UserDto userDto, final HttpServletRequest request) {
- log.debug("Registering user account with information: {}", userDto);
-
- User registeredUser = null;
+ public ResponseEntity registerUserAccount(@Valid @RequestBody UserDto userDto, HttpServletRequest request) {
try {
- registeredUser = userService.registerNewUserAccount(userDto);
-
- eventPublisher.publishEvent(OnRegistrationCompleteEvent.builder().user(registeredUser).locale(request.getLocale())
- .appUrl(UserUtils.getAppUrl(request)).build());
-
- AuditEvent registrationAuditEvent = AuditEvent.builder().source(this).user(registeredUser).sessionId(request.getSession().getId())
- .ipAddress(UserUtils.getClientIP(request)).userAgent(request.getHeader("User-Agent")).action("Registration")
- .actionStatus("Success").message("Registration Successful").build();
- eventPublisher.publishEvent(registrationAuditEvent);
- } catch (UserAlreadyExistException uaee) {
- log.warn("UserAPI.registerUserAccount:" + "UserAlreadyExistException on registration with email: {}!", userDto.getEmail());
- AuditEvent registrationAuditEvent = AuditEvent.builder().source(this).user(registeredUser).sessionId(request.getSession().getId())
- .ipAddress(UserUtils.getClientIP(request)).userAgent(request.getHeader("User-Agent")).action("Registration")
- .actionStatus("Failure").message("User Already Exists").build();
-
- eventPublisher.publishEvent(registrationAuditEvent);
-
- return new ResponseEntity(
- JSONResponse.builder().success(false).code(02).message("An account already exists for the email address").build(),
- HttpStatus.CONFLICT);
- } catch (Exception e) {
- log.error("UserAPI.registerUserAccount:" + "Exception!", e);
- AuditEvent registrationAuditEvent = AuditEvent.builder().source(this).user(registeredUser).sessionId(request.getSession().getId())
- .ipAddress(UserUtils.getClientIP(request)).userAgent(request.getHeader("User-Agent")).action("Registration")
- .actionStatus("Failure").message(e.getMessage()).build();
-
- eventPublisher.publishEvent(registrationAuditEvent);
-
- return new ResponseEntity(JSONResponse.builder().success(false).redirectUrl(null).code(05).message("System Error!").build(),
- HttpStatus.INTERNAL_SERVER_ERROR);
- }
-
- // If there were no exceptions then the registration was a success!
- String nextURL = registrationPendingURI;
- if (registeredUser.isEnabled()) {
- log.debug("UserAPI.registerUserAccount:" + "User is already enabled, skipping email verification and auto-logging them in.");
- nextURL = registrationSuccessURI;
- // Auto-login the user after registration (this is a UX choice, which is why it is in the controller)
- userService.authWithoutPassword(registeredUser);
+ validateUserDto(userDto);
+ User registeredUser = userService.registerNewUserAccount(userDto);
+ publishRegistrationEvent(registeredUser, request);
+ logAuditEvent("Registration", "Success", "Registration Successful", registeredUser, request);
+
+ String nextURL = registeredUser.isEnabled() ? handleAutoLogin(registeredUser) : registrationPendingURI;
+
+ return buildSuccessResponse("Registration Successful!", nextURL);
+ } catch (UserAlreadyExistException ex) {
+ log.warn("User already exists with email: {}", userDto.getEmail());
+ logAuditEvent("Registration", "Failure", "User Already Exists", null, request);
+ return buildErrorResponse("An account already exists for the email address", 2, HttpStatus.CONFLICT);
+ } catch (Exception ex) {
+ log.error("Unexpected error during registration.", ex);
+ logAuditEvent("Registration", "Failure", ex.getMessage(), null, request);
+ return buildErrorResponse("System Error!", 5, HttpStatus.INTERNAL_SERVER_ERROR);
}
- return new ResponseEntity(
- JSONResponse.builder().success(true).redirectUrl(nextURL).code(0).message("Registration Successful!").build(), HttpStatus.OK);
}
/**
- * Re-send registration verification token email.
+ * Resends the registration token. This is used when the user did not receive the initial registration email.
*
- * @param userDto the userDTO for passing in the email address from the form
- * @param request the request
- * @return the generic response
+ * @param userDto the user data transfer object containing user details
+ * @param request the HTTP servlet request
+ * @return a ResponseEntity containing a JSONResponse with the registration result
*/
@PostMapping("/resendRegistrationToken")
- public ResponseEntity resendRegistrationToken(@Valid final UserDto userDto, final HttpServletRequest request) {
- log.debug("UserAPI.resendRegistrationToken:" + "email: {}", userDto.getEmail());
-
- // Lookup User by email
+ public ResponseEntity resendRegistrationToken(@Valid @RequestBody UserDto userDto, HttpServletRequest request) {
User user = userService.findUserByEmail(userDto.getEmail());
- log.debug("UserAPI.resendRegistrationToken:" + "user: {}", user);
- // If user exists
if (user != null) {
- // If user is enabled
if (user.isEnabled()) {
- log.debug("UserAPI.resendRegistrationToken:" + "user is already enabled.");
- // Send response with message and recommendation to login/forgot password
- return new ResponseEntity(JSONResponse.builder().success(false).code(1).message("Account is already verified.").build(),
- HttpStatus.CONFLICT);
- } else {
- // Else send new token email
- log.debug("UserAPI.resendRegistrationToken:" + "sending a new verification token email.");
- String appUrl = UserUtils.getAppUrl(request);
- userEmailService.sendRegistrationVerificationEmail(user, appUrl);
- // Return happy path response
- AuditEvent resendRegTokenAuditEvent = AuditEvent.builder().source(this).user(user).sessionId(request.getSession().getId())
- .ipAddress(UserUtils.getClientIP(request)).userAgent(request.getHeader("User-Agent")).action("Resend Reg Token")
- .actionStatus("Success").message("Success").build();
-
- eventPublisher.publishEvent(resendRegTokenAuditEvent);
- return new ResponseEntity(JSONResponse.builder().success(true).redirectUrl(registrationPendingURI).code(0)
- .message("Verification Email Resent Successfully!").build(), HttpStatus.OK);
+ return buildErrorResponse("Account is already verified.", 1, HttpStatus.CONFLICT);
}
+ userEmailService.sendRegistrationVerificationEmail(user, UserUtils.getAppUrl(request));
+ logAuditEvent("Resend Reg Token", "Success", "Verification Email Resent", user, request);
+ return buildSuccessResponse("Verification Email Resent Successfully!", registrationPendingURI);
}
- // Return generic error response (don't leak too much info)
- return new ResponseEntity(JSONResponse.builder().success(false).code(2).message("System Error!").build(),
- HttpStatus.INTERNAL_SERVER_ERROR);
+ return buildErrorResponse("System Error!", 2, HttpStatus.INTERNAL_SERVER_ERROR);
}
+ /**
+ * Updates the user's password. This is used when the user is logged in and wants to change their password.
+ *
+ * @param userDetails the authenticated user details
+ * @param userDto the user data transfer object containing user details
+ * @param request the HTTP servlet request
+ * @param locale the locale
+ * @return a ResponseEntity containing a JSONResponse with the password update result
+ */
@PostMapping("/updateUser")
- public ResponseEntity updateUserAccount(@AuthenticationPrincipal DSUserDetails userDetails, @Valid final UserDto userDto,
- final HttpServletRequest request, final Locale locale) {
- log.debug("UserAPI.updateUserAccount:" + "called with userDetails: {} and userDto: {}", userDetails, userDto);
- // If the userDetails is not available, or if the user is not logged in, log an error and return a failure.
- if (userDetails == null || SecurityContextHolder.getContext().getAuthentication() == null
- || !SecurityContextHolder.getContext().getAuthentication().isAuthenticated()) {
- log.error("UserAPI.updateUserAccount:" + "updateUser called without logged in user state!");
- return new ResponseEntity(JSONResponse.builder().success(false).message("User Not Logged In!").build(), HttpStatus.OK);
- }
-
+ public ResponseEntity updateUserAccount(@AuthenticationPrincipal DSUserDetails userDetails, @Valid @RequestBody UserDto userDto,
+ HttpServletRequest request, Locale locale) {
+ validateAuthenticatedUser(userDetails);
User user = userDetails.getUser();
-
user.setFirstName(userDto.getFirstName());
user.setLastName(userDto.getLastName());
userService.saveRegisteredUser(user);
- AuditEvent userUpdateAuditEvent = AuditEvent.builder().source(this).user(userDetails.getUser()).sessionId(request.getSession().getId())
- .ipAddress(UserUtils.getClientIP(request)).userAgent(request.getHeader("User-Agent")).action("ProfileUpdate").actionStatus("Success")
- .message("Success").build();
-
- eventPublisher.publishEvent(userUpdateAuditEvent);
-
+ logAuditEvent("ProfileUpdate", "Success", "User profile updated", user, request);
- return new ResponseEntity(
- JSONResponse.builder().success(true).message(messages.getMessage("message.updateUserSuccess", null, locale) + "
").build(),
- HttpStatus.OK);
+ return buildSuccessResponse(messages.getMessage("message.update-user.success", null, locale), null);
}
/**
- * Start of the forgot password flow. This API takes in an email address and, if the user exists, will send a password reset token email to them.
+ * This is used when the user has forgotten their password and wants to reset their password. This will send an email to the user with a link to
+ * reset their password.
*
- * @param userDto the userDTO for passing in the email address from the form
- * @param request the request
- * @return a generic success response, so as to not leak information about accounts existing or not.
+ * @param userDto the user data transfer object containing user details
+ * @param request the HTTP servlet request
+ * @return a ResponseEntity containing a JSONResponse with the password reset email send result
*/
@PostMapping("/resetPassword")
- public ResponseEntity resetPassword(@Valid final UserDto userDto, final HttpServletRequest request) {
- log.debug("UserAPI.resetPassword:" + "email: {}", userDto.getEmail());
-
- // Lookup User by email
+ public ResponseEntity resetPassword(@Valid @RequestBody UserDto userDto, HttpServletRequest request) {
User user = userService.findUserByEmail(userDto.getEmail());
- log.debug("UserAPI.resendRegistrationToken:" + "user: {}", user);
-
if (user != null) {
- String appUrl = UserUtils.getAppUrl(request);
- userEmailService.sendForgotPasswordVerificationEmail(user, appUrl);
-
- AuditEvent resetPasswordAuditEvent =
- AuditEvent.builder().source(this).user(user).sessionId(request.getSession().getId()).ipAddress(UserUtils.getClientIP(request))
- .userAgent(request.getHeader("User-Agent")).action("Reset Password").actionStatus("Success").message("Success").build();
-
- eventPublisher.publishEvent(resetPasswordAuditEvent);
-
- } else {
- AuditEvent resetPasswordAuditEvent = AuditEvent.builder().source(this).user(user).sessionId(request.getSession().getId())
- .ipAddress(UserUtils.getClientIP(request)).userAgent(request.getHeader("User-Agent")).action("Reset Password")
- .actionStatus("Failure").message("Invalid EMail Submitted").extraData("Email submitted: " + userDto.getEmail()).build();
- eventPublisher.publishEvent(resetPasswordAuditEvent);
+ userEmailService.sendForgotPasswordVerificationEmail(user, UserUtils.getAppUrl(request));
+ logAuditEvent("Reset Password", "Success", "Password reset email sent", user, request);
}
-
- return new ResponseEntity(JSONResponse.builder().success(true).redirectUrl(forgotPasswordPendingURI)
- .message("If account exists, password reset email has been sent!").build(), HttpStatus.OK);
+ return buildSuccessResponse("If account exists, password reset email has been sent!", forgotPasswordPendingURI);
}
/**
- * Saves a new password from a password reset flow based on a password reset token.
+ * Deletes the user's account. This is used when the user wants to delete their account. This will either delete the account or disable it based
+ * on the configuration of the actuallyDeleteAccount property. After the account is disabled or deleted, the user will be logged out.
*
- * @param locale the locale
- * @param passwordDto the password dto
- * @param request the request
- * @return the generic response
+ * @param userDetails the authenticated user details
+ * @param request the HTTP servlet request
+ * @return a ResponseEntity containing a JSONResponse with the account deletion result
*/
- @PostMapping("/savePassword")
- public ResponseEntity savePassword(@Valid PasswordDto passwordDto, final HttpServletRequest request, final Locale locale) {
- log.debug("UserAPI.savePassword:" + "called with passwordDto: {}", passwordDto);
-
- final TokenValidationResult validationResult = userService.validatePasswordResetToken(passwordDto.getToken());
- log.debug("UserAPI.savePassword:" + "result: {}", validationResult);
- if (validationResult == TokenValidationResult.VALID) {
- Optional user = userService.getUserByPasswordResetToken(passwordDto.getToken());
- if (user.isPresent()) {
- userService.changeUserPassword(user.get(), passwordDto.getNewPassword());
- log.debug("UserAPI.savePassword:" + "password updated!");
-
- AuditEvent savePasswordAuditEvent = AuditEvent.builder().source(this).user(user.get()).sessionId(request.getSession().getId())
- .ipAddress(UserUtils.getClientIP(request)).userAgent(request.getHeader("User-Agent")).action("Reset Save Password")
- .actionStatus("Success").message("Success").build();
- eventPublisher.publishEvent(savePasswordAuditEvent);
+ @DeleteMapping("/deleteAccount")
+ public ResponseEntity deleteAccount(@AuthenticationPrincipal DSUserDetails userDetails, HttpServletRequest request) {
+ validateAuthenticatedUser(userDetails);
+ User user = userDetails.getUser();
- // In this case we are returning a success, with multiple messages designed to be displayed on-page,
- // instead of a redirect URL like most of the other calls.
- return new ResponseEntity(
- JSONResponse.builder().success(true).message(messages.getMessage("message.resetPasswordSuccess", null, locale))
- .message("Login").build(),
- HttpStatus.OK);
- } else {
- log.debug("UserAPI.savePassword:" + "user could not be found!");
- return new ResponseEntity(
- JSONResponse.builder().success(false).code(1).message(messages.getMessage("message.error", null, locale)).build(),
- HttpStatus.OK);
- }
+ if (actuallyDeleteAccount) {
+ userService.deleteUser(user);
} else {
- return new ResponseEntity(
- JSONResponse.builder().success(false).code(2).message(messages.getMessage("message.error", null, locale)).build(), HttpStatus.OK);
+ user.setEnabled(false);
+ userService.saveRegisteredUser(user);
}
- }
+ logoutUser(request);
+ return buildSuccessResponse("Account Deleted", null);
+ }
+ // Helper Methods
/**
- * Updates a user's password.
+ * Validates the user data transfer object.
*
- * @param locale the locale
- * @param passwordDto the password dto
- * @return the generic response
+ * @param userDto the user data transfer object
*/
- // Change user password
- @PostMapping("/updatePassword")
- public ResponseEntity changeUserPassword(@AuthenticationPrincipal DSUserDetails userDetails, final Locale locale,
- @Valid PasswordDto passwordDto, final HttpServletRequest request) {
- if (userDetails == null || userDetails.getUser() == null) {
- log.error("UserAPI.changeUserPassword:" + "changeUserPassword called with null userDetails or user.");
- return new ResponseEntity(
- JSONResponse.builder().success(false).code(2).message(messages.getMessage("message.error", null, locale)).build(),
- HttpStatus.INTERNAL_SERVER_ERROR);
+ private void validateUserDto(UserDto userDto) {
+ if (isNullOrEmpty(userDto.getEmail())) {
+ throw new IllegalArgumentException("Email is required.");
}
- final User user = userDetails.getUser();
- // Check to see if the provided old password matches the current password
- if (!userService.checkIfValidOldPassword(user, passwordDto.getOldPassword())) {
- return new ResponseEntity(JSONResponse.builder().success(false).code(1).message("Invalid Old Password").build(),
- HttpStatus.UNAUTHORIZED);
-
+ if (isNullOrEmpty(userDto.getPassword())) {
+ throw new IllegalArgumentException("Password is required.");
}
- userService.changeUserPassword(user, passwordDto.getNewPassword());
-
- AuditEvent updatePasswordAuditEvent =
- AuditEvent.builder().source(this).user(user).sessionId(request.getSession().getId()).ipAddress(UserUtils.getClientIP(request))
- .userAgent(request.getHeader("User-Agent")).action("Update Save Password").actionStatus("Success").message("Success").build();
-
- eventPublisher.publishEvent(updatePasswordAuditEvent);
-
- return new ResponseEntity(
- JSONResponse.builder().success(true).code(0).message(messages.getMessage("message.updatePasswordSuccess", null, locale)).build(),
- HttpStatus.OK);
}
/**
- * Deletes the current user's account.
+ * Validates the authenticated user.
*
- * @param locale the locale
- * @param request the request
- * @return the generic response
+ * @param userDetails the authenticated user details
*/
- @DeleteMapping("/deleteAccount")
- public ResponseEntity deleteAccount(@AuthenticationPrincipal DSUserDetails userDetails, final Locale locale,
- final HttpServletRequest request) {
-
+ private void validateAuthenticatedUser(DSUserDetails userDetails) {
if (userDetails == null || userDetails.getUser() == null) {
- log.error("UserAPI.deleteAccount:" + "deleteAccount called with null userDetails or user.");
- return new ResponseEntity(
- JSONResponse.builder().success(false).code(2).message(messages.getMessage("message.error", null, locale)).build(),
- HttpStatus.INTERNAL_SERVER_ERROR);
+ throw new SecurityException("User not logged in.");
}
- final User user = userDetails.getUser();
+ }
- if (actuallyDeleteAccount) {
- userService.deleteUser(user);
- } else {
- user.setEnabled(false);
- userService.saveRegisteredUser(user);
- }
+ /**
+ * Handles the auto login of the user after registration.
+ *
+ * @param user the registered user
+ * @return the URI to redirect to after registration
+ */
+ private String handleAutoLogin(User user) {
+ userService.authWithoutPassword(user);
+ return registrationSuccessURI;
+ }
+
+ /**
+ * Logs out the user.
+ *
+ * @param request the HTTP servlet request
+ */
+ private void logoutUser(HttpServletRequest request) {
try {
SecurityContextHolder.clearContext();
request.logout();
} catch (ServletException e) {
- log.warn("UserAPI.deleteAccount:" + "Exception on logout!", e);
+ log.warn("Logout failed during account deletion.", e);
}
+ }
+
+ /**
+ * Publishes a registration event.
+ *
+ * @param user the registered user
+ * @param request the HTTP servlet request
+ */
+ private void publishRegistrationEvent(User user, HttpServletRequest request) {
+ String appUrl = request.getContextPath();
+ eventPublisher.publishEvent(new OnRegistrationCompleteEvent(user, request.getLocale(), appUrl));
+ }
+
+ /**
+ * Logs an audit event.
+ *
+ * @param action the action performed
+ * @param status the status of the action
+ * @param message the message describing the action
+ * @param user the user involved in the action
+ * @param request the HTTP servlet request
+ */
+ private void logAuditEvent(String action, String status, String message, User user, HttpServletRequest request) {
+ AuditEvent event =
+ AuditEvent.builder().source(this).user(user).sessionId(request.getSession().getId()).ipAddress(UserUtils.getClientIP(request))
+ .userAgent(request.getHeader("User-Agent")).action(action).actionStatus(status).message(message).build();
+ eventPublisher.publishEvent(event);
+ }
- return new ResponseEntity(JSONResponse.builder().success(true).message("Account Deleted").build(), HttpStatus.OK);
+ /**
+ * Checks if a string is null or empty.
+ *
+ * @param value
+ * @return true if the string is null or empty, false otherwise
+ */
+ private boolean isNullOrEmpty(String value) {
+ return value == null || value.isEmpty();
+ }
+
+ /**
+ * Builds an error response.
+ *
+ * @param message
+ * @param code
+ * @param status
+ * @return a ResponseEntity containing a JSONResponse with the error response
+ */
+ private ResponseEntity buildErrorResponse(String message, int code, HttpStatus status) {
+ return ResponseEntity.status(status).body(JSONResponse.builder().success(false).code(code).message(message).build());
+ }
+
+ /**
+ * Builds a success response.
+ *
+ * @param message
+ * @param redirectUrl
+ * @return a ResponseEntity containing a JSONResponse with the success response
+ */
+ private ResponseEntity buildSuccessResponse(String message, String redirectUrl) {
+ return ResponseEntity.ok(JSONResponse.builder().success(true).code(0).message(message).redirectUrl(redirectUrl).build());
}
}
diff --git a/src/main/java/com/digitalsanctuary/spring/user/api/package-info.java b/src/main/java/com/digitalsanctuary/spring/user/api/package-info.java
index 3845d26..4ddb65c 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/api/package-info.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/api/package-info.java
@@ -1 +1,7 @@
-package com.digitalsanctuary.spring.user.api;
\ No newline at end of file
+/**
+ * This package contains the API classes for the Spring User Framework.
+ *
+ * The classes in this package are responsible for providing the public API for user-related operations within the Spring User Framework.
+ *
+ */
+package com.digitalsanctuary.spring.user.api;
diff --git a/src/main/java/com/digitalsanctuary/spring/user/audit/AuditConfig.java b/src/main/java/com/digitalsanctuary/spring/user/audit/AuditConfig.java
new file mode 100644
index 0000000..c2441bb
--- /dev/null
+++ b/src/main/java/com/digitalsanctuary/spring/user/audit/AuditConfig.java
@@ -0,0 +1,41 @@
+package com.digitalsanctuary.spring.user.audit;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.PropertySource;
+import org.springframework.stereotype.Component;
+import lombok.Data;
+
+/**
+ * The AuditConfig class is a Spring Boot configuration class that provides properties for configuring user audit logging. This class is used to
+ * define properties that control the behavior of the audit logging, such as the log file path and the flush rate.
+ */
+@Data
+@Component
+@PropertySource("classpath:config/dsspringuserconfig.properties")
+@ConfigurationProperties(prefix = "user.audit")
+public class AuditConfig {
+
+ /**
+ * The enabled flag. If set to false, audit logging will be disabled.
+ */
+ private boolean logEvents;
+
+ /**
+ * The log file path. This is the path to the log file where audit events will be written. The path can be absolute or relative to the application
+ */
+ private String logFilePath;
+
+ /**
+ * The flush on write flag, if enabled, causes the BufferedWriter to be flushed on every log entry. This has a performance impact under heavy
+ * loads, but ensures events are written to the log file without delay. This is beneficial in development environments, or environments where the
+ * performance penalty is less important that ensuring events are not lost in case of JVM or server crash.
+ */
+ private boolean flushOnWrite;
+
+ /**
+ * The flush rate. This is the rate at which the audit log buffer is flushed to the log file. The value is in milliseconds and can be set to any
+ * positive integer. The default value is 1000 (1 second).
+ */
+ private int flushRate;
+
+}
diff --git a/src/main/java/com/digitalsanctuary/spring/user/event/AuditEvent.java b/src/main/java/com/digitalsanctuary/spring/user/audit/AuditEvent.java
similarity index 97%
rename from src/main/java/com/digitalsanctuary/spring/user/event/AuditEvent.java
rename to src/main/java/com/digitalsanctuary/spring/user/audit/AuditEvent.java
index 77df0e7..d3ee922 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/event/AuditEvent.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/audit/AuditEvent.java
@@ -1,4 +1,4 @@
-package com.digitalsanctuary.spring.user.event;
+package com.digitalsanctuary.spring.user.audit;
import java.util.Date;
import org.springframework.context.ApplicationEvent;
diff --git a/src/main/java/com/digitalsanctuary/spring/user/audit/AuditEventListener.java b/src/main/java/com/digitalsanctuary/spring/user/audit/AuditEventListener.java
new file mode 100644
index 0000000..3cc59ee
--- /dev/null
+++ b/src/main/java/com/digitalsanctuary/spring/user/audit/AuditEventListener.java
@@ -0,0 +1,40 @@
+package com.digitalsanctuary.spring.user.audit;
+
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * This class processes AuditEvents. This class writes the AuditEvent data to a text file on the server. You could easily change the logic to write to
+ * a database, send events to a REST API, or anything else.
+ *
+ * @see AuditEvent
+ */
+@Slf4j
+@Async
+@Component
+@RequiredArgsConstructor
+public class AuditEventListener {
+
+ private final AuditConfig auditConfig;
+
+ private final AuditLogWriter auditLogWriter;
+
+ /**
+ * Handle the AuditEvents.
+ *
+ * In this case we are writing the event data out to an audit log on the server, using pipe delimiters.
+ *
+ * @param event the event
+ */
+ @EventListener
+ public void onApplicationEvent(AuditEvent event) {
+ log.debug("AuditEventListener.onApplicationEvent: called with event: {}", event);
+ if (auditConfig.isLogEvents() && event != null) {
+ log.debug("AuditEventListener.onApplicationEvent: logging event...");
+ auditLogWriter.writeLog(event);
+ }
+ }
+}
diff --git a/src/main/java/com/digitalsanctuary/spring/user/audit/AuditLogWriter.java b/src/main/java/com/digitalsanctuary/spring/user/audit/AuditLogWriter.java
new file mode 100644
index 0000000..c06fb97
--- /dev/null
+++ b/src/main/java/com/digitalsanctuary/spring/user/audit/AuditLogWriter.java
@@ -0,0 +1,34 @@
+package com.digitalsanctuary.spring.user.audit;
+
+/**
+ * Interface for writing audit log
+ *
+ *
+ *
+ *
+ * Implementations of this interface are responsible for writing audit log messages to a log file or other destination.
+ *
+ *
+ * This can include writing to a file, writing to a database, sending messages to a REST API, SIEM or any other method of storing or transmitting
+ * audit log messages.
+ *
+ */
+public interface AuditLogWriter {
+
+ /**
+ * Write an audit log message
+ *
+ * @param event the audit event to log
+ */
+ void writeLog(AuditEvent event);
+
+ /**
+ * Setup the audit log writer
+ */
+ void setup();
+
+ /**
+ * Cleanup the audit log writer
+ */
+ void cleanup();
+}
diff --git a/src/main/java/com/digitalsanctuary/spring/user/audit/FileAuditLogFlushScheduler.java b/src/main/java/com/digitalsanctuary/spring/user/audit/FileAuditLogFlushScheduler.java
new file mode 100644
index 0000000..5f83715
--- /dev/null
+++ b/src/main/java/com/digitalsanctuary/spring/user/audit/FileAuditLogFlushScheduler.java
@@ -0,0 +1,33 @@
+package com.digitalsanctuary.spring.user.audit;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * The FileAuditLogFlushScheduler class is a Spring Boot component that flushes the audit log buffer to the file. This class is used to ensure that
+ * the audit log buffer is flushed periodically to balance performance with data integrity.
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+@ConditionalOnProperty(name = "user.audit.flushOnWrite", havingValue = "false")
+public class FileAuditLogFlushScheduler {
+
+ /**
+ * The file audit log writer. This is the writer that is used to write audit log events to the file.
+ */
+ private final FileAuditLogWriter fileAuditLogWriter;
+
+ /**
+ * Flushes the audit log buffer to the file. This method is called on a schedule to ensure that the buffer is flushed periodically to balance
+ * performance with data integrity.
+ */
+ @Scheduled(fixedRateString = "#{@auditConfig.flushRate}")
+ public void flushAuditLog() {
+ log.info("FileAuditLogFlushScheduler.flushAuditLog: Flushing audit log buffer to file.");
+ fileAuditLogWriter.flushWriter();
+ }
+}
diff --git a/src/main/java/com/digitalsanctuary/spring/user/audit/FileAuditLogWriter.java b/src/main/java/com/digitalsanctuary/spring/user/audit/FileAuditLogWriter.java
new file mode 100644
index 0000000..d82dc86
--- /dev/null
+++ b/src/main/java/com/digitalsanctuary/spring/user/audit/FileAuditLogWriter.java
@@ -0,0 +1,167 @@
+package com.digitalsanctuary.spring.user.audit;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.text.MessageFormat;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Implementation of {@link AuditLogWriter} that writes audit logs to a file. This class handles the lifecycle of the log file, including opening,
+ * writing, and closing the file. It also supports scheduled flushing of the buffer to balance performance with data integrity.
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class FileAuditLogWriter implements AuditLogWriter {
+
+ private final AuditConfig auditConfig;
+ private BufferedWriter bufferedWriter;
+
+ /**
+ * Initializes the log file writer. This method is called after the bean is constructed. It validates the configuration and opens the log file for
+ * writing.
+ */
+ @PostConstruct
+ @Override
+ public void setup() {
+ log.info("FileAuditLogWriter.setup: Entering...");
+ if (!validateConfig()) {
+ return;
+ }
+ openLogFile();
+ }
+
+ /**
+ * Cleans up the log file writer. This method is called before the bean is destroyed. It closes the log file to ensure all data is flushed and
+ * resources are released.
+ */
+ @PreDestroy
+ @Override
+ public void cleanup() {
+ log.info("FileAuditLogWriter.cleanup: Closing log file.");
+ closeLogFile();
+ }
+
+ /**
+ * Writes an audit event to the log file. The event data is formatted and written as a single line. If the buffered writer is not initialized, an
+ * error is logged.
+ *
+ * @param event the audit event to write
+ */
+ @Override
+ public void writeLog(AuditEvent event) {
+ if (bufferedWriter == null) {
+ log.error("FileAuditLogWriter.writeLog: BufferedWriter is not initialized.");
+ return;
+ }
+ try {
+ String userId = event.getUser() != null ? event.getUser().getId().toString() : null;
+ String userEmail = event.getUser() != null ? event.getUser().getEmail() : null;
+ String output = MessageFormat.format("{0}|{1}|{2}|{3}|{4}|{5}|{6}|{7}|{8}|{9}", event.getDate(), event.getAction(),
+ event.getActionStatus(), userId, userEmail, event.getIpAddress(), event.getSessionId(), event.getMessage(), event.getUserAgent(),
+ event.getExtraData());
+ bufferedWriter.write(output);
+ bufferedWriter.newLine();
+ if (auditConfig.isFlushOnWrite()) {
+ bufferedWriter.flush();
+ }
+ } catch (IOException e) {
+ log.error("FileAuditLogWriter.writeLog: IOException writing to log file: {}", auditConfig.getLogFilePath(), e);
+ }
+ }
+
+
+ /**
+ * Flushes the buffered writer to ensure all data is written to the log file. This method is called by the {@link FileAuditLogFlushScheduler} to
+ * ensure that the buffer is flushed periodically to balance performance with data integrity.
+ */
+ public void flushWriter() {
+ if (bufferedWriter != null) {
+ try {
+ bufferedWriter.flush();
+ } catch (IOException e) {
+ log.error("FileAuditLogWriter.flushWriter: IOException flushing buffer!", e);
+ }
+ }
+ }
+
+ /**
+ * Validates the audit configuration to ensure it is properly set up. Logs errors if the configuration is invalid.
+ *
+ * @return true if the configuration is valid, false otherwise
+ */
+ private boolean validateConfig() {
+ if (auditConfig == null) {
+ log.error("FileAuditLogWriter.setup: No AuditConfig has been configured!");
+ return false;
+ }
+ if (!auditConfig.isLogEvents()) {
+ log.info("FileAuditLogWriter.setup: Audit logging is disabled.");
+ return false;
+ }
+ if (!StringUtils.hasText(auditConfig.getLogFilePath())) {
+ log.error("FileAuditLogWriter.setup: No user.audit.logFilePath has been configured!");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Opens the log file for writing. If the file does not exist, it is created. If the file is newly created, a header is written to the file.
+ */
+ private void openLogFile() {
+ String logFilePath = auditConfig.getLogFilePath();
+ log.debug("FileAuditLogWriter.setup: Opening log file: {}", logFilePath);
+ try {
+ OpenOption[] fileOptions = {StandardOpenOption.CREATE, StandardOpenOption.APPEND, StandardOpenOption.WRITE};
+ boolean newFile = Files.notExists(Path.of(logFilePath));
+ bufferedWriter = Files.newBufferedWriter(Path.of(logFilePath), fileOptions);
+ if (newFile) {
+ writeHeader();
+ }
+ log.info("FileAuditLogWriter.setup: Log file opened.");
+ } catch (IOException e) {
+ log.error("FileAuditLogWriter.setup: IOException trying to open log file: {}", logFilePath, e);
+ }
+ }
+
+ /**
+ * Closes the log file to ensure all data is flushed and resources are released.
+ */
+ private void closeLogFile() {
+ try {
+ if (bufferedWriter != null) {
+ bufferedWriter.close();
+ }
+ } catch (IOException e) {
+ log.error("FileAuditLogWriter.cleanup: IOException closing log file: {}", auditConfig.getLogFilePath(), e);
+ }
+ }
+
+ /**
+ * Writes a header to the log file. This method is called when the log file is newly created.
+ */
+ private void writeHeader() {
+ log.debug("FileAuditLogWriter.writeHeader: writing header.");
+ if (bufferedWriter != null) {
+ String output = MessageFormat.format("{0}|{1}|{2}|{3}|{4}|{5}|{6}|{7}|{8}|{9}", "Date", "Action", "Action Status", "User ID", "Email",
+ "IP Address", "SessionId", "Message", "User Agent", "Extra Data");
+ try {
+ bufferedWriter.write(output);
+ bufferedWriter.newLine();
+ bufferedWriter.flush();
+ } catch (IOException e) {
+ log.error("FileAuditLogWriter.writeHeader: IOException writing header: {}", output, e);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/digitalsanctuary/spring/user/controller/PageController.java b/src/main/java/com/digitalsanctuary/spring/user/controller/PageController.java
deleted file mode 100644
index e9c27b1..0000000
--- a/src/main/java/com/digitalsanctuary/spring/user/controller/PageController.java
+++ /dev/null
@@ -1,70 +0,0 @@
-package com.digitalsanctuary.spring.user.controller;
-
-import java.util.Locale;
-import java.util.Optional;
-import org.springframework.context.MessageSource;
-import org.springframework.security.core.annotation.AuthenticationPrincipal;
-import org.springframework.stereotype.Controller;
-import org.springframework.ui.ModelMap;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import com.digitalsanctuary.spring.user.service.DSUserDetails;
-import jakarta.servlet.http.HttpServletRequest;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-
-/**
- * The Main Page Controller for pages outside of the actual User Management Framework.
- */
-@Slf4j
-@RequiredArgsConstructor
-@Controller
-public class PageController {
-
- private final MessageSource messages;
-
- /**
- * Home Page.
- *
- * @return the string
- */
- @GetMapping({"/", "/index.html"})
- public String index(@AuthenticationPrincipal DSUserDetails userDetails, final HttpServletRequest request, final ModelMap model,
- @RequestParam("messageKey") final Optional messageKey) {
- log.debug("PageController.index: called with messageKey={}", messageKey.orElse(""));
- // If the user is logged in, we'll add their details to the model
- if (userDetails != null) {
- log.debug("PageController.index: userDetails={}", userDetails);
- model.addAttribute("user", userDetails.getUser());
- }
-
- // If there is a messageKey GET param, we'll map that into a locale specific message and add that to the model
- Locale locale = request.getLocale();
- messageKey.ifPresent(key -> {
- String message = messages.getMessage(key, null, locale);
- model.addAttribute("message", message);
- });
- return "index";
- }
-
- /**
- * An example Protected page
- *
- * @return the string
- */
- @GetMapping("/protected.html")
- public String protectedPage() {
- return "protected";
- }
-
- /**
- * An example Unprotected page.
- *
- * @return the string
- */
- @GetMapping("/unprotected.html")
- public String unprotectedPage() {
- return "unprotected";
- }
-
-}
diff --git a/src/main/java/com/digitalsanctuary/spring/user/controller/UserActionController.java b/src/main/java/com/digitalsanctuary/spring/user/controller/UserActionController.java
index 9aad293..3f00041 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/controller/UserActionController.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/controller/UserActionController.java
@@ -10,7 +10,7 @@
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
-import com.digitalsanctuary.spring.user.event.AuditEvent;
+import com.digitalsanctuary.spring.user.audit.AuditEvent;
import com.digitalsanctuary.spring.user.persistence.model.User;
import com.digitalsanctuary.spring.user.service.UserService;
import com.digitalsanctuary.spring.user.service.UserService.TokenValidationResult;
@@ -58,6 +58,7 @@ public class UserActionController {
/**
* Validate a forgot password token link from an email, and if valid, show the change password page.
*
+ * @param request the request
* @param model the model
* @param token the token
* @return the model and view
@@ -113,7 +114,7 @@ public ModelAndView confirmRegistration(final HttpServletRequest request, final
eventPublisher.publishEvent(registrationAuditEvent);
}
- model.addAttribute("message", messages.getMessage("message.accountVerified", null, locale));
+ model.addAttribute("message", messages.getMessage("message.account.verified", null, locale));
log.debug("UserAPI.confirmRegistration: account verified and user logged in!");
String redirectString = "redirect:" + registrationSuccessURI;
return new ModelAndView(redirectString, model);
diff --git a/src/main/java/com/digitalsanctuary/spring/user/controller/UserPageController.java b/src/main/java/com/digitalsanctuary/spring/user/controller/UserPageController.java
index 54fd8e6..601f502 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/controller/UserPageController.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/controller/UserPageController.java
@@ -31,6 +31,9 @@ public class UserPageController {
* Login Page.
*
* @param userDetails the user details
+ * @param session the session
+ * @param model the model
+ *
* @return the string
*/
@GetMapping("/user/login.html")
@@ -48,6 +51,9 @@ public String login(@AuthenticationPrincipal DSUserDetails userDetails, HttpSess
/**
* Register Page.
*
+ * @param userDetails the user details
+ * @param session the session
+ * @param model the model
* @return the string
*/
@GetMapping("/user/register.html")
@@ -75,7 +81,9 @@ public String registrationPending() {
/**
* Registration complete.
*
- * @param userDetails
+ * @param userDetails the user details
+ * @param session the session
+ * @param model the model
*
* @return the string
*/
@@ -125,6 +133,13 @@ public String forgotPasswordChange() {
return "user/forgot-password-change";
}
+
+ /**
+ * @param userDetails the user details
+ * @param request the request
+ * @param model the model
+ * @return String
+ */
@GetMapping("/user/update-user.html")
public String updateUser(@AuthenticationPrincipal DSUserDetails userDetails, final HttpServletRequest request, final ModelMap model) {
if (userDetails != null) {
@@ -137,11 +152,21 @@ public String updateUser(@AuthenticationPrincipal DSUserDetails userDetails, fin
return "user/update-user";
}
+ /**
+ * Update password.
+ *
+ * @return the string
+ */
@GetMapping("/user/update-password.html")
public String updatePassword() {
return "user/update-password";
}
+ /**
+ * Delete account.
+ *
+ * @return the string
+ */
@GetMapping("/user/delete-account.html")
public String deleteAccount() {
return "user/delete-account";
diff --git a/src/main/java/com/digitalsanctuary/spring/user/controller/package-info.java b/src/main/java/com/digitalsanctuary/spring/user/controller/package-info.java
index d226a03..5708f08 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/controller/package-info.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/controller/package-info.java
@@ -1 +1,12 @@
-package com.digitalsanctuary.spring.user.controller;
\ No newline at end of file
+/**
+ * This package contains the controller classes for the Spring User Framework.
+ *
+ * The controllers in this package are responsible for handling HTTP requests and returning appropriate responses. They act as the entry point for the
+ * user-related operations and interact with the service layer to perform business logic.
+ *
+ *
+ * The controllers are designed to be RESTful and follow the principles of REST architecture. They provide endpoints for creating, updating,
+ * retrieving, and deleting user information.
+ *
+ */
+package com.digitalsanctuary.spring.user.controller;
diff --git a/src/main/java/com/digitalsanctuary/spring/user/dto/package-info.java b/src/main/java/com/digitalsanctuary/spring/user/dto/package-info.java
index c72be9a..dd0ddcc 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/dto/package-info.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/dto/package-info.java
@@ -1 +1,7 @@
-package com.digitalsanctuary.spring.user.dto;
\ No newline at end of file
+/**
+ * This package contains Data Transfer Object (DTO) classes for the Spring User Framework.
+ *
+ * DTOs are used to transfer data between different layers of the application. They are simple objects that should not contain any business logic.
+ *
+ */
+package com.digitalsanctuary.spring.user.dto;
diff --git a/src/main/java/com/digitalsanctuary/spring/user/exceptions/OAuth2AuthenticationProcessingException.java b/src/main/java/com/digitalsanctuary/spring/user/exceptions/OAuth2AuthenticationProcessingException.java
index 1c60a48..00934b7 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/exceptions/OAuth2AuthenticationProcessingException.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/exceptions/OAuth2AuthenticationProcessingException.java
@@ -1,17 +1,36 @@
package com.digitalsanctuary.spring.user.exceptions;
+/**
+ * Exception thrown when there is an error processing OAuth2 authentication.
+ */
public class OAuth2AuthenticationProcessingException extends RuntimeException {
private static final long serialVersionUID = 1L;
+ /**
+ * Constructs a new OAuth2AuthenticationProcessingException with the specified detail message and cause.
+ *
+ * @param message the detail message
+ * @param cause the cause of the exception
+ */
public OAuth2AuthenticationProcessingException(String message, Throwable cause) {
super(message, cause);
}
+ /**
+ * Constructs a new OAuth2AuthenticationProcessingException with the specified detail message.
+ *
+ * @param message the detail message
+ */
public OAuth2AuthenticationProcessingException(String message) {
super(message);
}
+ /**
+ * Constructs a new OAuth2AuthenticationProcessingException with the specified cause.
+ *
+ * @param cause the cause of the exception
+ */
public OAuth2AuthenticationProcessingException(Throwable cause) {
super(cause);
}
diff --git a/src/main/java/com/digitalsanctuary/spring/user/exceptions/package-info.java b/src/main/java/com/digitalsanctuary/spring/user/exceptions/package-info.java
index 046cc27..8f230b2 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/exceptions/package-info.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/exceptions/package-info.java
@@ -1 +1,8 @@
-package com.digitalsanctuary.spring.user.exceptions;
\ No newline at end of file
+/**
+ * This package contains custom exception classes for the Spring User Framework.
+ *
+ * The exceptions in this package are used to handle various error conditions that may occur within the user management functionality of the
+ * framework.
+ *
+ */
+package com.digitalsanctuary.spring.user.exceptions;
diff --git a/src/main/java/com/digitalsanctuary/spring/user/jobs/package-info.java b/src/main/java/com/digitalsanctuary/spring/user/jobs/package-info.java
index 910a63d..de1aa5f 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/jobs/package-info.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/jobs/package-info.java
@@ -1 +1,22 @@
-package com.digitalsanctuary.spring.user.jobs;
\ No newline at end of file
+/**
+ * This package contains job-related classes and interfaces for the Spring User Framework.
+ *
+ *
+ * The classes in this package are responsible for handling various job operations within the Spring User Framework, such as scheduling, execution,
+ * and management of jobs.
+ *
+ *
+ *
+ * The main functionalities provided by this package include:
+ *
+ *
Job scheduling and execution
+ *
Job management and monitoring
+ *
Integration with other components of the Spring User Framework
+ *
+ *
+ *
+ *
+ * This package is part of the Digital Sanctuary's Spring User Framework project.
+ *
+ */
+package com.digitalsanctuary.spring.user.jobs;
diff --git a/src/main/java/com/digitalsanctuary/spring/user/listener/AuditEventListener.java b/src/main/java/com/digitalsanctuary/spring/user/listener/AuditEventListener.java
deleted file mode 100644
index 6efc5fc..0000000
--- a/src/main/java/com/digitalsanctuary/spring/user/listener/AuditEventListener.java
+++ /dev/null
@@ -1,165 +0,0 @@
-package com.digitalsanctuary.spring.user.listener;
-
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.OpenOption;
-import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
-import java.text.MessageFormat;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.context.event.EventListener;
-import org.springframework.scheduling.annotation.Async;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Component;
-import org.springframework.util.StringUtils;
-import com.digitalsanctuary.spring.user.event.AuditEvent;
-import jakarta.annotation.PostConstruct;
-import jakarta.annotation.PreDestroy;
-import lombok.extern.slf4j.Slf4j;
-
-/**
- * This class processes AuditEvents. This class writes the AuditEvent data to a text file on the server. You could easily change the logic to write to
- * a database, send events to a REST API, or anything else.
- *
- * @see AuditEvent
- */
-@Slf4j
-@Async
-@Component
-public class AuditEventListener {
-
- /** The logEvents flag. Set to true to log audit events. */
- @Value("${user.audit.logEvents:false}")
- private boolean logEvents;
-
- /** The audit log file path. */
- @Value("${user.audit.logFilePath:}")
- private String logFilePath;
-
- /**
- * The flush on write flag, if enabled, causes the BufferedWriter to be flushed on every log entry. This has a performance impact under heavy
- * loads, but ensures events are written to the log file without delay. This is beneficial in development environments, or environments where the
- * performance penalty is less important that ensuring events are not lost in case of JVM or server crash.
- */
- @Value("${user.audit.flushOnWrite:false}")
- private boolean flushOnWrite;
-
- /** The buffered writer. This gets instantiated by the setup method. */
- private BufferedWriter bufferedWriter;
-
- /**
- * Setup the service, opening the log file for writing, and if the file is new, write a header line first.
- */
- @PostConstruct
- private void setup() {
- log.info("AuditEventListener.setup: Entering...");
- if (logEvents) {
- if (!StringUtils.hasText(logFilePath)) {
- log.error("AuditEventListener.setup: user.audit.logEvents is true, but no user.audit.logFilePath has been configured!");
- } else {
- log.debug("AuditEventListener.setup: Opening log file: {}", logFilePath);
- try {
- OpenOption[] fileOptions = {StandardOpenOption.CREATE, StandardOpenOption.APPEND, StandardOpenOption.WRITE};
- boolean newFile = false;
- if (Files.notExists(Path.of(logFilePath))) {
- newFile = true;
- }
- bufferedWriter = Files.newBufferedWriter(Path.of(logFilePath), fileOptions);
- if (newFile) {
- writeHeader();
- }
- log.info("AuditEventListener.setup: Log file opened.");
- } catch (IOException e) {
- log.error("AuditEventListener.setup: IOException trying to open log file: {}", logFilePath, e);
- }
- }
- }
- }
-
- /**
- * Write a field header line to the start of a log file.
- */
- private void writeHeader() {
- log.debug("AuditEventListener.writeHeader: writing header.");
- if (bufferedWriter != null) {
- String output = MessageFormat.format("{0}|{1}|{2}|{3}|{4}|{5}|{6}|{7}|{8}|{9}", "Date", "Action", "Action Status", "User ID", "Email",
- "IP Address", "SessionId", "Message", "User Agent", "Extra Data");
- try {
- bufferedWriter.write(output);
- bufferedWriter.newLine();
- bufferedWriter.flush();
- } catch (IOException e) {
- log.error("AuditEventListener.onApplicationEvent: IOException writing line: {}", output, e);
- }
- }
- }
-
- /**
- * Teardown the service, closing the file writer.
- */
- @PreDestroy
- public void teardown() {
- if (logEvents) {
- if (bufferedWriter != null) {
- log.debug("AuditEventListener.teardown: Closing log file: {}", logFilePath);
- try {
- bufferedWriter.close();
- log.debug("AuditEventListener.teardown: Log file closed.");
- } catch (IOException e) {
- log.error("AuditEventListener.teardown: IOException while trying to close bufferedWriter!", e);
- }
- }
- }
- }
-
- /**
- * Flush writer on schedule to balance performance with getting data written to the audit log.
- */
- @Scheduled(fixedDelay = 30000, initialDelay = 30000)
- public void flushWriterOnSchedule() {
- if (bufferedWriter != null && !flushOnWrite) {
- try {
- bufferedWriter.flush();
- } catch (IOException e) {
- log.error("AuditEventListener.flushWriterOnSchedule: IOException flushing buffer!", e);
- }
- }
- }
-
- /**
- * Handle the AuditEvents.
- *
- * In this case we are writing the event data out to an audit log on the server, using pipe delimiters.
- *
- * @param event the event
- */
- @EventListener
- public void onApplicationEvent(AuditEvent event) {
- log.debug("AuditEventListener.onApplicationEvent: called with event: {}", event);
- if (logEvents && bufferedWriter != null && event != null) {
- log.debug("AuditEventListener.onApplicationEvent: logging event...");
- String userId = null;
- String userEmail = null;
- // If the event has a User object on it, we'll get some data from it
- if (event.getUser() != null) {
- userId = event.getUser().getId().toString();
- userEmail = event.getUser().getEmail();
- }
- String output = MessageFormat.format("{0}|{1}|{2}|{3}|{4}|{5}|{6}|{7}|{8}|{9}", event.getDate(), event.getAction(),
- event.getActionStatus(), userId, userEmail, event.getIpAddress(), event.getSessionId(), event.getMessage(), event.getUserAgent(),
- event.getExtraData());
-
- log.debug("AuditEventListener.onApplicationEvent: output: {}", output);
- try {
- bufferedWriter.write(output);
- bufferedWriter.newLine();
- if (flushOnWrite) {
- bufferedWriter.flush();
- }
- } catch (IOException e) {
- log.error("AuditEventListener.onApplicationEvent: IOException writing line: {}", output, e);
- }
- }
- }
-}
diff --git a/src/main/java/com/digitalsanctuary/spring/user/listener/AuthenticationEventLIstener.java b/src/main/java/com/digitalsanctuary/spring/user/listener/AuthenticationEventLIstener.java
index 14f9119..109a405 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/listener/AuthenticationEventLIstener.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/listener/AuthenticationEventLIstener.java
@@ -10,7 +10,7 @@
/**
* This class is used to listen for authentication events and handle account lockout functionality if needed.
- *
+ *
* https://github.com/devondragon/SpringUserFramework/issues/29
*/
@Slf4j
@@ -20,6 +20,11 @@ public class AuthenticationEventLIstener {
final private LoginAttemptService loginAttemptService;
+ /**
+ * This method listens for successful authentications and handles account lockout functionality.
+ *
+ * @param success the success event
+ */
@EventListener
public void onSuccess(AuthenticationSuccessEvent success) {
// Handle successful authentication, e.g. logging or auditing
@@ -28,6 +33,11 @@ public void onSuccess(AuthenticationSuccessEvent success) {
loginAttemptService.loginSucceeded(username);
}
+ /**
+ * This method listens for authentication failures and handles account lockout functionality.
+ *
+ * @param failure the failure event
+ */
@EventListener
public void onFailure(AbstractAuthenticationFailureEvent failure) {
// Handle unsuccessful authentication, e.g. logging or auditing
diff --git a/src/main/java/com/digitalsanctuary/spring/user/mail/package-info.java b/src/main/java/com/digitalsanctuary/spring/user/mail/package-info.java
index 88a5a26..ce7601b 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/mail/package-info.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/mail/package-info.java
@@ -1 +1,32 @@
-package com.digitalsanctuary.spring.user.mail;
\ No newline at end of file
+/**
+ * This package contains classes and interfaces related to the mailing functionality within the Spring User Framework. It provides the necessary
+ * components to handle email operations such as sending and receiving emails.
+ *
+ *
+ * Key components include:
+ *
+ *
+ *
EmailService: A service interface for email operations.
+ *
EmailServiceImpl: An implementation of the EmailService interface.
+ *
EmailTemplate: A class representing email templates used for sending emails.
+ * Ensure that the necessary email server configurations are provided in the application properties file to enable the email functionality.
+ *
+ */
+package com.digitalsanctuary.spring.user.mail;
diff --git a/src/main/java/com/digitalsanctuary/spring/user/persistence/model/Privilege.java b/src/main/java/com/digitalsanctuary/spring/user/persistence/model/Privilege.java
index 6910dba..3e8aca4 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/persistence/model/Privilege.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/persistence/model/Privilege.java
@@ -32,15 +32,29 @@ public class Privilege {
@ManyToMany(mappedBy = "privileges")
private Collection roles;
+ /**
+ * Instantiates a new privilege.
+ */
public Privilege() {
super();
}
+ /**
+ * Instantiates a new privilege.
+ *
+ * @param name the name
+ */
public Privilege(final String name) {
super();
this.name = name;
}
+ /**
+ * Instantiates a new privilege.
+ *
+ * @param name the name
+ * @param description the description
+ */
public Privilege(final String name, final String description) {
super();
this.name = name;
diff --git a/src/main/java/com/digitalsanctuary/spring/user/persistence/model/Role.java b/src/main/java/com/digitalsanctuary/spring/user/persistence/model/Role.java
index 00f46a3..6523074 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/persistence/model/Role.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/persistence/model/Role.java
@@ -42,15 +42,29 @@ public class Role {
private String description;
+ /**
+ * Instantiates a new role.
+ */
public Role() {
super();
}
+ /**
+ * Instantiates a new role.
+ *
+ * @param name the name
+ */
public Role(final String name) {
super();
this.name = name;
}
+ /**
+ * Instantiates a new role.
+ *
+ * @param name the name
+ * @param description the description
+ */
public Role(final String name, final String description) {
super();
this.name = name;
diff --git a/src/main/java/com/digitalsanctuary/spring/user/persistence/model/User.java b/src/main/java/com/digitalsanctuary/spring/user/persistence/model/User.java
index 4ea552e..508fcd8 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/persistence/model/User.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/persistence/model/User.java
@@ -18,8 +18,29 @@
@Table(name = "user_account")
public class User {
+ /**
+ * Enum representing the available login providers.
+ */
public enum Provider {
- LOCAL, FACEBOOK, GOOGLE, APPLE
+ /**
+ * Local authentication, typically using a username and password stored in the application's database.
+ */
+ LOCAL,
+
+ /**
+ * Login using Facebook as the authentication provider.
+ */
+ FACEBOOK,
+
+ /**
+ * Login using Google as the authentication provider.
+ */
+ GOOGLE,
+
+ /**
+ * Login using Apple as the authentication provider.
+ */
+ APPLE
}
/** The id. */
@@ -88,6 +109,11 @@ public void setLastActivityDate() {
setLastActivityDate(new Date());
}
+ /**
+ * Gets the full name.
+ *
+ * @return the full name
+ */
public String getFullName() {
return firstName + " " + lastName;
}
diff --git a/src/main/java/com/digitalsanctuary/spring/user/persistence/model/package-info.java b/src/main/java/com/digitalsanctuary/spring/user/persistence/model/package-info.java
index f7ff647..6d136f5 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/persistence/model/package-info.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/persistence/model/package-info.java
@@ -1 +1,22 @@
-package com.digitalsanctuary.spring.user.persistence.model;
\ No newline at end of file
+/**
+ * This package contains the model classes for the user persistence layer of the Spring User Framework.
+ *
+ *
+ * The classes in this package are responsible for representing the data structures and entities that are used to persist user information in the
+ * database.
+ *
+ *
+ *
+ * These model classes typically include annotations for ORM (Object-Relational Mapping) frameworks such as JPA (Java Persistence API) to facilitate
+ * the mapping of Java objects to database tables.
+ *
+ *
+ *
+ * The package is part of the larger Spring User Framework, which provides a comprehensive solution for user management, including registration,
+ * authentication, and authorization.
+ *
+ *
+ * @since 1.0
+ * @author Devon Hillard
+ */
+package com.digitalsanctuary.spring.user.persistence.model;
diff --git a/src/main/java/com/digitalsanctuary/spring/user/persistence/package-info.java b/src/main/java/com/digitalsanctuary/spring/user/persistence/package-info.java
deleted file mode 100644
index 1e6f8cf..0000000
--- a/src/main/java/com/digitalsanctuary/spring/user/persistence/package-info.java
+++ /dev/null
@@ -1 +0,0 @@
-package com.digitalsanctuary.spring.user.persistence;
\ No newline at end of file
diff --git a/src/main/java/com/digitalsanctuary/spring/user/persistence/repository/package-info.java b/src/main/java/com/digitalsanctuary/spring/user/persistence/repository/package-info.java
index 25c0fdd..2a5bf30 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/persistence/repository/package-info.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/persistence/repository/package-info.java
@@ -1 +1,30 @@
-package com.digitalsanctuary.spring.user.persistence.repository;
\ No newline at end of file
+/**
+ * This package contains the repository interfaces for the Spring User Framework.
+ *
+ *
+ * The repository interfaces are responsible for providing CRUD operations and other database interactions for the user-related entities.
+ *
+ *
+ *
+ * The repositories in this package extend Spring Data JPA repositories, leveraging Spring Data's powerful features for data access.
+ *
+ *
+ *
+ * Example usage:
+ *
+ *
+ * {@code
+ * @Autowired
+ * private UserRepository userRepository;
+ *
+ * public void someMethod() {
+ * User user = userRepository.findById(1L).orElse(null);
+ * // perform operations with the user
+ * }
+ * }
+ *
+ *
+ *
+ * @see org.springframework.data.jpa.repository.JpaRepository
+ */
+package com.digitalsanctuary.spring.user.persistence.repository;
diff --git a/src/main/java/com/digitalsanctuary/spring/user/service/RolePrivilegeSetupService.java b/src/main/java/com/digitalsanctuary/spring/user/roles/RolePrivilegeSetupService.java
similarity index 96%
rename from src/main/java/com/digitalsanctuary/spring/user/service/RolePrivilegeSetupService.java
rename to src/main/java/com/digitalsanctuary/spring/user/roles/RolePrivilegeSetupService.java
index 5d65ca2..b169a73 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/service/RolePrivilegeSetupService.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/roles/RolePrivilegeSetupService.java
@@ -1,4 +1,4 @@
-package com.digitalsanctuary.spring.user.service;
+package com.digitalsanctuary.spring.user.roles;
import java.util.HashSet;
import java.util.List;
@@ -11,7 +11,6 @@
import com.digitalsanctuary.spring.user.persistence.model.Role;
import com.digitalsanctuary.spring.user.persistence.repository.PrivilegeRepository;
import com.digitalsanctuary.spring.user.persistence.repository.RoleRepository;
-import com.digitalsanctuary.spring.user.util.RolesAndPrivilegesConfig;
import jakarta.transaction.Transactional;
import lombok.Data;
import lombok.RequiredArgsConstructor;
diff --git a/src/main/java/com/digitalsanctuary/spring/user/util/RolesAndPrivilegesConfig.java b/src/main/java/com/digitalsanctuary/spring/user/roles/RolesAndPrivilegesConfig.java
similarity index 56%
rename from src/main/java/com/digitalsanctuary/spring/user/util/RolesAndPrivilegesConfig.java
rename to src/main/java/com/digitalsanctuary/spring/user/roles/RolesAndPrivilegesConfig.java
index 28598ae..4402221 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/util/RolesAndPrivilegesConfig.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/roles/RolesAndPrivilegesConfig.java
@@ -13,7 +13,7 @@
* @version 1.0
* @since 1.0
*/
-package com.digitalsanctuary.spring.user.util;
+package com.digitalsanctuary.spring.user.roles;
import java.util.ArrayList;
import java.util.HashMap;
@@ -21,15 +21,36 @@
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.PropertySource;
+import org.springframework.stereotype.Component;
import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+/**
+ * The RolesAndPrivilegesConfig class is a Spring Boot configuration class that provides properties for configuring user roles and privileges. This
+ * class is used to define properties that control the behavior of the user roles and privileges, such as the role hierarchy and the mapping of roles
+ * to privileges.
+ */
+@Slf4j
@Data
-@Configuration
-@ConfigurationProperties(prefix = "user")
+@Component
+@PropertySource("classpath:config/dsspringuserconfig.properties")
+@ConfigurationProperties(prefix = "user.roles")
public class RolesAndPrivilegesConfig {
+ /**
+ * The roles and privileges map. This map defines the roles and their associated privileges. The map is structured as follows:
+ *
+ *
Key: the role name
+ *
Value: a list of privilege names
+ *
+ *
+ */
private Map> rolesAndPrivileges = new HashMap<>();
+ /**
+ * The role hierarchy list. This list defines the hierarchy of roles. Each role relationship is defined as a string in the format
+ * {@code "role1 > role2"}, where {@code role1} is the parent role and {@code role2} is the child role.
+ */
private List roleHierarchy = new ArrayList<>();
/**
@@ -39,6 +60,7 @@ public class RolesAndPrivilegesConfig {
* @return a formatted string representation of the role hierarchy, or {@code null} if the hierarchy is empty or {@code null}
*/
public String getRoleHierarchyString() {
+ log.info("roleHierarchy: {}", roleHierarchy);
if (roleHierarchy == null || roleHierarchy.isEmpty()) {
return null;
}
diff --git a/src/main/java/com/digitalsanctuary/spring/user/util/CustomOAuth2AuthenticationEntryPoint.java b/src/main/java/com/digitalsanctuary/spring/user/security/CustomOAuth2AuthenticationEntryPoint.java
similarity index 69%
rename from src/main/java/com/digitalsanctuary/spring/user/util/CustomOAuth2AuthenticationEntryPoint.java
rename to src/main/java/com/digitalsanctuary/spring/user/security/CustomOAuth2AuthenticationEntryPoint.java
index 114b937..6bb05a9 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/util/CustomOAuth2AuthenticationEntryPoint.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/security/CustomOAuth2AuthenticationEntryPoint.java
@@ -1,4 +1,4 @@
-package com.digitalsanctuary.spring.user.util;
+package com.digitalsanctuary.spring.user.security;
import java.io.IOException;
import org.springframework.security.core.AuthenticationException;
@@ -10,16 +10,36 @@
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
+/**
+ * The CustomOAuth2AuthenticationEntryPoint class is used to handle OAuth2 authentication exceptions. This class will redirect the user to the login
+ * page if an exception occurs during the OAuth2 authentication process.
+ */
@Slf4j
public class CustomOAuth2AuthenticationEntryPoint implements AuthenticationEntryPoint {
private final AuthenticationFailureHandler failureHandler;
private final String redirectURL;
+ /**
+ * Instantiates a new custom OAuth2 authentication entry point.
+ *
+ * @param failureHandler the failure handler
+ * @param redirectURL the redirect URL
+ */
public CustomOAuth2AuthenticationEntryPoint(AuthenticationFailureHandler failureHandler, String redirectURL) {
this.failureHandler = failureHandler;
this.redirectURL = redirectURL;
}
+ /**
+ * Commence. This method is called when an exception occurs during the OAuth2 authentication process. It will redirect the user to the login page
+ * if an exception occurs.
+ *
+ * @param request the request
+ * @param response the response
+ * @param authException the auth exception
+ * @throws IOException Signals that an I/O exception has occurred.
+ * @throws ServletException the servlet exception
+ */
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException,
ServletException {
diff --git a/src/main/java/com/digitalsanctuary/spring/user/util/WebSecurityConfig.java b/src/main/java/com/digitalsanctuary/spring/user/security/WebSecurityConfig.java
similarity index 81%
rename from src/main/java/com/digitalsanctuary/spring/user/util/WebSecurityConfig.java
rename to src/main/java/com/digitalsanctuary/spring/user/security/WebSecurityConfig.java
index a079018..a3bacbe 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/util/WebSecurityConfig.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/security/WebSecurityConfig.java
@@ -1,4 +1,4 @@
-package com.digitalsanctuary.spring.user.util;
+package com.digitalsanctuary.spring.user.security;
import static org.springframework.security.config.Customizer.withDefaults;
import java.util.ArrayList;
@@ -18,7 +18,6 @@
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
-import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.core.userdetails.UserDetailsService;
@@ -28,6 +27,7 @@
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.session.HttpSessionEventPublisher;
+import com.digitalsanctuary.spring.user.roles.RolesAndPrivilegesConfig;
import com.digitalsanctuary.spring.user.service.DSOAuth2UserService;
import com.digitalsanctuary.spring.user.service.LoginSuccessService;
import com.digitalsanctuary.spring.user.service.LogoutSuccessService;
@@ -36,6 +36,11 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+/**
+ * The WebSecurityConfig class is a Spring Boot configuration class that provides properties for configuring the web security. This class is used to
+ * define properties that control the behavior of the web security, such as the default action for protected URIs and the URIs that are protected or
+ * unprotected.
+ */
@Slf4j
@Data
@EqualsAndHashCode(callSuper = false)
@@ -44,6 +49,7 @@
@EnableWebSecurity
public class WebSecurityConfig {
+
private static final String DEFAULT_ACTION_DENY = "deny";
private static final String DEFAULT_ACTION_ALLOW = "allow";
@@ -180,12 +186,13 @@ private void setupOAuth2(HttpSecurity http) throws Exception {
}).userInfoEndpoint(userInfo -> userInfo.userService(dsOAuth2UserService)));
}
- @Bean
- public WebSecurityCustomizer webSecurityCustomizer() {
- // Ignore the error endpoint. This can get caught in the auth filter chain from a failed static asset request and cause a bad redirect on a
- // successful auth
- return (web) -> web.ignoring().requestMatchers("/error", "/ignore2");
- }
+ // Commenting this out to try adding /error to the unprotected URIs list instead
+ // @Bean
+ // public WebSecurityCustomizer webSecurityCustomizer() {
+ // // Ignore the error endpoint. This can get caught in the auth filter chain from a failed static asset request and cause a bad redirect on a
+ // // successful auth
+ // return (web) -> web.ignoring().requestMatchers("/error");
+ // }
private List getUnprotectedURIsList() {
// Add the required user pages and actions to the unprotectedURIsArray
@@ -205,6 +212,11 @@ private List getUnprotectedURIsList() {
return unprotectedURIs;
}
+ /**
+ * The authProvider method creates a DaoAuthenticationProvider and sets the UserDetailsService and PasswordEncoder for the provider.
+ *
+ * @return the DaoAuthenticationProvider object
+ */
@Bean
public DaoAuthenticationProvider authProvider() {
final DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
@@ -213,23 +225,51 @@ public DaoAuthenticationProvider authProvider() {
return authProvider;
}
+ /**
+ * The encoder method creates a BCryptPasswordEncoder with the bcryptStrength value.
+ *
+ * @return the BCryptPasswordEncoder object
+ */
@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder(bcryptStrength);
}
+ /**
+ * The sessionRegistry method creates a SessionRegistryImpl object.
+ *
+ * @return the SessionRegistryImpl object
+ */
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
+ /**
+ * The roleHierarchy method creates a RoleHierarchyImpl object from the roleHierarchyString in the rolesAndPrivilegesConfig object.
+ *
+ * @return the RoleHierarchyImpl object
+ */
@Bean
public RoleHierarchy roleHierarchy() {
+ if (rolesAndPrivilegesConfig == null) {
+ log.error("WebSecurityConfig.roleHierarchy: rolesAndPrivilegesConfig is null!");
+ return null;
+ }
+ if (rolesAndPrivilegesConfig.getRoleHierarchyString() == null) {
+ log.error("WebSecurityConfig.roleHierarchy: rolesAndPrivilegesConfig.getRoleHierarchyString() is null!");
+ return null;
+ }
RoleHierarchyImpl roleHierarchy = RoleHierarchyImpl.fromHierarchy(rolesAndPrivilegesConfig.getRoleHierarchyString());
log.debug("WebSecurityConfig.roleHierarchy: roleHierarchy: {}", roleHierarchy.toString());
return roleHierarchy;
}
+ /**
+ * The webExpressionHandler method creates a DefaultWebSecurityExpressionHandler object and sets the roleHierarchy for the handler.
+ *
+ * @return the DefaultWebSecurityExpressionHandler object
+ */
@Bean
public SecurityExpressionHandler webExpressionHandler() {
DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler = new DefaultWebSecurityExpressionHandler();
@@ -237,6 +277,11 @@ public SecurityExpressionHandler webExpressionHandler() {
return defaultWebSecurityExpressionHandler;
}
+ /**
+ * The httpSessionEventPublisher method creates an HttpSessionEventPublisher object.
+ *
+ * @return the HttpSessionEventPublisher object
+ */
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
@@ -246,7 +291,7 @@ public HttpSessionEventPublisher httpSessionEventPublisher() {
* This is required to publish authentication events to the Spring event system. This allows us to listen for authentication events and perform
* actions based on successful or failed authentication.
*
- * @param applicationEventPublisher
+ * @param applicationEventPublisher the Spring ApplicationEventPublisher
* @return the Spring Security default AuthenticationEventPublisher
*/
@Bean
diff --git a/src/main/java/com/digitalsanctuary/spring/user/service/DSOAuth2UserService.java b/src/main/java/com/digitalsanctuary/spring/user/service/DSOAuth2UserService.java
index 5833cf2..f034fca 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/service/DSOAuth2UserService.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/service/DSOAuth2UserService.java
@@ -7,7 +7,6 @@
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
-import com.digitalsanctuary.spring.user.exceptions.OAuth2AuthenticationProcessingException;
import com.digitalsanctuary.spring.user.persistence.model.User;
import com.digitalsanctuary.spring.user.persistence.repository.UserRepository;
import lombok.RequiredArgsConstructor;
@@ -32,7 +31,6 @@
* @see org.springframework.security.core.userdetails.User
* @see com.digitalsanctuary.spring.user.persistence.model.User
* @see com.digitalsanctuary.spring.user.persistence.repository.UserRepository
- * @see com.digitalsanctuary.spring.user.exceptions.OAuth2AuthenticationProcessingException
*/
@Slf4j
@Service
@@ -52,8 +50,6 @@ public class DSOAuth2UserService implements OAuth2UserService grantedAuthorities;
+ /** The attributes. */
private Map attributes;
/**
diff --git a/src/main/java/com/digitalsanctuary/spring/user/service/DSUserDetailsService.java b/src/main/java/com/digitalsanctuary/spring/user/service/DSUserDetailsService.java
index 8896178..a6f6e03 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/service/DSUserDetailsService.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/service/DSUserDetailsService.java
@@ -16,15 +16,14 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-
-@Slf4j
-@RequiredArgsConstructor
-@Service
-@Transactional
/**
* DSUserDetailsService is an implementation of Spring Security's UserDetailsService. It is responsible for loading user-specific data during
* authentication.
*/
+@Slf4j
+@RequiredArgsConstructor
+@Service
+@Transactional
public class DSUserDetailsService implements UserDetailsService {
/** The user repository. */
@@ -42,7 +41,6 @@ public class DSUserDetailsService implements UserDetailsService {
* @param email the email address
* @return the user details object
* @throws UsernameNotFoundException if no user is found with the provided email address
- * @throws CustomBlockedException if the request is coming from a blocked IP address
*/
@Override
public DSUserDetails loadUserByUsername(final String email) throws UsernameNotFoundException {
diff --git a/src/main/java/com/digitalsanctuary/spring/user/service/LoginAttemptService.java b/src/main/java/com/digitalsanctuary/spring/user/service/LoginAttemptService.java
index 5490cfa..78f3106 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/service/LoginAttemptService.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/service/LoginAttemptService.java
@@ -26,7 +26,7 @@ public class LoginAttemptService {
/** The max failed login attempts on a given account before it is locked. A value of 0 will disable locking accounts based on failed logins. */
@Value("${user.security.failedLoginAttempts}")
- private int failedLoginAttempts;
+ private int maxFailedLoginAttempts;
/**
* The account lockout duration. A value less than 0 means accounts can only be unlocked by action, not duration. A value of 0 means account
@@ -60,7 +60,7 @@ public void loginSucceeded(final String email) {
@Transactional
public void loginFailed(final String email) {
log.debug("Login attempt failed for user: {}", email);
- if (failedLoginAttempts > 0) {
+ if (maxFailedLoginAttempts > 0) {
User user = userRepository.findByEmail(email);
if (user != null) {
incrementFailedAttempts(user);
@@ -78,7 +78,7 @@ public void loginFailed(final String email) {
private void incrementFailedAttempts(User user) {
int currentAttempts = user.getFailedLoginAttempts();
user.setFailedLoginAttempts(++currentAttempts);
- if (currentAttempts >= failedLoginAttempts) {
+ if (currentAttempts >= maxFailedLoginAttempts) {
user.setLocked(true);
user.setLockedDate(new Date());
}
@@ -109,9 +109,9 @@ public boolean isLocked(final String email) {
/**
* Check if user should be unlocked, and unlock the user if necessary.
- *
- * @param user
- * @return
+ *
+ * @param user the user
+ * @return the user
*/
public User checkIfUserShouldBeUnlocked(User user) {
log.debug("Checking if user should be unlocked: {}", user.getEmail());
diff --git a/src/main/java/com/digitalsanctuary/spring/user/service/LoginSuccessService.java b/src/main/java/com/digitalsanctuary/spring/user/service/LoginSuccessService.java
index 1baf4f3..16dc53e 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/service/LoginSuccessService.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/service/LoginSuccessService.java
@@ -7,7 +7,7 @@
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Service;
import org.thymeleaf.util.StringUtils;
-import com.digitalsanctuary.spring.user.event.AuditEvent;
+import com.digitalsanctuary.spring.user.audit.AuditEvent;
import com.digitalsanctuary.spring.user.persistence.model.User;
import com.digitalsanctuary.spring.user.util.UserUtils;
import jakarta.servlet.ServletException;
diff --git a/src/main/java/com/digitalsanctuary/spring/user/service/LogoutSuccessService.java b/src/main/java/com/digitalsanctuary/spring/user/service/LogoutSuccessService.java
index fb234c7..5b78358 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/service/LogoutSuccessService.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/service/LogoutSuccessService.java
@@ -7,7 +7,7 @@
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import org.springframework.stereotype.Service;
import org.thymeleaf.util.StringUtils;
-import com.digitalsanctuary.spring.user.event.AuditEvent;
+import com.digitalsanctuary.spring.user.audit.AuditEvent;
import com.digitalsanctuary.spring.user.persistence.model.User;
import com.digitalsanctuary.spring.user.util.UserUtils;
import jakarta.servlet.ServletException;
diff --git a/src/main/java/com/digitalsanctuary/spring/user/service/UserEmailService.java b/src/main/java/com/digitalsanctuary/spring/user/service/UserEmailService.java
index 2c7f112..3f809b3 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/service/UserEmailService.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/service/UserEmailService.java
@@ -5,7 +5,7 @@
import java.util.UUID;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
-import com.digitalsanctuary.spring.user.event.AuditEvent;
+import com.digitalsanctuary.spring.user.audit.AuditEvent;
import com.digitalsanctuary.spring.user.mail.MailService;
import com.digitalsanctuary.spring.user.persistence.model.PasswordResetToken;
import com.digitalsanctuary.spring.user.persistence.model.User;
@@ -13,6 +13,9 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+/**
+ * The UserEmailService class provides methods for sending emails to users for various purposes, such as registration verification and password reset.
+ */
@Slf4j
@RequiredArgsConstructor
@Service
diff --git a/src/main/java/com/digitalsanctuary/spring/user/service/UserService.java b/src/main/java/com/digitalsanctuary/spring/user/service/UserService.java
index 501048e..cc2c6de 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/service/UserService.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/service/UserService.java
@@ -45,27 +45,128 @@
*
* @author Devon Hillard
*/
+/**
+ * Service class for managing users. Provides methods for user registration, authentication, password management, and user-related operations. This
+ * class is transactional and uses various repositories and services for its operations.
+ *
+ *
+ * Dependencies:
+ *
+ *
+ *
{@link UserRepository}
+ *
{@link VerificationTokenRepository}
+ *
{@link PasswordResetTokenRepository}
+ *
{@link PasswordEncoder}
+ *
{@link RoleRepository}
+ *
{@link SessionRegistry}
+ *
{@link UserEmailService}
+ *
{@link UserVerificationService}
+ *
{@link DSUserDetailsService}
+ *
+ *
+ *
+ * Configuration:
+ *
+ *
+ *
sendRegistrationVerificationEmail: Flag to determine if a verification email should be sent upon registration.
+ *
+ *
+ *
+ * Enum:
+ *
+ *
+ *
{@link TokenValidationResult}: Enum representing the result of token validation.
+ *
+ *
+ *
+ * Methods:
+ *
+ *
+ *
{@link #registerNewUserAccount(UserDto)}: Registers a new user account.
+ *
{@link #saveRegisteredUser(User)}: Saves a registered user.
+ *
{@link #deleteUser(User)}: Deletes a user and cleans up associated tokens.
+ *
{@link #findUserByEmail(String)}: Finds a user by email.
+ *
{@link #getPasswordResetToken(String)}: Gets a password reset token by token string.
+ *
{@link #getUserByPasswordResetToken(String)}: Gets a user by password reset token.
+ *
{@link #findUserByID(long)}: Finds a user by ID.
+ *
{@link #changeUserPassword(User, String)}: Changes the user's password.
+ *
{@link #checkIfValidOldPassword(User, String)}: Checks if the provided old password is valid.
+ *
{@link #validatePasswordResetToken(String)}: Validates a password reset token.
+ *
{@link #getUsersFromSessionRegistry()}: Gets the list of users from the session registry.
+ *
{@link #authWithoutPassword(User)}: Authenticates a user without a password.
+ *
+ *
+ *
+ * Private Methods:
+ *
+ *
+ *
{@link #emailExists(String)}: Checks if an email exists in the user repository.
+ *
{@link #getAuthorities(User)}: Generates the list of authorities for a user.
+ *
{@link #authenticateUser(DSUserDetails, List)}: Authenticates a user by setting the authentication object in the security context.
+ *
{@link #storeSecurityContextInSession()}: Stores the current security context in the session.
+ *
+ *
+ *
+ * Annotations:
+ *
+ *
+ *
{@link Slf4j}: For logging.
+ *
{@link Service}: Indicates that this class is a service component in Spring.
+ *
{@link RequiredArgsConstructor}: Generates a constructor with required arguments.
+ *
{@link Transactional}: Indicates that the class or methods should be transactional.
+ *
{@link Value}: Injects property values.
+ *
+ */
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class UserService {
+ /**
+ * Enum representing the result of token validation.
+ */
public enum TokenValidationResult {
- VALID("valid"), INVALID_TOKEN("invalidToken"), EXPIRED("expired");
+
+ /**
+ * Indicates that the token is valid and can be used.
+ */
+ VALID("valid"),
+
+ /**
+ * Indicates that the token is invalid, either due to tampering or an unknown format.
+ */
+ INVALID_TOKEN("invalidToken"),
+
+ /**
+ * Indicates that the token was valid but has expired and is no longer usable.
+ */
+ EXPIRED("expired");
private final String value;
+ /**
+ * Instantiates a new token validation result.
+ *
+ * @param value the string representation of the token validation result.
+ */
TokenValidationResult(String value) {
this.value = value;
}
+ /**
+ * Gets the string representation of the token validation result.
+ *
+ * @return the value of the token validation result.
+ */
public String getValue() {
return value;
}
}
+
+ /** The user role name. */
private static final String USER_ROLE_NAME = "ROLE_USER";
/** The user repository. */
@@ -137,6 +238,7 @@ public User registerNewUserAccount(final UserDto newUserDto) {
* Save registered user.
*
* @param user the user
+ * @return the user
*/
public User saveRegisteredUser(final User user) {
return userRepository.save(user);
diff --git a/src/main/java/com/digitalsanctuary/spring/user/service/UserVerificationService.java b/src/main/java/com/digitalsanctuary/spring/user/service/UserVerificationService.java
index dc558b4..5874e83 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/service/UserVerificationService.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/service/UserVerificationService.java
@@ -10,6 +10,10 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+/**
+ * The UserVerificationService class is a Spring service class that provides methods for managing user verification tokens. This class is used to
+ * create, validate, and delete verification tokens for users.
+ */
@Slf4j
@RequiredArgsConstructor
@Service
diff --git a/src/main/java/com/digitalsanctuary/spring/user/service/package-info.java b/src/main/java/com/digitalsanctuary/spring/user/service/package-info.java
index 4b180c3..63625d9 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/service/package-info.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/service/package-info.java
@@ -1 +1,52 @@
-package com.digitalsanctuary.spring.user.service;
\ No newline at end of file
+/**
+ * This package contains service classes for the DigitalSanctuary Spring User framework.
+ *
+ *
+ * The services in this package provide core functionalities related to user management, including user registration, email verification, login
+ * attempts tracking, and user-related operations. These services interact with the persistence layer to perform CRUD operations and other business
+ * logic.
+ *
+ *
+ *
Classes:
+ *
+ *
{@link com.digitalsanctuary.spring.user.service.UserService} - Provides user-related operations such as registration, password management, and
+ * user retrieval.
+ *
{@link com.digitalsanctuary.spring.user.service.UserEmailService} - Handles email-related operations for users, including sending verification
+ * and password reset emails.
+ *
{@link com.digitalsanctuary.spring.user.service.UserVerificationService} - Manages user verification processes, including token generation and
+ * validation.
+ *
{@link com.digitalsanctuary.spring.user.service.LoginAttemptService} - Tracks login attempts and manages account lockout policies to prevent
+ * brute-force attacks.
+ *
+ *
+ *
Usage:
+ *
+ * These services are typically used by controllers and other components to perform user-related operations. They encapsulate the business logic and
+ * interact with the persistence layer to ensure data consistency and integrity.
+ *
+ * The services in this package depend on the persistence layer (repositories) and may also interact with other services and utilities within the
+ * application.
+ *
+ *
+ * @see com.digitalsanctuary.spring.user.service.UserService
+ * @see com.digitalsanctuary.spring.user.service.UserEmailService
+ * @see com.digitalsanctuary.spring.user.service.UserVerificationService
+ * @see com.digitalsanctuary.spring.user.service.LoginAttemptService
+ */
+package com.digitalsanctuary.spring.user.service;
diff --git a/src/main/java/com/digitalsanctuary/spring/user/util/GenericResponse.java b/src/main/java/com/digitalsanctuary/spring/user/util/GenericResponse.java
index 1aaa37d..0a36642 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/util/GenericResponse.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/util/GenericResponse.java
@@ -6,22 +6,72 @@
import org.springframework.validation.ObjectError;
import lombok.Data;
+/**
+ * A generic response class used to encapsulate response messages and errors. This class is typically used to provide feedback to the client in a
+ * structured format. It can handle simple messages as well as validation errors.
+ *
+ *
+ */
@Data
public class GenericResponse {
+ /**
+ * The message to be conveyed in the response.
+ */
private String message;
+
+ /**
+ * The error message, if any, associated with the response.
+ */
private String error;
+ /**
+ * Constructs a new GenericResponse with the specified message.
+ *
+ * @param message the message to be conveyed in the response
+ */
public GenericResponse(final String message) {
super();
this.message = message;
}
+ /**
+ * Constructs a new GenericResponse with the specified message and error.
+ *
+ * @param message the message to be conveyed in the response
+ * @param error the error message associated with the response
+ */
public GenericResponse(final String message, final String error) {
super();
this.message = message;
this.error = error;
}
+ /**
+ * Constructs a new GenericResponse with the specified list of validation errors and error message. The validation errors are converted to a
+ * JSON-like string format.
+ *
+ * @param allErrors the list of validation errors
+ * @param error the error message associated with the response
+ */
public GenericResponse(List allErrors, String error) {
this.error = error;
String temp = allErrors.stream().map(e -> {
diff --git a/src/main/java/com/digitalsanctuary/spring/user/util/JpaAuditingConfig.java b/src/main/java/com/digitalsanctuary/spring/user/util/JpaAuditingConfig.java
index 6aec02a..f728c59 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/util/JpaAuditingConfig.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/util/JpaAuditingConfig.java
@@ -11,16 +11,34 @@
import com.digitalsanctuary.spring.user.service.DSUserDetails;
import lombok.extern.slf4j.Slf4j;
+/**
+ * Configuration class for JPA Auditing. Enables JPA Auditing and provides an implementation of AuditorAware to capture the current auditor.
+ */
@Slf4j
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class JpaAuditingConfig {
+
+ /**
+ * Provides an implementation of AuditorAware to capture the current auditor.
+ *
+ * @return an instance of AuditorAware
+ */
@Bean
public AuditorAware auditorProvider() {
return new AuditorAwareImpl();
}
+ /**
+ * Implementation of AuditorAware to capture the current auditor.
+ */
private class AuditorAwareImpl implements AuditorAware {
+
+ /**
+ * Returns the current auditor based on the authentication context.
+ *
+ * @return an Optional containing the current auditor, or an empty Optional if no auditor is available
+ */
@Override
public Optional getCurrentAuditor() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
diff --git a/src/main/java/com/digitalsanctuary/spring/user/util/LocaleConfiguration.java b/src/main/java/com/digitalsanctuary/spring/user/util/LocaleConfiguration.java
deleted file mode 100644
index 05f328e..0000000
--- a/src/main/java/com/digitalsanctuary/spring/user/util/LocaleConfiguration.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package com.digitalsanctuary.spring.user.util;
-
-import java.util.Locale;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.web.servlet.LocaleResolver;
-import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
-import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
-import org.springframework.web.servlet.i18n.CookieLocaleResolver;
-import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
-
-@Configuration
-public class LocaleConfiguration implements WebMvcConfigurer {
-
- @Bean
- public LocaleResolver localeResolver() {
- CookieLocaleResolver resolver = new CookieLocaleResolver();
- resolver.setDefaultLocale(Locale.US);
- return resolver;
- }
-
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- registry.addInterceptor(localeChangeInterceptor());
- }
-
- @Bean
- public LocaleChangeInterceptor localeChangeInterceptor() {
- LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
- interceptor.setParamName("lang");
- return interceptor;
- }
-}
diff --git a/src/main/java/com/digitalsanctuary/spring/user/util/PasswordHashTimeTester.java b/src/main/java/com/digitalsanctuary/spring/user/util/PasswordHashTimeTester.java
index b1e23f2..d49f461 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/util/PasswordHashTimeTester.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/util/PasswordHashTimeTester.java
@@ -9,18 +9,26 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+/**
+ * The PasswordHashTimeTester class is a Spring Boot service class that tests the time it takes to hash a password. This class is used to test the
+ * performance of the password hashing algorithm and provide feedback on the security and usability trade-offs of the password hashing configuration.
+ */
@Slf4j
@Service
@RequiredArgsConstructor
public class PasswordHashTimeTester {
-
/** The password encoder. */
private final PasswordEncoder passwordEncoder;
+ /** The test hash time flag. */
@Value("${user.security.testHashTime}")
private boolean testHashTime = true;
+ /**
+ * Tests the time it takes to hash a password. This method is called when the application starts and tests the performance of the password hashing
+ * algorithm. The results are logged to provide feedback on the security and usability trade-offs of the password hashing configuration.
+ */
@Async
@EventListener(ApplicationStartedEvent.class)
public void testHashTime() {
diff --git a/src/main/java/com/digitalsanctuary/spring/user/util/TimeLogger.java b/src/main/java/com/digitalsanctuary/spring/user/util/TimeLogger.java
index d0c7d69..d4f8b11 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/util/TimeLogger.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/util/TimeLogger.java
@@ -2,6 +2,10 @@
import org.slf4j.Logger;
+/**
+ * The TimeLogger class is a utility for logging the time taken for a process. It can log the time to a provided SLF4J Logger or to the console if no
+ * Logger is provided.
+ */
public class TimeLogger {
private Logger logger;
@@ -9,30 +13,54 @@ public class TimeLogger {
private long startTime;
private long endTime;
+ /**
+ * Default constructor that initializes the TimeLogger and starts the timer.
+ */
public TimeLogger() {
start();
}
+ /**
+ * Constructor that initializes the TimeLogger with a provided SLF4J Logger and starts the timer.
+ *
+ * @param logger the SLF4J Logger to use for logging the time
+ */
public TimeLogger(Logger logger) {
this.logger = logger;
start();
}
+ /**
+ * Constructor that initializes the TimeLogger with a provided SLF4J Logger and a label, then starts the timer.
+ *
+ * @param logger the SLF4J Logger to use for logging the time
+ * @param label a label to include in the log message
+ */
public TimeLogger(Logger logger, String label) {
this.logger = logger;
this.label = label;
start();
}
+ /**
+ * Starts the timer by recording the current system time in milliseconds.
+ */
public void start() {
startTime = System.currentTimeMillis();
}
+ /**
+ * Ends the timer by recording the current system time in milliseconds and logs the time taken.
+ */
public void end() {
endTime = System.currentTimeMillis();
logTime();
}
+ /**
+ * Logs the time taken between the start and end times. If a Logger is provided, it logs the message at the debug level. Otherwise, it prints the
+ * message to the console.
+ */
public void logTime() {
long duration = endTime - startTime;
String logMessage = label + " took " + duration + " milliseconds";
diff --git a/src/main/java/com/digitalsanctuary/spring/user/util/package-info.java b/src/main/java/com/digitalsanctuary/spring/user/util/package-info.java
index 0450fd9..7ec7048 100644
--- a/src/main/java/com/digitalsanctuary/spring/user/util/package-info.java
+++ b/src/main/java/com/digitalsanctuary/spring/user/util/package-info.java
@@ -1 +1,7 @@
-package com.digitalsanctuary.spring.user.util;
\ No newline at end of file
+/**
+ * This package contains utility classes for the Spring User Framework.
+ *
+ * The utility classes in this package provide common functionalities and helper methods that are used across the Spring User Framework application.
+ *