From b5c12b75becb9f9718d49dbca0764731e2a6cca6 Mon Sep 17 00:00:00 2001 From: Devon Hillard Date: Sun, 27 Oct 2024 14:22:30 -0600 Subject: [PATCH 1/5] Update project version to 3.0.0-SNAPSHOT and Gradle distribution to 8.10.2 --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- sweep.yaml | 46 ------------------------ 3 files changed, 2 insertions(+), 48 deletions(-) delete mode 100644 sweep.yaml diff --git a/build.gradle b/build.gradle index 0e1631e..93d6d6a 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ plugins { } group = 'com.digitalsanctuary.spring' -version = '1.0.0-SNAPSHOT' +version = '3.0.0-SNAPSHOT' sourceCompatibility = '17' targetCompatibility = '17' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3fa8f86..df97d72 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.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/sweep.yaml b/sweep.yaml deleted file mode 100644 index 1eca36c..0000000 --- a/sweep.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# Sweep AI turns bugs & feature requests into code changes (https://sweep.dev) -# For details on our config file, check out our docs at https://docs.sweep.dev/usage/config - -# This setting contains a list of rules that Sweep will check for. If any of these rules are broken in a new commit, Sweep will create an pull request to fix the broken rule. -rules: - - "All docstrings and comments should be up to date." - - "Remove unused imports" - - "Remove duplicate attributes and meta tags from pages" - - "Use dynamic html lang attribute based on user locale" - - "Ensure CSRF handling works with SpringSecurity 6" - - "Ensure all form inputs have corresponding labels" - -# This is the branch that Sweep will develop from and make pull requests to. Most people use 'main' or 'master' but some users also use 'dev' or 'staging'. -branch: 'main' - -# By default Sweep will read the logs and outputs from your existing Github Actions. To disable this, set this to false. -gha_enabled: True - -# This is the description of your project. It will be used by sweep when creating PRs. You can tell Sweep what's unique about your project, what frameworks you use, or anything else you want. -# -# Example: -# -# description: sweepai/sweep is a python project. The main api endpoints are in sweepai/api.py. Write code that adheres to PEP8. -description: '' - -# This sets whether to create pull requests as drafts. If this is set to True, then all pull requests will be created as drafts and GitHub Actions will not be triggered. -draft: False - -# This is a list of directories that Sweep will not be able to edit. -blocked_dirs: [] - -# This is a list of documentation links that Sweep will use to help it understand your code. You can add links to documentation for any packages you use here. -# -# Example: -# -# docs: -# - PyGitHub: ["https://pygithub.readthedocs.io/en/latest/", "We use pygithub to interact with the GitHub API"] -docs: [] - -# Sandbox executes commands in a sandboxed environment to validate code changes after every edit to guarantee pristine code. For more details, see the [Sandbox](./sandbox) page. -sandbox: - install: - - trunk init - check: - - trunk fmt {file_path} || return 0 - - trunk check --fix --print-failures {file_path} From de20a2172b9a9f83ec476e0918f02fbee2a120aa Mon Sep 17 00:00:00 2001 From: Devon Hillard Date: Sun, 27 Oct 2024 14:31:04 -0600 Subject: [PATCH 2/5] Remove sweep-template.yml, update dependabot.yml, and delete codeql-analysis.yml --- .github/ISSUE_TEMPLATE/sweep-template.yml | 15 ----- .github/dependabot.yml | 2 +- .github/workflows/codeql-analysis.yml | 77 ----------------------- 3 files changed, 1 insertion(+), 93 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/sweep-template.yml delete mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/ISSUE_TEMPLATE/sweep-template.yml b/.github/ISSUE_TEMPLATE/sweep-template.yml deleted file mode 100644 index 44116f5..0000000 --- a/.github/ISSUE_TEMPLATE/sweep-template.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Sweep Issue -title: 'Sweep: ' -description: For small bugs, features, refactors, and tests to be handled by Sweep, an AI-powered junior developer. -labels: sweep -body: - - type: textarea - id: description - attributes: - label: Details - description: Tell Sweep where and what to edit and provide enough context for a new developer to the codebase - placeholder: | - Unit Tests: Write unit tests for . Test each function in the file. Make sure to test edge cases. - Bugs: The bug might be in . Here are the logs: ... - Features: the new endpoint should use the ... class from because it contains ... logic. - Refactors: We are migrating this function to ... version because ... \ No newline at end of file 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 From d48d262bea0d18c68f1ecdd508b1860ed3b18da9 Mon Sep 17 00:00:00 2001 From: Devon Hillard Date: Sun, 27 Oct 2024 17:02:12 -0600 Subject: [PATCH 3/5] Update project name to 'ds-spring-user-framework' --- build.gradle | 36 +++++++++++++++++++++++++----------- settings.gradle | 2 +- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/build.gradle b/build.gradle index 93d6d6a..aa8df7a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,15 +1,31 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.3.5' - id 'io.spring.dependency-management' version '1.1.6' - id "com.github.ben-manes.versions" version "0.51.0" - + id 'org.springframework.boot' version '3.3.4' + id 'io.spring.dependency-management' version '1.1.6' + 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' } -group = 'com.digitalsanctuary.spring' +group = 'com.digitalsanctuary.springuser' version = '3.0.0-SNAPSHOT' -sourceCompatibility = '17' -targetCompatibility = '17' +description = 'Spring User Framework' + +ext { + springBootVersion = '3.3.5' + lombokVersion = '1.18.34' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +repositories { + mavenCentral() +} // Define the configurations used in the project configurations { @@ -26,9 +42,7 @@ configurations { dev } -repositories { - mavenCentral() -} + dependencies { // Spring Boot starters 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' From 6d08965407c0da4b8fd24c420ea6e5e211804b5b Mon Sep 17 00:00:00 2001 From: Devon Hillard Date: Sun, 12 Jan 2025 16:36:14 -0700 Subject: [PATCH 4/5] Massive Refactoring to be a Maven Library Changed this SpringBoot Application into a Maven Library, which can be included via Gradle or Maven in your SpringBoot Application. Removed front end html, javascript, css, etc.. Updated all libraries. Setup Maven Central build steps. Created a CHANGELOG and automatic CHANGELOG generation. Bumped version to 3.0.0. And much much more! --- .vscode/settings.json | 5 +- config/README.md => CHANGELOG.md | 0 CONFIG.md | 9 - Dockerfile | 3 - PUBLISH.md | 34 + QUICKSTART.md | 33 - README.md | 124 +- build.gradle | 221 +- config/application.properties.example | 8 - docker-compose.yml | 74 - generate_changelog.py | 88 + gradle.properties | 1 + gradle/wrapper/gradle-wrapper.properties | 2 +- mailserver.env | 446 -- .../spring/user/UserApplication.java | 29 - .../spring/user/UserConfiguration.java | 36 + .../spring/user/api/UserAPI.java | 405 +- .../spring/user/api/package-info.java | 8 +- .../spring/user/audit/AuditConfig.java | 41 + .../user/{event => audit}/AuditEvent.java | 2 +- .../spring/user/audit/AuditEventListener.java | 40 + .../spring/user/audit/AuditLogWriter.java | 34 + .../audit/FileAuditLogFlushScheduler.java | 33 + .../spring/user/audit/FileAuditLogWriter.java | 167 + .../user/controller/PageController.java | 70 - .../user/controller/UserActionController.java | 5 +- .../user/controller/UserPageController.java | 27 +- .../spring/user/controller/package-info.java | 13 +- .../spring/user/dto/package-info.java | 8 +- ...uth2AuthenticationProcessingException.java | 19 + .../spring/user/exceptions/package-info.java | 9 +- .../spring/user/jobs/package-info.java | 23 +- .../user/listener/AuditEventListener.java | 165 - .../listener/AuthenticationEventLIstener.java | 12 +- .../spring/user/mail/package-info.java | 33 +- .../user/persistence/model/Privilege.java | 14 + .../spring/user/persistence/model/Role.java | 14 + .../spring/user/persistence/model/User.java | 28 +- .../user/persistence/model/package-info.java | 23 +- .../spring/user/persistence/package-info.java | 1 - .../persistence/repository/package-info.java | 31 +- .../RolePrivilegeSetupService.java | 3 +- .../RolesAndPrivilegesConfig.java | 30 +- .../CustomOAuth2AuthenticationEntryPoint.java | 22 +- .../{util => security}/WebSecurityConfig.java | 63 +- .../user/service/DSOAuth2UserService.java | 8 +- .../spring/user/service/DSUserDetails.java | 1 + .../user/service/DSUserDetailsService.java | 10 +- .../user/service/LoginAttemptService.java | 12 +- .../user/service/LoginSuccessService.java | 2 +- .../user/service/LogoutSuccessService.java | 2 +- .../spring/user/service/UserEmailService.java | 5 +- .../spring/user/service/UserService.java | 104 +- .../user/service/UserVerificationService.java | 4 + .../spring/user/service/package-info.java | 53 +- .../spring/user/util/GenericResponse.java | 50 + .../spring/user/util/JpaAuditingConfig.java | 18 + .../spring/user/util/LocaleConfiguration.java | 33 - .../user/util/PasswordHashTimeTester.java | 10 +- .../spring/user/util/TimeLogger.java | 28 + .../spring/user/util/package-info.java | 8 +- .../spring/user/web/ExcludeUserFromModel.java | 15 + .../web/GlobalMessageControllerAdvice.java | 35 + .../user/web/GlobalUserModelInterceptor.java | 93 + .../spring/user/web/IncludeUserInModel.java | 15 + .../spring/user/web/UserWebConfig.java | 26 + .../spring/user/web/WebInterceptorConfig.java | 25 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + src/main/resources/application-dev.yml | 32 - .../resources/application-local.yml-example | 105 - src/main/resources/application-prd.yml | 1 - .../config/dsspringuserconfig.properties | 95 + .../messages/dsspringusermessages.properties | 25 + .../resources/messages/messages.properties | 138 - src/main/resources/schema.sql | 94 + .../resources/static/css/nucleo-icons.css | 597 --- src/main/resources/static/css/nucleo-svg.css | 135 - .../static/css/perfect-scrollbar.css | 116 - .../static/css/soft-ui-dashboard-tailwind.css | 4241 ----------------- .../css/soft-ui-dashboard-tailwind.min.css | 28 - src/main/resources/static/css/tooltips.css | 15 - src/main/resources/static/css/user.css | 0 .../resources/static/fonts/nucleo-icons.eot | Bin 18516 -> 0 bytes .../resources/static/fonts/nucleo-icons.svg | 312 -- .../resources/static/fonts/nucleo-icons.ttf | Bin 18292 -> 0 bytes .../resources/static/fonts/nucleo-icons.woff | Bin 10220 -> 0 bytes .../resources/static/fonts/nucleo-icons.woff2 | Bin 8580 -> 0 bytes src/main/resources/static/fonts/nucleo.eot | Bin 26524 -> 0 bytes src/main/resources/static/fonts/nucleo.ttf | Bin 26364 -> 0 bytes src/main/resources/static/fonts/nucleo.woff | Bin 15168 -> 0 bytes src/main/resources/static/fonts/nucleo.woff2 | Bin 12616 -> 0 bytes src/main/resources/static/js/chart-1.js | 71 - src/main/resources/static/js/chart-2.js | 105 - src/main/resources/static/js/dropdown.js | 29 - src/main/resources/static/js/fixed-plugin.js | 287 -- src/main/resources/static/js/login.js | 17 - src/main/resources/static/js/nav-pills.js | 124 - .../resources/static/js/navbar-collapse.js | 37 - src/main/resources/static/js/navbar-sticky.js | 16 - .../resources/static/js/perfect-scrollbar.js | 36 - .../static/js/plugins/Chart.extension.js | 128 - .../static/js/plugins/chartjs.min.js | 13 - .../js/plugins/perfect-scrollbar.min.js | 19 - src/main/resources/static/js/pwstrength.js | 0 src/main/resources/static/js/register.js | 58 - .../resources/static/js/sidenav-burger.js | 35 - .../static/js/soft-ui-dashboard-tailwind.js | 79 - .../js/soft-ui-dashboard-tailwind.min.js | 17 - src/main/resources/static/js/tooltips.js | 57 - src/main/resources/static/js/user.js | 0 .../resources/templates/fragments/footer.html | 49 - .../resources/templates/fragments/header.html | 25 - src/main/resources/templates/index.html | 26 - src/main/resources/templates/layout.html | 62 - .../templates/mail/forgot-password-token.html | 6 +- .../templates/mail/registration-token.html | 8 +- src/main/resources/templates/protected.html | 20 - src/main/resources/templates/unprotected.html | 21 - .../templates/user/delete-account.html | 119 - .../user/forgot-password-change.html | 87 - .../forgot-password-pending-verification.html | 24 - .../templates/user/forgot-password.html | 80 - src/main/resources/templates/user/login.html | 142 - .../resources/templates/user/register.html | 143 - .../templates/user/registration-complete.html | 26 - .../registration-pending-verification.html | 25 - .../user/request-new-verification-email.html | 98 - .../templates/user/update-password.html | 97 - .../resources/templates/user/update-user.html | 91 - .../spring/user/UserApplicationTests.java | 2 + .../spring/user/api/UserApiTest.java | 97 +- .../spring/user/config/TestConfig.java | 7 +- .../spring/user/jdbc/Jdbc.java | 26 +- .../user/service/LoginAttemptServiceTest.java | 2 +- .../service/UserVerificationServiceTest.java | 19 +- .../spring/user/ui/BaseUiTest.java | 52 - .../user/ui/SpringUserFrameworkUiTest.java | 72 - .../spring/user/ui/data/UiTestData.java | 24 - .../user/ui/page/ForgotPasswordPage.java | 29 - .../spring/user/ui/page/LoginPage.java | 24 - .../spring/user/ui/page/LoginSuccessPage.java | 13 - .../spring/user/ui/page/RegisterPage.java | 45 - .../user/ui/page/SuccessRegisterPage.java | 13 - .../ui/page/SuccessResetPasswordPage.java | 13 - .../resources/application-test.yml} | 0 145 files changed, 2003 insertions(+), 9660 deletions(-) rename config/README.md => CHANGELOG.md (100%) delete mode 100644 Dockerfile create mode 100644 PUBLISH.md delete mode 100644 QUICKSTART.md delete mode 100644 config/application.properties.example delete mode 100644 docker-compose.yml create mode 100644 generate_changelog.py create mode 100644 gradle.properties delete mode 100644 mailserver.env delete mode 100644 src/main/java/com/digitalsanctuary/spring/user/UserApplication.java create mode 100644 src/main/java/com/digitalsanctuary/spring/user/UserConfiguration.java create mode 100644 src/main/java/com/digitalsanctuary/spring/user/audit/AuditConfig.java rename src/main/java/com/digitalsanctuary/spring/user/{event => audit}/AuditEvent.java (97%) create mode 100644 src/main/java/com/digitalsanctuary/spring/user/audit/AuditEventListener.java create mode 100644 src/main/java/com/digitalsanctuary/spring/user/audit/AuditLogWriter.java create mode 100644 src/main/java/com/digitalsanctuary/spring/user/audit/FileAuditLogFlushScheduler.java create mode 100644 src/main/java/com/digitalsanctuary/spring/user/audit/FileAuditLogWriter.java delete mode 100644 src/main/java/com/digitalsanctuary/spring/user/controller/PageController.java delete mode 100644 src/main/java/com/digitalsanctuary/spring/user/listener/AuditEventListener.java delete mode 100644 src/main/java/com/digitalsanctuary/spring/user/persistence/package-info.java rename src/main/java/com/digitalsanctuary/spring/user/{service => roles}/RolePrivilegeSetupService.java (96%) rename src/main/java/com/digitalsanctuary/spring/user/{util => roles}/RolesAndPrivilegesConfig.java (56%) rename src/main/java/com/digitalsanctuary/spring/user/{util => security}/CustomOAuth2AuthenticationEntryPoint.java (69%) rename src/main/java/com/digitalsanctuary/spring/user/{util => security}/WebSecurityConfig.java (81%) delete mode 100644 src/main/java/com/digitalsanctuary/spring/user/util/LocaleConfiguration.java create mode 100644 src/main/java/com/digitalsanctuary/spring/user/web/ExcludeUserFromModel.java create mode 100644 src/main/java/com/digitalsanctuary/spring/user/web/GlobalMessageControllerAdvice.java create mode 100644 src/main/java/com/digitalsanctuary/spring/user/web/GlobalUserModelInterceptor.java create mode 100644 src/main/java/com/digitalsanctuary/spring/user/web/IncludeUserInModel.java create mode 100644 src/main/java/com/digitalsanctuary/spring/user/web/UserWebConfig.java create mode 100644 src/main/java/com/digitalsanctuary/spring/user/web/WebInterceptorConfig.java create mode 100644 src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports delete mode 100644 src/main/resources/application-dev.yml delete mode 100644 src/main/resources/application-local.yml-example delete mode 100644 src/main/resources/application-prd.yml create mode 100644 src/main/resources/config/dsspringuserconfig.properties create mode 100644 src/main/resources/messages/dsspringusermessages.properties delete mode 100644 src/main/resources/messages/messages.properties create mode 100644 src/main/resources/schema.sql delete mode 100644 src/main/resources/static/css/nucleo-icons.css delete mode 100644 src/main/resources/static/css/nucleo-svg.css delete mode 100644 src/main/resources/static/css/perfect-scrollbar.css delete mode 100644 src/main/resources/static/css/soft-ui-dashboard-tailwind.css delete mode 100644 src/main/resources/static/css/soft-ui-dashboard-tailwind.min.css delete mode 100644 src/main/resources/static/css/tooltips.css delete mode 100644 src/main/resources/static/css/user.css delete mode 100644 src/main/resources/static/fonts/nucleo-icons.eot delete mode 100644 src/main/resources/static/fonts/nucleo-icons.svg delete mode 100644 src/main/resources/static/fonts/nucleo-icons.ttf delete mode 100644 src/main/resources/static/fonts/nucleo-icons.woff delete mode 100644 src/main/resources/static/fonts/nucleo-icons.woff2 delete mode 100644 src/main/resources/static/fonts/nucleo.eot delete mode 100644 src/main/resources/static/fonts/nucleo.ttf delete mode 100644 src/main/resources/static/fonts/nucleo.woff delete mode 100644 src/main/resources/static/fonts/nucleo.woff2 delete mode 100644 src/main/resources/static/js/chart-1.js delete mode 100644 src/main/resources/static/js/chart-2.js delete mode 100644 src/main/resources/static/js/dropdown.js delete mode 100644 src/main/resources/static/js/fixed-plugin.js delete mode 100644 src/main/resources/static/js/login.js delete mode 100644 src/main/resources/static/js/nav-pills.js delete mode 100644 src/main/resources/static/js/navbar-collapse.js delete mode 100644 src/main/resources/static/js/navbar-sticky.js delete mode 100644 src/main/resources/static/js/perfect-scrollbar.js delete mode 100644 src/main/resources/static/js/plugins/Chart.extension.js delete mode 100644 src/main/resources/static/js/plugins/chartjs.min.js delete mode 100644 src/main/resources/static/js/plugins/perfect-scrollbar.min.js delete mode 100644 src/main/resources/static/js/pwstrength.js delete mode 100644 src/main/resources/static/js/register.js delete mode 100644 src/main/resources/static/js/sidenav-burger.js delete mode 100644 src/main/resources/static/js/soft-ui-dashboard-tailwind.js delete mode 100644 src/main/resources/static/js/soft-ui-dashboard-tailwind.min.js delete mode 100644 src/main/resources/static/js/tooltips.js delete mode 100644 src/main/resources/static/js/user.js delete mode 100644 src/main/resources/templates/fragments/footer.html delete mode 100644 src/main/resources/templates/fragments/header.html delete mode 100644 src/main/resources/templates/index.html delete mode 100644 src/main/resources/templates/layout.html delete mode 100644 src/main/resources/templates/protected.html delete mode 100644 src/main/resources/templates/unprotected.html delete mode 100644 src/main/resources/templates/user/delete-account.html delete mode 100644 src/main/resources/templates/user/forgot-password-change.html delete mode 100644 src/main/resources/templates/user/forgot-password-pending-verification.html delete mode 100644 src/main/resources/templates/user/forgot-password.html delete mode 100644 src/main/resources/templates/user/login.html delete mode 100644 src/main/resources/templates/user/register.html delete mode 100644 src/main/resources/templates/user/registration-complete.html delete mode 100644 src/main/resources/templates/user/registration-pending-verification.html delete mode 100644 src/main/resources/templates/user/request-new-verification-email.html delete mode 100644 src/main/resources/templates/user/update-password.html delete mode 100644 src/main/resources/templates/user/update-user.html delete mode 100644 src/test/java/com/digitalsanctuary/spring/user/ui/BaseUiTest.java delete mode 100644 src/test/java/com/digitalsanctuary/spring/user/ui/SpringUserFrameworkUiTest.java delete mode 100644 src/test/java/com/digitalsanctuary/spring/user/ui/data/UiTestData.java delete mode 100644 src/test/java/com/digitalsanctuary/spring/user/ui/page/ForgotPasswordPage.java delete mode 100644 src/test/java/com/digitalsanctuary/spring/user/ui/page/LoginPage.java delete mode 100644 src/test/java/com/digitalsanctuary/spring/user/ui/page/LoginSuccessPage.java delete mode 100644 src/test/java/com/digitalsanctuary/spring/user/ui/page/RegisterPage.java delete mode 100644 src/test/java/com/digitalsanctuary/spring/user/ui/page/SuccessRegisterPage.java delete mode 100644 src/test/java/com/digitalsanctuary/spring/user/ui/page/SuccessResetPasswordPage.java rename src/{main/resources/application.yml => test/resources/application-test.yml} (100%) 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 a654c6d..24c943d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,22 @@ +## 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 -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. +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. + +The framework includes basic example pages that are unstyled, allowing for seamless integration into your application. ## Summary @@ -21,6 +37,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. @@ -31,85 +49,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 aa8df7a..947614f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,20 +1,25 @@ plugins { - id 'org.springframework.boot' version '3.3.4' - id 'io.spring.dependency-management' version '1.1.6' + 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' } +import com.vanniktech.maven.publish.SonatypeHost +import com.vanniktech.maven.publish.JavaLibrary +import com.vanniktech.maven.publish.JavadocJar + group = 'com.digitalsanctuary.springuser' -version = '3.0.0-SNAPSHOT' +// version = '3.0.0-SNAPSHOT' description = 'Spring User Framework' ext { - springBootVersion = '3.3.5' - lombokVersion = '1.18.34' + springBootVersion = '3.4.1' + lombokVersion = '1.18.36' } java { @@ -27,58 +32,47 @@ repositories { mavenCentral() } -// Define the configurations used in the project -configurations { - developmentOnly - runtimeOnly { - extendsFrom developmentOnly - } - testImplementation { - extendsFrom runtimeOnly - } - compileOnly { - extendsFrom annotationProcessor - } - dev -} - - - 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.2.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.0' + 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.3.1-jre' - implementation 'org.springframework.boot:spring-boot-starter-actuator' - - 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' - - // Test dependencies - testImplementation 'org.springframework.boot:spring-boot-starter-test' + implementation 'com.google.guava:guava:33.4.0-jre' + 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" + + // 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:$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.5.1' - testImplementation group: 'io.github.bonigarcia', name: 'webdrivermanager', version: '5.9.2' +} +tasks.named('bootJar') { + enabled = false } test { @@ -88,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 df97d72..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.10.2-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/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.
  • + *
+ * + *

+ * Usage example: + *

+ * + *
+ * {@code
+ * EmailService emailService = new EmailServiceImpl();
+ * emailService.sendEmail("recipient@example.com", "Subject", "Email body");
+ * }
+ * 
+ * + *

+ * Configuration: + *

+ *

+ * 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. + *

+ * + *

Example:

+ * + *
+ * {@code
+ * @Autowired
+ * private UserService userService;
+ *
+ * public void registerUser(UserDto userDto) {
+ *     userService.registerNewUserAccount(userDto);
+ * }
+ * }
+ * 
+ * + *

Dependencies:

+ *

+ * 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. + * + *

+ * Example usage: + *

+ * + *
+ * {@code
+ * GenericResponse response = new GenericResponse("Success");
+ * }
+ * 
+ * + *

+ * Example usage with errors: + *

+ * + *
+ * {@code
+ * List errors = ...;
+ * GenericResponse response = new GenericResponse(errors, "Validation failed");
+ * }
+ * 
+ */ @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. + *

+ */ +package com.digitalsanctuary.spring.user.util; diff --git a/src/main/java/com/digitalsanctuary/spring/user/web/ExcludeUserFromModel.java b/src/main/java/com/digitalsanctuary/spring/user/web/ExcludeUserFromModel.java new file mode 100644 index 0000000..465c4e3 --- /dev/null +++ b/src/main/java/com/digitalsanctuary/spring/user/web/ExcludeUserFromModel.java @@ -0,0 +1,15 @@ +package com.digitalsanctuary.spring.user.web; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation is used to indicate that the current user should be excluded from the model for the request. This annotation can be applied to a + * method or a class. It should be used when the user in model mode is set to opt-out. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface ExcludeUserFromModel { +} diff --git a/src/main/java/com/digitalsanctuary/spring/user/web/GlobalMessageControllerAdvice.java b/src/main/java/com/digitalsanctuary/spring/user/web/GlobalMessageControllerAdvice.java new file mode 100644 index 0000000..8869c89 --- /dev/null +++ b/src/main/java/com/digitalsanctuary/spring/user/web/GlobalMessageControllerAdvice.java @@ -0,0 +1,35 @@ +package com.digitalsanctuary.spring.user.web; + +import java.util.Locale; +import org.springframework.context.MessageSource; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.context.request.WebRequest; +import lombok.RequiredArgsConstructor; + +/** + * Global advice to handle common model attributes across all controllers. + */ +@ControllerAdvice(annotations = Controller.class) +@RequiredArgsConstructor +public class GlobalMessageControllerAdvice { + private final MessageSource messages; + + /** + * Adds a localized message to the model if a `messageKey` GET parameter is present. + * + * @param request the web request + * @param model the model + */ + @ModelAttribute + public void addMessageFromKey(WebRequest request, org.springframework.ui.Model model) { + // Retrieve the `messageKey` parameter from the request + String messageKey = request.getParameter("messageKey"); + if (messageKey != null) { + Locale locale = request.getLocale(); + String message = messages.getMessage(messageKey, null, locale); + model.addAttribute("message", message); + } + } +} diff --git a/src/main/java/com/digitalsanctuary/spring/user/web/GlobalUserModelInterceptor.java b/src/main/java/com/digitalsanctuary/spring/user/web/GlobalUserModelInterceptor.java new file mode 100644 index 0000000..8c999ef --- /dev/null +++ b/src/main/java/com/digitalsanctuary/spring/user/web/GlobalUserModelInterceptor.java @@ -0,0 +1,93 @@ +package com.digitalsanctuary.spring.user.web; + + +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; +import com.digitalsanctuary.spring.user.service.DSUserDetails; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * Interceptor to add the current user to the model for applicable requests. + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class GlobalUserModelInterceptor implements HandlerInterceptor { + + // The UserWebConfig object is used to determine the global user model opt-in behavior + private final UserWebConfig userWebConfig; + + /** + * Pre-handle method to allow all requests to proceed by default. + */ + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + // Allow all requests to proceed by default + log.debug("Handling request for path: {}", request.getRequestURI()); + + return true; + } + + + /** + * Post-handle method to add the current user to the model for applicable requests. + */ + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { + log.debug("handler is: {}", handler.getClass().getName()); + log.debug("modelAndView: {}", modelAndView); + if (modelAndView == null || !(handler instanceof HandlerMethod)) { + return; + } + + HandlerMethod handlerMethod = (HandlerMethod) handler; + + // Apply global opt-in or opt-out behavior + if (userWebConfig.isGlobalUserModelOptIn()) { + // Global Opt-In Mode: Skip if not explicitly opted-in + if (!hasAnnotation(handlerMethod, IncludeUserInModel.class)) { + return; // Skip if not explicitly opted-in + } + } else { + // Global Opt-Out Mode: Skip if explicitly excluded + if (hasAnnotation(handlerMethod, ExcludeUserFromModel.class)) { + return; // Skip if explicitly excluded + } + } + + // Add user to the model if applicable + log.debug("GlobalUserModelInterceptor.postHandle: Adding user to model"); + + + // Retrieve the authenticated user from the security context + if (SecurityContextHolder.getContext().getAuthentication() == null) { + return; + } + Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + + if (principal instanceof DSUserDetails userDetails) { + modelAndView.addObject("user", userDetails.getUser()); + } + + } + + /** + * Helper method to determine if the specified annotation is present on the handler method or controller class. + */ + private boolean hasAnnotation(HandlerMethod handlerMethod, Class annotationClass) { + // Check for the annotation on the method + if (handlerMethod.getMethodAnnotation(annotationClass) != null) { + return true; + } + + // Check for the annotation on the controller class + return handlerMethod.getBeanType().isAnnotationPresent(annotationClass); + } + +} diff --git a/src/main/java/com/digitalsanctuary/spring/user/web/IncludeUserInModel.java b/src/main/java/com/digitalsanctuary/spring/user/web/IncludeUserInModel.java new file mode 100644 index 0000000..52d20f0 --- /dev/null +++ b/src/main/java/com/digitalsanctuary/spring/user/web/IncludeUserInModel.java @@ -0,0 +1,15 @@ +package com.digitalsanctuary.spring.user.web; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation is used to indicate that the current user should be included in the model for the request. This annotation can be applied to a + * method or a class. It should be used when the user in model mode is set to opt-in. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface IncludeUserInModel { +} diff --git a/src/main/java/com/digitalsanctuary/spring/user/web/UserWebConfig.java b/src/main/java/com/digitalsanctuary/spring/user/web/UserWebConfig.java new file mode 100644 index 0000000..5287781 --- /dev/null +++ b/src/main/java/com/digitalsanctuary/spring/user/web/UserWebConfig.java @@ -0,0 +1,26 @@ +package com.digitalsanctuary.spring.user.web; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Component; +import lombok.Data; + +/** + * The UserWebConfig class is a Spring Boot configuration class that provides properties for configuring the user web interface. This class is used to + * define properties that control the behavior of the user web interface, such as whether the user object is added to the model for all requests. + */ +@Data +@Component +@PropertySource("classpath:config/dsspringuserconfig.properties") +@ConfigurationProperties(prefix = "user.web") +public class UserWebConfig { + + /** + * The global user model opt in flag. This flag determines whether the user object is added to the model for all requests. If set to false, the + * default, then the user object will be added to all requests unless the Controller or Controller method has the {@code @ExcludeUserFromModel} + * annotation. If set to true, then the user object will only be added to the model if the Controller or Controller method has the + * {@code @IncludeUserInModel} annotation. + */ + private boolean globalUserModelOptIn; + +} diff --git a/src/main/java/com/digitalsanctuary/spring/user/web/WebInterceptorConfig.java b/src/main/java/com/digitalsanctuary/spring/user/web/WebInterceptorConfig.java new file mode 100644 index 0000000..231f030 --- /dev/null +++ b/src/main/java/com/digitalsanctuary/spring/user/web/WebInterceptorConfig.java @@ -0,0 +1,25 @@ +package com.digitalsanctuary.spring.user.web; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import lombok.RequiredArgsConstructor; + +/** + * Web configuration class for setting up interceptors + */ +@Configuration +@RequiredArgsConstructor +public class WebInterceptorConfig implements WebMvcConfigurer { + + private final GlobalUserModelInterceptor globalUserModelInterceptor; + + /** + * Add the global user model interceptor to the registry + */ + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(globalUserModelInterceptor).addPathPatterns("/", "/**") // Apply to all paths + .excludePathPatterns("/static/**", "/css/**", "/js/**", "/images/**", "/favicon.ico"); // Exclude static assets + } +} diff --git a/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..7963c40 --- /dev/null +++ b/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.digitalsanctuary.spring.user.UserConfiguration diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml deleted file mode 100644 index 07680bf..0000000 --- a/src/main/resources/application-dev.yml +++ /dev/null @@ -1,32 +0,0 @@ -debug: true - -logging: - level: - com: - digitalsanctuary: - spring: DEBUG - org: - springframework: - security: DEBUG - web: - filter: - CommonsRequestLoggingFilter: DEBUG - nodeValue: DEBUG -spring: - mvc: - log-request-details: true - thymeleaf: - cache: false - devtools: - restart: - enabled: true - -server: - servlet: - session: - cookie: - secure: false -user: - audit: - flushOnWrite: true - diff --git a/src/main/resources/application-local.yml-example b/src/main/resources/application-local.yml-example deleted file mode 100644 index 1818263..0000000 --- a/src/main/resources/application-local.yml-example +++ /dev/null @@ -1,105 +0,0 @@ -debug: true # Enable or disable debug mode - -logging: - level: - com: - digitalsanctuary: DEBUG # Set logging level for digitalsanctuary package - org: - springframework: - web: DEBUG # Set logging level for web - filter: - CommonsRequestLoggingFilter: DEBUG # Set logging level for CommonsRequestLoggingFilter - security: DEBUG # Set logging level for security - -spring: - mail: # Mail configuration - username: AAAAAAAAA # Mail server username - password: BBBBBBBBBBB # Mail server password - host: mail.myhost.com # Mail server hostname - security: - oauth2: - enabled: true # Enable or disable OAuth2 - client: - registration: - google: - client-id: 45XXXXXXXXX.apps.googleusercontent.com # Google client ID for OAuth2 - client-secret: GOXXXXXXXXXXXXXXX # Google client secret for OAuth2 - authorization-grant-type: authorization_code # Authorization grant type for OAuth2 - redirect-uri: 'https://yourtestdomain.ngrok.io/login/oauth2/code/{registrationId}' # Redirect URI for OAuth2 - scope: - - email # Request email scope for OAuth2 - - profile # Request profile scope for OAuth2 - client-name: Google # Name of the OAuth2 client - facebook: - client-id: 3333333333333333 # Facebook client ID for OAuth2 - client-secret: 555555555555555GGGGGGGGG # Facebook client secret for OAuth2 - authorization-grant-type: authorization_code # Authorization grant type for OAuth2 - redirect-uri: 'https://yourtestdomain.ngrok.io/login/oauth2/code/{registrationId}' # Redirect URI for OAuth2 - scope: - - email # Request email scope for OAuth2 - - public_profile # Request public_profile scope for OAuth2 - client-name: Facebook # Name of the OAuth2 client - # apple: # This isn't working currently - # client-id: com.digitalsanctuary.springuserapp - # client-secret: XXXXXX - # authorization-grant-type: authorization_code - # redirect-uri: 'https://springuser.ngrok.io/login/oauth2/code/{registrationId}' - # scope: - # - email - # - name - # client-name: Apple - # client-authentication-method: client_secret_post - # provider: - # apple: - # authorization-uri: https://appleid.apple.com/auth/authorize?response_mode=form_post - # token-uri: https://appleid.apple.com/auth/token - # jwk-set-uri: https://appleid.apple.com/auth/keys - # user-name-attribute: sub - thymeleaf: - cache: 'false' # Enable or disable Thymeleaf cache - prefix: file:src/main/resources/templates/ # Prefix for Thymeleaf templates - devtools: - restart: - enabled: 'true' # Enable or disable devtools restart - poll-interval: '2s' # Poll interval for devtools restart - quiet-period: '1s' # Quiet period for devtools restart - - additional-paths: - - src/main/java/ # Additional paths for devtools restart - - livereload: - enabled: 'true' # Enable or disable livereload - https: 'true' # Enable or disable HTTPS for livereload - - mvc: - log-request-details: 'true' # Enable or disable request details logging - web: # Web configuration - resources: - static-locations: file:src/main/resources/static/, classpath:/static/ - cache: - period: 0 - -server: - servlet: - session: - cookie: - secure: false # disabling secure cookie for local development - -user: - audit: - flushOnWrite: true # Enable flush on write for user audit - registration: # User registration configuration - sendVerificationEmail: false # Disable sending verification email - googleEnabled: true # Enable Google registration - facebookEnabled: true # Enable Facebook registration - security: - # unprotectedURIs: /,/index.html,/favicon.ico,/css/*,/js/*,/img/*,/user/registration,/user/resendRegistrationToken,/user/resetPassword,/user/registrationConfirm,/user/changePassword,/user/savePassword # Unprotected URIs - mail: - fromAddress: you@test.com # From address for outbound mail - -management: - newrelic: - metrics: - export: - account-id: ACCTID # Account ID for New Relic metrics export - api-key: KEYYYYY # API key for New Relic metrics export diff --git a/src/main/resources/application-prd.yml b/src/main/resources/application-prd.yml deleted file mode 100644 index 8b13789..0000000 --- a/src/main/resources/application-prd.yml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/main/resources/config/dsspringuserconfig.properties b/src/main/resources/config/dsspringuserconfig.properties new file mode 100644 index 0000000..ab8d7e2 --- /dev/null +++ b/src/main/resources/config/dsspringuserconfig.properties @@ -0,0 +1,95 @@ +# Spring Configuration Overrides +spring.messages.basename=messages/messages,messages/dsspringusermessages + +# DigitalSanctuary Spring User Configuration + +# User Audit Log Configuration + +# The path to the audit log file. +user.audit.logFilePath=/opt/app/logs/user-audit.log + +# If true, the audit log will be flushed to disk after every write (less performant). If false, the audit log will be flushed to disk every 10 seconds (more performant). +user.audit.flushOnWrite=false + +# The rate at which the audit log will be flushed to disk in milliseconds. +user.audit.flushRate=30000 + +# If true, all events will be logged. +user.audit.logEvents=true + + +# If true, users can delete their own accounts. If false, accounts are disabled instead of deleted. +user.actuallyDeleteAccount=false + +# If true, a verification email will be sent to the user after registration. If false, the user will be automatically verified. +user.registration.sendVerificationEmail=true +# If true, Google OAuth2 will be enabled for registration. +user.registration.googleEnabled=false + +# If true, Facebook OAuth2 will be enabled for registration. +user.registration.facebookEnabled=false + + + +# The number of failed login attempts before the user account is locked out. Set this to 0 to disable account lockout. +user.security.failedLoginAttempts=10 +# The number of minutes to lock the user account after the maximum number of failed login attempts is reached. Set this to 0 to disable account lockout. Set this to -1 to lock the account until an administrator unlocks it. +user.security.accountLockoutDuration=30 +# The bcrypt strength to use for password hashing. The higher the number, the longer it takes to hash the password. The default is 12. The minimum is 4. The maximum is 31. +user.security.bcryptStrength=12 +# If true, the test hash time will be logged to the console on startup. This is useful for determining the optimal bcryptStrength value. +user.security.testHashTime=true +# The default action for all requests. This can be either deny or allow. +user.security.defaultAction=deny +# A comma delimited list of URIs that should not be protected by Spring Security if the defaultAction is deny. +user.security.unprotectedURIs=/,/index.html,/favicon.ico,/css/*,/js/*,/img/*,/user/registration,/user/resendRegistrationToken,/user/resetPassword,/user/registrationConfirm,/user/changePassword,/user/savePassword,/oauth2/authorization/*,/login,/error +# A comma delimited list of URIs that should be protected by Spring Security if the defaultAction is allow. +user.security.protectedURIs=/protected.html +# A comma delimited list of URIs that should not be protected by CSRF protection. This may include API endpoints that need to be called without a CSRF token. +user.security.disableCSRFdURIs=/no-csrf-test + +# The URI for the login page. +user.security.loginPageURI=/user/login.html +# The URI for the login action. +user.security.loginActionURI=/user/login +# The URI for the login success page. +user.security.loginSuccessURI=/index.html?messageKey=message.login.success +# The URI for the logout success page. +user.security.logoutSuccessURI=/index.html?messageKey=message.logout.success +# The URI for the logout action. +user.security.logoutActionURI=/user/logout +# The URI for the forgot password change page. +user.security.forgotPasswordChangeURI=/user/forgot-password-change.html +# The URI for the forgot password page. +user.security.forgotPasswordURI=/user/forgot-password.html +# The URI for the forgot password pending verification page. +user.security.forgotPasswordPendingURI=/user/forgot-password-pending-verification.html +# The URI for the registration pending verification page. +user.security.registrationPendingURI=/user/registration-pending-verification.html +# The URI for the registration page. +user.security.registrationURI=/user/register.html +# The URI for the registration success page. +user.security.registrationSuccessURI=/user/registration-complete.html +# The URI for the request new verification email page. +user.security.registrationNewVerificationURI=/user/request-new-verification-email.html +# The URI for the update user page. +user.security.updateUserURI=/user/update-user.html + +# The from address for all emails sent by the application. +user.mail.fromAddress=test@test.com +# The cron expression for the token purge job. This defaults to 3 am every day. +user.purgetokens.cron.expression=0 0 3 * * ? +# The first year of the copyright. This is used for dispaly of the page footer. +user.copyrightFirstYear=2020 + +user.web.globalUserModelOptIn=false + +# Roles and privileges configuration. +# Each role can have one or more privileges. Privileges are comma delimited. +user.roles.roles-and-privileges.ROLE_ADMIN=ADMIN_PRIVILEGE,INVITE_USER_PRIVILEGE,READ_USER_PRIVILEGE,ASSIGN_MANAGER_PRIVILEGE,RESET_ANY_USER_PASSWORD_PRIVILEGE +user.roles.roles-and-privileges.ROLE_MANAGER=ADD_USER_TO_TEAM_PRIVILEGE,REMOVE_USER_FROM_TEAM_PRIVILEGE,RESET_TEAM_PASSWORD_PRIVILEGE +user.roles.roles-and-privileges.ROLE_USER=LOGIN_PRIVILEGE,UPDATE_OWN_USER_PRIVILEGE,RESET_OWN_PASSWORD_PRIVILEGE + +# Role hierarchy configuration. Higher level roles inherit all roles from lower level roles. +user.roles.role-hierarchy[0]=ROLE_ADMIN > ROLE_MANAGER +user.roles.role-hierarchy[1]=ROLE_MANAGER > ROLE_USER diff --git a/src/main/resources/messages/dsspringusermessages.properties b/src/main/resources/messages/dsspringusermessages.properties new file mode 100644 index 0000000..12d5c8a --- /dev/null +++ b/src/main/resources/messages/dsspringusermessages.properties @@ -0,0 +1,25 @@ +# Email Messages +email.forgot-password.intro=A password reset request was made for your account. If you requested this, click the link below to reset your password. If not, you can safely ignore this email. No changes have been made to your account. If you believe someone is trying to access your account, please contact support. +email.forgot-password.prompt=Reset your password +email.forgot-password.link-expiration=This link will be valid for 24 hours. If it expires, you can request a new password reset link. + +email.registration-confirmation.intro=Thank you for registering with the Spring User Application!
To activate your account, you must verify your email address by clicking the link below. +email.registration-confirmation.link-instructions=You’ve successfully registered. To confirm your account, click the link below. +email.registration-confirmation.link-expiration=This link will be valid for 24 hours. If it expires, you can request a new verification email. + +email.signature=Best regards,
The DigitalSanctuary Team + + +# Messages +message.update-user.success=Your profile has been successfully updated. + +message.account.verified=Your account has been successfully verified. +message.logout.success=You logged out successfully +message.login.success=You logged in successfully + + +token.message=Your token is: +auth.message.disabled=Your account is disabled please check your mail and click on the confirmation link +auth.message.expired=Your registration token has expired. Please register again. +auth.message.invalidUser=This username is invalid, or does not exist. +auth.message.invalidToken=Invalid token. diff --git a/src/main/resources/messages/messages.properties b/src/main/resources/messages/messages.properties deleted file mode 100644 index b44d625..0000000 --- a/src/main/resources/messages/messages.properties +++ /dev/null @@ -1,138 +0,0 @@ -site.h1=Spring User Framework -site.h2=Easy User Management - -page.title.home=Home Page -page.title.login=Log In -page.title.registration=Register Your Account -page.title.registrationPendingVerification=Registration Pending Verification -page.title.registrationResendVerification=Send New Verification Email -page.title.registrationComplete=Registration Complete! -page.title.forgotPassword=Forgot Your Password? -page.title.forgotPasswordPending=Forgot Password Pending Verification -page.title.updateUser=Update Your Profile -page.title.updatePassword=Change Your Password -page.title.deleteAccount=Delete Your Account - - - -message.welcome=Welcome - -message.createdBy=Created by Devon Hillard @ DigitalSanctuary -message.copyright=Copyright © - -action.login=Log In -action.logout=Log Out -action.register=Register Here -action.forgotPassword=Forgot Your Password? -action.updateUser=Update Profile -action.updatePassword=Change Password -action.deleteAccount=Delete Account - -email.forgotPassword.introPara=A forgot password email has been requested for your account. If this was you, please click the link below and reset your password. If this wasn't you, it is safe to ignore this, no changes have been made to your account or password. If you belive someone is trying to gain access to your account, please contact support! -email.forgotPassword.linkExpPara=Please note that this link will only be valid for 24 hours. Click if you need to request another forgot password link. -email.registrationConfirmation.introPara=Thank you for registering on the Spring User Application!
Your account will NOT be activated unti you verify your email address by clicking the link below. -email.registrationConfirmation.linkExpPara=Please note that this link will only be valid for 24 hours. Click if you need to request another verficiation email. -email.signature=Regards,
The DigitalSanctuary Team - - - -label.form.resendRegistrationToken=Re-send Token -message.resendToken=We will send an email with a new registration token to your email account -label.form.forgotPassword=Forgot Password -message.forgotPassword=Forgot Password -message.resetPassword=Reset Password -message.updatePassword=Update Password -message.userNotFound=User Not Found -message.resetPasswordSuccess=Password reset successfully -message.resetYourPassword=Reset your password -message.resetPasswordEmail=You should receive a password reset email shortly -message.deleteAccount=Delete Your Account - -message.updateUserSuccess=Your Profile Was Successfully Updated. - -label.form.updateUser=Update Your Profile - - -message.username=Email address required -message.password=Password required -message.unauth=Unauthorized Access !! -message.badCredentials=Invalid Credentials -message.sessionExpired=Session Expired. Please reload and try again. -message.logoutError=Sorry, error logging out -message.logoutSuccess=You logged out successfully -message.loginError=Sorry, error logging in -message.loginSuccess=You logged in successfully -message.regThankYou=Thank you for registering! -message.regSuccess=You will receive an email with a verification link. Please click that link to activate your account. -message.regSuccessLink=You registered successfully. To confirm your registration, please click on the below link. -message.regSuccessConfirmed=Thank you for completing your registration! -message.regSuccessNextSteps=You have been logged in! Enter the site here: -message.regErrorAlreadyExists=An account for that username/email already exists. Please enter a different email. -message.regErrorAlreadyExists2=You can also go here to Log in, or here if you Forgot Your Password. -message.regError=An error occurred during registration. Please try again. -message.regTokenExpired=Your token has expired. Please enter your email address below to get a new verification email. -message.regTokenInvalid=Your token has either been used, or is invalid. If you are sure you haven't already verified your account, please enter your email address below to get a new verification email. -message.regAlreadyEnabled=Your account is already verified. Please try Logging in, or click here if you Forgot Your Password. -message.accountLocked=Your account is locked. Please try again later or contact support. - -message.lastName=Last name is required -message.firstName=First name required -message.badEmail=Invalid email address -message.error=Sorry, an error has occured! - -token.message=Your token is: -auth.message.disabled=Your account is disabled please check your mail and click on the confirmation link -auth.message.expired=Your registration token has expired. Please register again. -auth.message.invalidUser=This username is invalid, or does not exist. -auth.message.invalidToken=Invalid token. - -label.user.email=Email: -label.user.firstName=First Name: -label.user.lastName=Last Name: -label.user.password=Password: -label.user.confirmPass=Confirm Password -label.form.submit=Submit -label.form.registrationTitle=Registration Form -label.form.loginLink=Sign In -label.login=Log in here -label.form.loginTitle=Log In -label.form.loginEmail=Email -label.form.loginPass=Password -label.form.loginButton=Log In -label.form.loginEnglish=English -label.form.loginSpanish=Spanish -label.form.loginSignUp=Sign up - -label.pages.logout=Log out -label.pages.admin=Administrator -label.pages.home.title=Home -label.pages.home.message=Welcome Home -label.successRegister.title=Registration Success - -label.badUser.title=Invalid Link -ValidEmail.user.email=Invalid email address! -UniqueUsername.user.username=An account with that username/email already exists -Size.userDto.firstName=Length must be greater than {min} -Size.userDto.lastName=Length must be greater than {min} -Size.userDto.email=Length must be greater than {min} -NotNull.user.firstName=First name required -NotEmpty.user.firstName=First name required -NotNull.user.lastName=Last name required -NotEmpty.user.lastName=Last name required -NotNull.user.username=Username(Email) required -NotEmpty.user.username=Username(Email) required -NotNull.user.password=Password required -NotEmpty.user.password=Password required -NotNull.user.matchingPassword=Required -NotEmpty.user.matchingPassword=Required -PasswordMatches.user:Password does not match! -Email.user.email=Invalid Username (Email) - - -auth.message.blocked=This ip is blocked for 24 hours -message.accountVerified=Your account verified successfully -message.error=Error Occurred -message.updatePasswordSuccess=Password updated successfully -message.changePassword=Change Password -label.user.newPassword=New Password -label.user.oldPassword=Old Password diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..43ead50 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,94 @@ +-- Simplified schema for `springuser` + +-- Sequence structure +DROP SEQUENCE IF EXISTS `password_reset_token_seq`; +CREATE SEQUENCE `password_reset_token_seq` START WITH 1 INCREMENT BY 50 CACHE 1000 ENGINE=InnoDB; + +DROP SEQUENCE IF EXISTS `privilege_seq`; +CREATE SEQUENCE `privilege_seq` START WITH 1 INCREMENT BY 50 CACHE 1000 ENGINE=InnoDB; + +DROP SEQUENCE IF EXISTS `role_seq`; +CREATE SEQUENCE `role_seq` START WITH 1 INCREMENT BY 50 CACHE 1000 ENGINE=InnoDB; + +DROP SEQUENCE IF EXISTS `user_account_seq`; +CREATE SEQUENCE `user_account_seq` START WITH 1 INCREMENT BY 50 CACHE 1000 ENGINE=InnoDB; + +DROP SEQUENCE IF EXISTS `verification_token_seq`; +CREATE SEQUENCE `verification_token_seq` START WITH 1 INCREMENT BY 50 CACHE 1000 ENGINE=InnoDB; + +-- Table structure +DROP TABLE IF EXISTS `password_reset_token`; +CREATE TABLE `password_reset_token` ( + `id` BIGINT(20) NOT NULL, + `expiry_date` DATETIME(6) DEFAULT NULL, + `token` VARCHAR(255) DEFAULT NULL, + `user_id` BIGINT(20) NOT NULL, + PRIMARY KEY (`id`), + KEY `FKns9q9f0f318uaoxiqn6lka9ux` (`user_id`), + CONSTRAINT `FKns9q9f0f318uaoxiqn6lka9ux` FOREIGN KEY (`user_id`) REFERENCES `user_account` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +DROP TABLE IF EXISTS `privilege`; +CREATE TABLE `privilege` ( + `id` BIGINT(20) NOT NULL, + `description` VARCHAR(255) DEFAULT NULL, + `name` VARCHAR(255) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +DROP TABLE IF EXISTS `role`; +CREATE TABLE `role` ( + `id` BIGINT(20) NOT NULL, + `description` VARCHAR(255) DEFAULT NULL, + `name` VARCHAR(255) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +DROP TABLE IF EXISTS `roles_privileges`; +CREATE TABLE `roles_privileges` ( + `role_id` BIGINT(20) NOT NULL, + `privilege_id` BIGINT(20) NOT NULL, + PRIMARY KEY (`role_id`, `privilege_id`), + KEY `FK5yjwxw2gvfyu76j3rgqwo685u` (`privilege_id`), + CONSTRAINT `FK5yjwxw2gvfyu76j3rgqwo685u` FOREIGN KEY (`privilege_id`) REFERENCES `privilege` (`id`), + CONSTRAINT `FK9h2vewsqh8luhfq71xokh4who` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +DROP TABLE IF EXISTS `user_account`; +CREATE TABLE `user_account` ( + `id` BIGINT(20) NOT NULL, + `email` VARCHAR(255) NOT NULL, + `enabled` BIT(1) NOT NULL, + `first_name` VARCHAR(255) DEFAULT NULL, + `last_activity_date` DATETIME(6) DEFAULT NULL, + `last_name` VARCHAR(255) DEFAULT NULL, + `locked` BIT(1) NOT NULL, + `password` VARCHAR(60) DEFAULT NULL, + `provider` ENUM('LOCAL','FACEBOOK','GOOGLE','APPLE') DEFAULT NULL, + `registration_date` DATETIME(6) DEFAULT NULL, + `failed_login_attempts` INT(11) NOT NULL, + `locked_date` DATETIME(6) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UK_hl02wv5hym99ys465woijmfib` (`email`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +DROP TABLE IF EXISTS `users_roles`; +CREATE TABLE `users_roles` ( + `user_id` BIGINT(20) NOT NULL, + `role_id` BIGINT(20) NOT NULL, + KEY `FKt4v0rrweyk393bdgt107vdx0x` (`role_id`), + KEY `FKci4mdvg1fmo9eqmwno1y9o0fa` (`user_id`), + CONSTRAINT `FKci4mdvg1fmo9eqmwno1y9o0fa` FOREIGN KEY (`user_id`) REFERENCES `user_account` (`id`), + CONSTRAINT `FKt4v0rrweyk393bdgt107vdx0x` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +DROP TABLE IF EXISTS `verification_token`; +CREATE TABLE `verification_token` ( + `id` BIGINT(20) NOT NULL, + `expiry_date` DATETIME(6) DEFAULT NULL, + `token` VARCHAR(255) DEFAULT NULL, + `user_id` BIGINT(20) NOT NULL, + PRIMARY KEY (`id`), + KEY `FK_VERIFY_USER` (`user_id`), + CONSTRAINT `FK_VERIFY_USER` FOREIGN KEY (`user_id`) REFERENCES `user_account` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; diff --git a/src/main/resources/static/css/nucleo-icons.css b/src/main/resources/static/css/nucleo-icons.css deleted file mode 100644 index d77d1db..0000000 --- a/src/main/resources/static/css/nucleo-icons.css +++ /dev/null @@ -1,597 +0,0 @@ -/*-------------------------------- - -hermes-dashboard-icons Web Font - built using nucleoapp.com -License - nucleoapp.com/license/ - --------------------------------- */ -@font-face { - font-family: 'NucleoIcons'; - src: url('../fonts/nucleo-icons.eot'); - src: url('../fonts/nucleo-icons.eot') format('embedded-opentype'), url('../fonts/nucleo-icons.woff2') format('woff2'), url('../fonts/nucleo-icons.woff') format('woff'), url('../fonts/nucleo-icons.ttf') format('truetype'), url('../fonts/nucleo-icons.svg') format('svg'); - font-weight: normal; - font-style: normal; -} - -/*------------------------ - base class definition --------------------------*/ -.ni { - display: inline-block; - font: normal normal normal 14px/1 NucleoIcons; - font-size: inherit; - text-rendering: auto; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -/*------------------------ - change icon size --------------------------*/ -.ni-lg { - font-size: 1.33333333em; - line-height: 0.75em; - vertical-align: -15%; -} - -.ni-2x { - font-size: 2em; -} - -.ni-3x { - font-size: 3em; -} - -.ni-4x { - font-size: 4em; -} - -.ni-5x { - font-size: 5em; -} - -/*---------------------------------- - add a square/circle background ------------------------------------*/ -.ni.square, -.ni.circle { - padding: 0.33333333em; - vertical-align: -16%; - background-color: #eee; -} - -.ni.circle { - border-radius: 50%; -} - -/*------------------------ - list icons --------------------------*/ -.ni-ul { - padding-left: 0; - margin-left: 2.14285714em; - list-style-type: none; -} - -.ni-ul>li { - position: relative; -} - -.ni-ul>li>.ni { - position: absolute; - left: -1.57142857em; - top: 0.14285714em; - text-align: center; -} - -.ni-ul>li>.ni.lg { - top: 0; - left: -1.35714286em; -} - -.ni-ul>li>.ni.circle, -.ni-ul>li>.ni.square { - top: -0.19047619em; - left: -1.9047619em; -} - -/*------------------------ - spinning icons --------------------------*/ -.ni.spin { - -webkit-animation: nc-spin 2s infinite linear; - -moz-animation: nc-spin 2s infinite linear; - animation: nc-spin 2s infinite linear; -} - -@-webkit-keyframes nc-spin { - 0% { - -webkit-transform: rotate(0deg); - } - - 100% { - -webkit-transform: rotate(360deg); - } -} - -@-moz-keyframes nc-spin { - 0% { - -moz-transform: rotate(0deg); - } - - 100% { - -moz-transform: rotate(360deg); - } -} - -@keyframes nc-spin { - 0% { - -webkit-transform: rotate(0deg); - -moz-transform: rotate(0deg); - -ms-transform: rotate(0deg); - -o-transform: rotate(0deg); - transform: rotate(0deg); - } - - 100% { - -webkit-transform: rotate(360deg); - -moz-transform: rotate(360deg); - -ms-transform: rotate(360deg); - -o-transform: rotate(360deg); - transform: rotate(360deg); - } -} - -/*------------------------ - rotated/flipped icons --------------------------*/ -.ni.rotate-90 { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); - -webkit-transform: rotate(90deg); - -moz-transform: rotate(90deg); - -ms-transform: rotate(90deg); - -o-transform: rotate(90deg); - transform: rotate(90deg); -} - -.ni.rotate-180 { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); - -webkit-transform: rotate(180deg); - -moz-transform: rotate(180deg); - -ms-transform: rotate(180deg); - -o-transform: rotate(180deg); - transform: rotate(180deg); -} - -.ni.rotate-270 { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); - -webkit-transform: rotate(270deg); - -moz-transform: rotate(270deg); - -ms-transform: rotate(270deg); - -o-transform: rotate(270deg); - transform: rotate(270deg); -} - -.ni.flip-y { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0); - -webkit-transform: scale(-1, 1); - -moz-transform: scale(-1, 1); - -ms-transform: scale(-1, 1); - -o-transform: scale(-1, 1); - transform: scale(-1, 1); -} - -.ni.flip-x { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); - -webkit-transform: scale(1, -1); - -moz-transform: scale(1, -1); - -ms-transform: scale(1, -1); - -o-transform: scale(1, -1); - transform: scale(1, -1); -} - -/*------------------------ - font icons --------------------------*/ - -.ni-active-40::before { - content: "\ea02"; -} - -.ni-air-baloon::before { - content: "\ea03"; -} - -.ni-album-2::before { - content: "\ea04"; -} - -.ni-align-center::before { - content: "\ea05"; -} - -.ni-align-left-2::before { - content: "\ea06"; -} - -.ni-ambulance::before { - content: "\ea07"; -} - -.ni-app::before { - content: "\ea08"; -} - -.ni-archive-2::before { - content: "\ea09"; -} - -.ni-atom::before { - content: "\ea0a"; -} - -.ni-badge::before { - content: "\ea0b"; -} - -.ni-bag-17::before { - content: "\ea0c"; -} - -.ni-basket::before { - content: "\ea0d"; -} - -.ni-bell-55::before { - content: "\ea0e"; -} - -.ni-bold-down::before { - content: "\ea0f"; -} - -.ni-bold-left::before { - content: "\ea10"; -} - -.ni-bold-right::before { - content: "\ea11"; -} - -.ni-bold-up::before { - content: "\ea12"; -} - -.ni-bold::before { - content: "\ea13"; -} - -.ni-book-bookmark::before { - content: "\ea14"; -} - -.ni-books::before { - content: "\ea15"; -} - -.ni-box-2::before { - content: "\ea16"; -} - -.ni-briefcase-24::before { - content: "\ea17"; -} - -.ni-building::before { - content: "\ea18"; -} - -.ni-bulb-61::before { - content: "\ea19"; -} - -.ni-bullet-list-67::before { - content: "\ea1a"; -} - -.ni-bus-front-12::before { - content: "\ea1b"; -} - -.ni-button-pause::before { - content: "\ea1c"; -} - -.ni-button-play::before { - content: "\ea1d"; -} - -.ni-button-power::before { - content: "\ea1e"; -} - -.ni-calendar-grid-58::before { - content: "\ea1f"; -} - -.ni-camera-compact::before { - content: "\ea20"; -} - -.ni-caps-small::before { - content: "\ea21"; -} - -.ni-cart::before { - content: "\ea22"; -} - -.ni-chart-bar-32::before { - content: "\ea23"; -} - -.ni-chart-pie-35::before { - content: "\ea24"; -} - -.ni-chat-round::before { - content: "\ea25"; -} - -.ni-check-bold::before { - content: "\ea26"; -} - -.ni-circle-08::before { - content: "\ea27"; -} - -.ni-cloud-download-95::before { - content: "\ea28"; -} - -.ni-cloud-upload-96::before { - content: "\ea29"; -} - -.ni-compass-04::before { - content: "\ea2a"; -} - -.ni-controller::before { - content: "\ea2b"; -} - -.ni-credit-card::before { - content: "\ea2c"; -} - -.ni-curved-next::before { - content: "\ea2d"; -} - -.ni-delivery-fast::before { - content: "\ea2e"; -} - -.ni-diamond::before { - content: "\ea2f"; -} - -.ni-email-83::before { - content: "\ea30"; -} - -.ni-fat-add::before { - content: "\ea31"; -} - -.ni-fat-delete::before { - content: "\ea32"; -} - -.ni-fat-remove::before { - content: "\ea33"; -} - -.ni-favourite-28::before { - content: "\ea34"; -} - -.ni-folder-17::before { - content: "\ea35"; -} - -.ni-glasses-2::before { - content: "\ea36"; -} - -.ni-hat-3::before { - content: "\ea37"; -} - -.ni-headphones::before { - content: "\ea38"; -} - -.ni-html5::before { - content: "\ea39"; -} - -.ni-istanbul::before { - content: "\ea3a"; -} - -.ni-key-25::before { - content: "\ea3b"; -} - -.ni-laptop::before { - content: "\ea3c"; -} - -.ni-like-2::before { - content: "\ea3d"; -} - -.ni-lock-circle-open::before { - content: "\ea3e"; -} - -.ni-map-big::before { - content: "\ea3f"; -} - -.ni-mobile-button::before { - content: "\ea40"; -} - -.ni-money-coins::before { - content: "\ea41"; -} - -.ni-note-03::before { - content: "\ea42"; -} - -.ni-notification-70::before { - content: "\ea43"; -} - -.ni-palette::before { - content: "\ea44"; -} - -.ni-paper-diploma::before { - content: "\ea45"; -} - -.ni-pin-3::before { - content: "\ea46"; -} - -.ni-planet::before { - content: "\ea47"; -} - -.ni-ruler-pencil::before { - content: "\ea48"; -} - -.ni-satisfied::before { - content: "\ea49"; -} - -.ni-scissors::before { - content: "\ea4a"; -} - -.ni-send::before { - content: "\ea4b"; -} - -.ni-settings-gear-65::before { - content: "\ea4c"; -} - -.ni-settings::before { - content: "\ea4d"; -} - -.ni-single-02::before { - content: "\ea4e"; -} - -.ni-single-copy-04::before { - content: "\ea4f"; -} - -.ni-sound-wave::before { - content: "\ea50"; -} - -.ni-spaceship::before { - content: "\ea51"; -} - -.ni-square-pin::before { - content: "\ea52"; -} - -.ni-support-16::before { - content: "\ea53"; -} - -.ni-tablet-button::before { - content: "\ea54"; -} - -.ni-tag::before { - content: "\ea55"; -} - -.ni-tie-bow::before { - content: "\ea56"; -} - -.ni-time-alarm::before { - content: "\ea57"; -} - -.ni-trophy::before { - content: "\ea58"; -} - -.ni-tv-2::before { - content: "\ea59"; -} - -.ni-umbrella-13::before { - content: "\ea5a"; -} - -.ni-user-run::before { - content: "\ea5b"; -} - -.ni-vector::before { - content: "\ea5c"; -} - -.ni-watch-time::before { - content: "\ea5d"; -} - -.ni-world::before { - content: "\ea5e"; -} - -.ni-zoom-split-in::before { - content: "\ea5f"; -} - -.ni-collection::before { - content: "\ea60"; -} - -.ni-image::before { - content: "\ea61"; -} - -.ni-shop::before { - content: "\ea62"; -} - -.ni-ungroup::before { - content: "\ea63"; -} - -.ni-world-2::before { - content: "\ea64"; -} - -.ni-ui-04::before { - content: "\ea65"; -} - - -/* all icon font classes list here */ \ No newline at end of file diff --git a/src/main/resources/static/css/nucleo-svg.css b/src/main/resources/static/css/nucleo-svg.css deleted file mode 100644 index c68c10e..0000000 --- a/src/main/resources/static/css/nucleo-svg.css +++ /dev/null @@ -1,135 +0,0 @@ -/* Generated using nucleoapp.com */ -/* -------------------------------- - -Icon colors - --------------------------------- */ - -.icon { - display: inline-block; - /* icon primary color */ - color: #111111; - height: 1em; - width: 1em; -} - -.icon use { - /* icon secondary color - fill */ - fill: #7ea6f6; -} - -.icon.icon-outline use { - /* icon secondary color - stroke */ - stroke: #7ea6f6; -} - -/* -------------------------------- - -Change icon size - --------------------------------- */ - -.icon-xs { - height: 0.5em; - width: 0.5em; -} - -.icon-sm { - height: 0.8em; - width: 0.8em; -} - -.icon-lg { - height: 1.6em; - width: 1.6em; -} - -.icon-xl { - height: 2em; - width: 2em; -} - -/* -------------------------------- - -Align icon and text - --------------------------------- */ - -.icon-text-aligner { - /* add this class to parent element that contains icon + text */ - display: flex; - align-items: center; -} - -.icon-text-aligner .icon { - color: inherit; - margin-right: 0.4em; -} - -.icon-text-aligner .icon use { - color: inherit; - fill: currentColor; -} - -.icon-text-aligner .icon.icon-outline use { - stroke: currentColor; -} - -/* -------------------------------- - -Icon reset values - used to enable color customizations - --------------------------------- */ - -.icon { - fill: currentColor; - stroke: none; -} - -.icon.icon-outline { - fill: none; - stroke: currentColor; -} - -.icon use { - stroke: none; -} - -.icon.icon-outline use { - fill: none; -} - -/* -------------------------------- - -Stroke effects - Nucleo outline icons - -- 16px icons -> up to 1px stroke (16px outline icons do not support stroke changes) -- 24px, 32px icons -> up to 2px stroke -- 48px, 64px icons -> up to 4px stroke - --------------------------------- */ - -.icon-outline.icon-stroke-1 { - stroke-width: 1px; -} - -.icon-outline.icon-stroke-2 { - stroke-width: 2px; -} - -.icon-outline.icon-stroke-3 { - stroke-width: 3px; -} - -.icon-outline.icon-stroke-4 { - stroke-width: 4px; -} - -.icon-outline.icon-stroke-1 use, -.icon-outline.icon-stroke-3 use { - -webkit-transform: translateX(0.5px) translateY(0.5px); - -moz-transform: translateX(0.5px) translateY(0.5px); - -ms-transform: translateX(0.5px) translateY(0.5px); - -o-transform: translateX(0.5px) translateY(0.5px); - transform: translateX(0.5px) translateY(0.5px); -} \ No newline at end of file diff --git a/src/main/resources/static/css/perfect-scrollbar.css b/src/main/resources/static/css/perfect-scrollbar.css deleted file mode 100644 index d16dda9..0000000 --- a/src/main/resources/static/css/perfect-scrollbar.css +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Container style - */ -.ps { - overflow: hidden !important; - overflow-anchor: none; - -ms-overflow-style: none; - touch-action: auto; - -ms-touch-action: auto; -} - -/* - * Scrollbar rail styles - */ -.ps__rail-x { - display: none; - opacity: 0; - transition: background-color 0.2s linear, opacity 0.2s linear; - -webkit-transition: background-color 0.2s linear, opacity 0.2s linear; - height: 15px; - /* there must be 'bottom' or 'top' for ps__rail-x */ - bottom: 0px; - /* please don't change 'position' */ - position: absolute; -} - -.ps__rail-y { - display: none; - opacity: 0; - transition: background-color 0.2s linear, opacity 0.2s linear; - -webkit-transition: background-color 0.2s linear, opacity 0.2s linear; - width: 15px; - /* there must be 'right' or 'left' for ps__rail-y */ - right: 0; - /* please don't change 'position' */ - position: absolute; -} - -.ps--active-x > .ps__rail-x, -.ps--active-y > .ps__rail-y { - display: block; - background-color: transparent; -} - -.ps:hover > .ps__rail-x, -.ps:hover > .ps__rail-y, -.ps--focus > .ps__rail-x, -.ps--focus > .ps__rail-y, -.ps--scrolling-x > .ps__rail-x, -.ps--scrolling-y > .ps__rail-y { - opacity: 0.6; -} - -.ps .ps__rail-x:hover, -.ps .ps__rail-y:hover, -.ps .ps__rail-x:focus, -.ps .ps__rail-y:focus, -.ps .ps__rail-x.ps--clicking, -.ps .ps__rail-y.ps--clicking { - background-color: #eee; - opacity: 0.9; -} - -/* - * Scrollbar thumb styles - */ -.ps__thumb-x { - background-color: #aaa; - border-radius: 6px; - transition: background-color 0.2s linear, height 0.2s ease-in-out; - -webkit-transition: background-color 0.2s linear, height 0.2s ease-in-out; - height: 6px; - /* there must be 'bottom' for ps__thumb-x */ - bottom: 2px; - /* please don't change 'position' */ - position: absolute; -} - -.ps__thumb-y { - background-color: #aaa; - border-radius: 6px; - transition: background-color 0.2s linear, width 0.2s ease-in-out; - -webkit-transition: background-color 0.2s linear, width 0.2s ease-in-out; - width: 6px; - /* there must be 'right' for ps__thumb-y */ - right: 2px; - /* please don't change 'position' */ - position: absolute; -} - -.ps__rail-x:hover > .ps__thumb-x, -.ps__rail-x:focus > .ps__thumb-x, -.ps__rail-x.ps--clicking .ps__thumb-x { - background-color: #999; - height: 11px; -} - -.ps__rail-y:hover > .ps__thumb-y, -.ps__rail-y:focus > .ps__thumb-y, -.ps__rail-y.ps--clicking .ps__thumb-y { - background-color: #999; - width: 11px; -} - -/* MS supports */ -@supports (-ms-overflow-style: none) { - .ps { - overflow: auto !important; - } -} - -@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { - .ps { - overflow: auto !important; - } -} diff --git a/src/main/resources/static/css/soft-ui-dashboard-tailwind.css b/src/main/resources/static/css/soft-ui-dashboard-tailwind.css deleted file mode 100644 index b7d35c3..0000000 --- a/src/main/resources/static/css/soft-ui-dashboard-tailwind.css +++ /dev/null @@ -1,4241 +0,0 @@ -/*! - -========================================================= -* Soft UI Dashboard Tailwind - v1.0.4 -========================================================= - -* Product Page: https://www.creative-tim.com/product/soft-ui-dashboard-tailwind -* Copyright 2022 Creative Tim (https://www.creative-tim.com) -* Licensed under MIT (site.license) - -* Coded by www.creative-tim.com - -========================================================= - -* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -*/ - -/*! tailwindcss v3.1.6 | MIT License | https://tailwindcss.com - -*/ - -/* -1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) -2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) -*/ - -*, -::before, -::after { - box-sizing: border-box; - /* 1 */ - border-width: 0; - /* 2 */ - border-style: solid; - /* 2 */ - border-color: #e9ecef; - /* 2 */ -} - -::before, -::after { - --tw-content: ''; -} - -/* -1. Use a consistent sensible line-height in all browsers. -2. Prevent adjustments of font size after orientation changes in iOS. -3. Use a more readable tab size. -4. Use the user's configured `sans` font-family by default. -*/ - -html { - line-height: 1.5; - /* 1 */ - -webkit-text-size-adjust: 100%; - /* 2 */ - -moz-tab-size: 4; - /* 3 */ - -o-tab-size: 4; - tab-size: 4; - /* 3 */ - font-family: Open Sans; - /* 4 */ -} - -/* -1. Remove the margin in all browsers. -2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. -*/ - -body { - margin: 0; - /* 1 */ - line-height: inherit; - /* 2 */ -} - -/* -1. Add the correct height in Firefox. -2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) -3. Ensure horizontal rules are visible by default. -*/ - -hr { - height: 0; - /* 1 */ - color: inherit; - /* 2 */ - border-top-width: 1px; - /* 3 */ -} - -/* -Add the correct text decoration in Chrome, Edge, and Safari. -*/ - -abbr:where([title]) { - -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; -} - -/* -Remove the default font size and weight for headings. -*/ - -h1, -h2, -h3, -h4, -h5, -h6 { - font-size: inherit; - font-weight: inherit; -} - -/* -Reset links to optimize for opt-in styling instead of opt-out. -*/ - -a { - color: inherit; - text-decoration: inherit; -} - -/* -Add the correct font weight in Edge and Safari. -*/ - -b, -strong { - font-weight: bolder; -} - -/* -1. Use the user's configured `mono` font family by default. -2. Correct the odd `em` font sizing in all browsers. -*/ - -code, -kbd, -samp, -pre { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - /* 1 */ - font-size: 1em; - /* 2 */ -} - -/* -Add the correct font size in all browsers. -*/ - -small { - font-size: 80%; -} - -/* -Prevent `sub` and `sup` elements from affecting the line height in all browsers. -*/ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: -0.5em; -} - -/* -1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) -2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) -3. Remove gaps between table borders by default. -*/ - -table { - text-indent: 0; - /* 1 */ - border-color: inherit; - /* 2 */ - border-collapse: collapse; - /* 3 */ -} - -/* -1. Change the font styles in all browsers. -2. Remove the margin in Firefox and Safari. -3. Remove default padding in all browsers. -*/ - -button, -input, -optgroup, -select, -textarea { - font-family: inherit; - /* 1 */ - font-size: 100%; - /* 1 */ - font-weight: inherit; - /* 1 */ - line-height: inherit; - /* 1 */ - color: inherit; - /* 1 */ - margin: 0; - /* 2 */ - padding: 0; - /* 3 */ -} - -/* -Remove the inheritance of text transform in Edge and Firefox. -*/ - -button, -select { - text-transform: none; -} - -/* -1. Correct the inability to style clickable types in iOS and Safari. -2. Remove default button styles. -*/ - -button, -[type='button'], -[type='reset'], -[type='submit'] { - -webkit-appearance: button; - /* 1 */ - background-color: transparent; - /* 2 */ - background-image: none; - /* 2 */ -} - -/* -Use the modern Firefox focus style for all focusable elements. -*/ - -:-moz-focusring { - outline: auto; -} - -/* -Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) -*/ - -:-moz-ui-invalid { - box-shadow: none; -} - -/* -Add the correct vertical alignment in Chrome and Firefox. -*/ - -progress { - vertical-align: baseline; -} - -/* -Correct the cursor style of increment and decrement buttons in Safari. -*/ - -::-webkit-inner-spin-button, -::-webkit-outer-spin-button { - height: auto; -} - -/* -1. Correct the odd appearance in Chrome and Safari. -2. Correct the outline style in Safari. -*/ - -[type='search'] { - -webkit-appearance: textfield; - /* 1 */ - outline-offset: -2px; - /* 2 */ -} - -/* -Remove the inner padding in Chrome and Safari on macOS. -*/ - -::-webkit-search-decoration { - -webkit-appearance: none; -} - -/* -1. Correct the inability to style clickable types in iOS and Safari. -2. Change font properties to `inherit` in Safari. -*/ - -::-webkit-file-upload-button { - -webkit-appearance: button; - /* 1 */ - font: inherit; - /* 2 */ -} - -/* -Add the correct display in Chrome and Safari. -*/ - -summary { - display: list-item; -} - -/* -Removes the default spacing and border for appropriate elements. -*/ - -blockquote, -dl, -dd, -h1, -h2, -h3, -h4, -h5, -h6, -hr, -figure, -p, -pre { - margin: 0; -} - -fieldset { - margin: 0; - padding: 0; -} - -legend { - padding: 0; -} - -ol, -ul, -menu { - list-style: none; - margin: 0; - padding: 0; -} - -/* -Prevent resizing textareas horizontally by default. -*/ - -textarea { - resize: vertical; -} - -/* -1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) -2. Set the default placeholder color to the user's configured gray 400 color. -*/ - -input::-moz-placeholder, textarea::-moz-placeholder { - opacity: 1; - /* 1 */ - color: #ced4da; - /* 2 */ -} - -input::placeholder, -textarea::placeholder { - opacity: 1; - /* 1 */ - color: #ced4da; - /* 2 */ -} - -/* -Set the default cursor for buttons. -*/ - -button, -[role="button"] { - cursor: pointer; -} - -/* -Make sure disabled buttons don't get the pointer cursor. -*/ - -:disabled { - cursor: default; -} - -/* -1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) -2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) - This can trigger a poorly considered lint error in some tools but is included by design. -*/ - -img, -svg, -video, -canvas, -audio, -iframe, -embed, -object { - display: block; - /* 1 */ - vertical-align: middle; - /* 2 */ -} - -/* -Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) -*/ - -img, -video { - max-width: 100%; - height: auto; -} - -*, ::before, ::after { - --tw-border-spacing-x: 0; - --tw-border-spacing-y: 0; - --tw-translate-x: 0; - --tw-translate-y: 0; - --tw-rotate: 0; - --tw-skew-x: 0; - --tw-skew-y: 0; - --tw-scale-x: 1; - --tw-scale-y: 1; - --tw-pan-x: ; - --tw-pan-y: ; - --tw-pinch-zoom: ; - --tw-scroll-snap-strictness: proximity; - --tw-ordinal: ; - --tw-slashed-zero: ; - --tw-numeric-figure: ; - --tw-numeric-spacing: ; - --tw-numeric-fraction: ; - --tw-ring-inset: ; - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-color: rgb(59 130 246 / 0.5); - --tw-ring-offset-shadow: 0 0 #0000; - --tw-ring-shadow: 0 0 #0000; - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - --tw-blur: ; - --tw-brightness: ; - --tw-contrast: ; - --tw-grayscale: ; - --tw-hue-rotate: ; - --tw-invert: ; - --tw-saturate: ; - --tw-sepia: ; - --tw-drop-shadow: ; - --tw-backdrop-blur: ; - --tw-backdrop-brightness: ; - --tw-backdrop-contrast: ; - --tw-backdrop-grayscale: ; - --tw-backdrop-hue-rotate: ; - --tw-backdrop-invert: ; - --tw-backdrop-opacity: ; - --tw-backdrop-saturate: ; - --tw-backdrop-sepia: ; -} - -::-webkit-backdrop { - --tw-border-spacing-x: 0; - --tw-border-spacing-y: 0; - --tw-translate-x: 0; - --tw-translate-y: 0; - --tw-rotate: 0; - --tw-skew-x: 0; - --tw-skew-y: 0; - --tw-scale-x: 1; - --tw-scale-y: 1; - --tw-pan-x: ; - --tw-pan-y: ; - --tw-pinch-zoom: ; - --tw-scroll-snap-strictness: proximity; - --tw-ordinal: ; - --tw-slashed-zero: ; - --tw-numeric-figure: ; - --tw-numeric-spacing: ; - --tw-numeric-fraction: ; - --tw-ring-inset: ; - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-color: rgb(59 130 246 / 0.5); - --tw-ring-offset-shadow: 0 0 #0000; - --tw-ring-shadow: 0 0 #0000; - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - --tw-blur: ; - --tw-brightness: ; - --tw-contrast: ; - --tw-grayscale: ; - --tw-hue-rotate: ; - --tw-invert: ; - --tw-saturate: ; - --tw-sepia: ; - --tw-drop-shadow: ; - --tw-backdrop-blur: ; - --tw-backdrop-brightness: ; - --tw-backdrop-contrast: ; - --tw-backdrop-grayscale: ; - --tw-backdrop-hue-rotate: ; - --tw-backdrop-invert: ; - --tw-backdrop-opacity: ; - --tw-backdrop-saturate: ; - --tw-backdrop-sepia: ; -} - -::backdrop { - --tw-border-spacing-x: 0; - --tw-border-spacing-y: 0; - --tw-translate-x: 0; - --tw-translate-y: 0; - --tw-rotate: 0; - --tw-skew-x: 0; - --tw-skew-y: 0; - --tw-scale-x: 1; - --tw-scale-y: 1; - --tw-pan-x: ; - --tw-pan-y: ; - --tw-pinch-zoom: ; - --tw-scroll-snap-strictness: proximity; - --tw-ordinal: ; - --tw-slashed-zero: ; - --tw-numeric-figure: ; - --tw-numeric-spacing: ; - --tw-numeric-fraction: ; - --tw-ring-inset: ; - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-color: rgb(59 130 246 / 0.5); - --tw-ring-offset-shadow: 0 0 #0000; - --tw-ring-shadow: 0 0 #0000; - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - --tw-blur: ; - --tw-brightness: ; - --tw-contrast: ; - --tw-grayscale: ; - --tw-hue-rotate: ; - --tw-invert: ; - --tw-saturate: ; - --tw-sepia: ; - --tw-drop-shadow: ; - --tw-backdrop-blur: ; - --tw-backdrop-brightness: ; - --tw-backdrop-contrast: ; - --tw-backdrop-grayscale: ; - --tw-backdrop-hue-rotate: ; - --tw-backdrop-invert: ; - --tw-backdrop-opacity: ; - --tw-backdrop-saturate: ; - --tw-backdrop-sepia: ; -} - -.container { - width: 100%; - margin-right: auto; - margin-left: auto; - padding-right: 1.5rem; - padding-left: 1.5rem; -} - -@media (min-width: 576px) { - .container { - max-width: 576px; - } -} - -@media (min-width: 768px) { - .container { - max-width: 768px; - } -} - -@media (min-width: 992px) { - .container { - max-width: 992px; - } -} - -@media (min-width: 1200px) { - .container { - max-width: 1200px; - } -} - -@media (min-width: 1320px) { - .container { - max-width: 1320px; - } -} - -a { - letter-spacing: -0.025rem; -} - -hr { - margin: 1rem 0; - border: 0; - opacity: .25; -} - -img { - max-width: none; -} - -label { - display: inline-block; -} - -p { - line-height: 1.625; - font-weight: 400; - margin-bottom: 1rem; -} - -small { - font-size: .875em; -} - -svg { - display: inline; -} - -table { - border-collapse: inherit; -} - -h1, h2, h3, h4, h5, h6 { - margin-bottom: .5rem; - color: #344767; -} - -h1, h2, h3, h4 { - letter-spacing: -0.05rem; -} - -h1, h2, h3 { - font-weight: 700; -} - -h4, h5, h6 { - font-weight: 600; -} - -h1 { - font-size: 3rem; - line-height: 1.25; -} - -h2 { - font-size: 2.25rem; - line-height: 1.3; -} - -h3 { - font-size: 1.875rem; - line-height: 1.375; -} - -h4 { - font-size: 1.5rem; - line-height: 1.375; -} - -h5 { - font-size: 1.25rem; - line-height: 1.375; -} - -h6 { - font-size: 1rem; - line-height: 1.625; -} - -.pointer-events-none { - pointer-events: none; -} - -.visible { - visibility: visible; -} - -.invisible { - visibility: hidden; -} - -.fixed { - position: fixed; -} - -.absolute { - position: absolute; -} - -.relative { - position: relative; -} - -.sticky { - position: -webkit-sticky; - position: sticky; -} - -.inset-y-0 { - top: 0px; - bottom: 0px; -} - -.inset-x-0 { - left: 0px; - right: 0px; -} - -.top-0 { - top: 0px; -} - -.right-0 { - right: 0px; -} - -.top-3\.5 { - top: 0.875rem; -} - -.top-3 { - top: 0.75rem; -} - -.left-0 { - left: 0px; -} - -.left-4 { - left: 1rem; -} - -.-top-1\.5 { - top: -0.375rem; -} - -.-top-1 { - top: -0.25rem; -} - -.bottom-7\.5 { - bottom: 1.875rem; -} - -.right-7\.5 { - right: 1.875rem; -} - -.bottom-7 { - bottom: 1.75rem; -} - -.right-7 { - right: 1.75rem; -} - -.-right-90 { - right: -22.5rem; -} - -.left-auto { - left: auto; -} - -.bottom-0 { - bottom: 0px; -} - -.top-auto { - top: auto; -} - -.top-31\/100 { - top: 31%; -} - -.right-4 { - right: 1rem; -} - -.left-7\.5 { - left: 1.875rem; -} - -.right-auto { - right: auto; -} - -.left-7 { - left: 1.75rem; -} - -.-left-90 { - left: -22.5rem; -} - -.-right-40 { - right: -10rem; -} - -.top-\[1\%\] { - top: 1%; -} - -.z-990 { - z-index: 990; -} - -.z-20 { - z-index: 20; -} - -.z-10 { - z-index: 10; -} - -.z-50 { - z-index: 50; -} - -.z-100 { - z-index: 100; -} - -.z-sticky { - z-index: 1020; -} - -.z-30 { - z-index: 30; -} - -.z-0 { - z-index: 0; -} - -.z-110 { - z-index: 110; -} - -.float-right { - float: right; -} - -.float-left { - float: left; -} - -.clear-both { - clear: both; -} - -.m-0 { - margin: 0px; -} - -.m-4 { - margin: 1rem; -} - -.my-4 { - margin-top: 1rem; - margin-bottom: 1rem; -} - -.my-0 { - margin-top: 0px; - margin-bottom: 0px; -} - -.mx-4 { - margin-left: 1rem; - margin-right: 1rem; -} - -.mx-6 { - margin-left: 1.5rem; - margin-right: 1.5rem; -} - -.mx-auto { - margin-left: auto; - margin-right: auto; -} - -.my-auto { - margin-top: auto; - margin-bottom: auto; -} - -.-mx-3 { - margin-left: -0.75rem; - margin-right: -0.75rem; -} - -.my-6 { - margin-top: 1.5rem; - margin-bottom: 1.5rem; -} - -.mx-0 { - margin-left: 0px; - margin-right: 0px; -} - -.my-1 { - margin-top: 0.25rem; - margin-bottom: 0.25rem; -} - -.my-2 { - margin-top: 0.5rem; - margin-bottom: 0.5rem; -} - -.my-56 { - margin-top: 14rem; - margin-bottom: 14rem; -} - -.mx-2 { - margin-left: 0.5rem; - margin-right: 0.5rem; -} - -.ml-4 { - margin-left: 1rem; -} - -.ml-1 { - margin-left: 0.25rem; -} - -.mt-0 { - margin-top: 0px; -} - -.mb-0 { - margin-bottom: 0px; -} - -.mt-0\.5 { - margin-top: 0.125rem; -} - -.mr-2 { - margin-right: 0.5rem; -} - -.mt-4 { - margin-top: 1rem; -} - -.ml-2 { - margin-left: 0.5rem; -} - -.mb-7\.5 { - margin-bottom: 1.875rem; -} - -.mb-7 { - margin-bottom: 1.75rem; -} - -.mb-4 { - margin-bottom: 1rem; -} - -.mr-12 { - margin-right: 3rem; -} - -.mt-2 { - margin-top: 0.5rem; -} - -.-ml-px { - margin-left: -1px; -} - -.mr-4 { - margin-right: 1rem; -} - -.mb-0\.75 { - margin-bottom: 0.1875rem; -} - -.mb-2 { - margin-bottom: 0.5rem; -} - -.mb-1 { - margin-bottom: 0.25rem; -} - -.mr-1 { - margin-right: 0.25rem; -} - -.mb-6 { - margin-bottom: 1.5rem; -} - -.mt-6 { - margin-top: 1.5rem; -} - -.mb-12 { - margin-bottom: 3rem; -} - -.mt-auto { - margin-top: auto; -} - -.mt-12 { - margin-top: 3rem; -} - -.ml-auto { - margin-left: auto; -} - -.mt-1 { - margin-top: 0.25rem; -} - -.-mt-0\.38 { - margin-top: -0.095rem; -} - -.-mt-0 { - margin-top: -0px; -} - -.-ml-34 { - margin-left: -8.5rem; -} - -.-ml-4 { - margin-left: -1rem; -} - -.ml-11\.252 { - margin-left: 2.813rem; -} - -.ml-11 { - margin-left: 2.75rem; -} - -.mr-1\.25 { - margin-right: 0.3125rem; -} - -.mb-0\.5 { - margin-bottom: 0.125rem; -} - -.mr-6 { - margin-right: 1.5rem; -} - -.ml-6 { - margin-left: 1.5rem; -} - -.-mt-16 { - margin-top: -4rem; -} - -.mt-0\.54 { - margin-top: 0.135rem; -} - -.-mr-px { - margin-right: -1px; -} - -.ml-0 { - margin-left: 0px; -} - -.mr-auto { - margin-right: auto; -} - -.-mr-34 { - margin-right: -8.5rem; -} - -.-mr-4 { - margin-right: -1rem; -} - -.mr-11\.252 { - margin-right: 2.813rem; -} - -.mr-11 { - margin-right: 2.75rem; -} - -.mt-1\.75 { - margin-top: 0.4375rem; -} - -.mt-32 { - margin-top: 8rem; -} - -.-ml-12 { - margin-left: -3rem; -} - -.-mr-32 { - margin-right: -8rem; -} - -.-ml-16 { - margin-left: -4rem; -} - -.mb-32 { - margin-bottom: 8rem; -} - -.-mt-48 { - margin-top: -12rem; -} - -.-ml-6\.92 { - margin-left: -1.73rem; -} - -.-ml-6 { - margin-left: -1.5rem; -} - -.-mt-6 { - margin-top: -1.5rem; -} - -.-mt-2 { - margin-top: -0.5rem; -} - -.mt-0\.75 { - margin-top: 0.1875rem; -} - -.block { - display: block; -} - -.inline-block { - display: inline-block; -} - -.inline { - display: inline; -} - -.flex { - display: flex; -} - -.inline-flex { - display: inline-flex; -} - -.table { - display: table; -} - -.grid { - display: grid; -} - -.hidden { - display: none; -} - -.h-19\.5 { - height: 4.875rem; -} - -.h-full { - height: 100%; -} - -.h-px { - height: 1px; -} - -.h-sidenav { - height: calc(100vh - 370px); -} - -.h-8 { - height: 2rem; -} - -.h-0\.5 { - height: 0.125rem; -} - -.h-0 { - height: 0px; -} - -.h-9 { - height: 2.25rem; -} - -.h-12 { - height: 3rem; -} - -.h-5 { - height: 1.25rem; -} - -.h-0\.75 { - height: 0.1875rem; -} - -.h-1\.5 { - height: 0.375rem; -} - -.h-1 { - height: 0.25rem; -} - -.h-6 { - height: 1.5rem; -} - -.h-2 { - height: 0.5rem; -} - -.h-6\.5 { - height: 1.625rem; -} - -.h-5\.75 { - height: 1.4375rem; -} - -.h-\[80vh\] { - height: 80vh; -} - -.h-16 { - height: 4rem; -} - -.h-6\.35 { - height: 1.5875rem; -} - -.h-18\.5 { - height: 4.625rem; -} - -.h-4\.92 { - height: 1.23rem; -} - -.h-4 { - height: 1rem; -} - -.max-h-8 { - max-height: 2rem; -} - -.max-h-screen { - max-height: 100vh; -} - -.min-h-6 { - min-height: 1.5rem; -} - -.min-h-75 { - min-height: 18.75rem; -} - -.min-h-75-screen { - min-height: 75vh; -} - -.min-h-screen { - min-height: 100vh; -} - -.min-h-50-screen { - min-height: 50vh; -} - -.min-h-85-screen { - min-height: 85vh; -} - -.w-full { - width: 100%; -} - -.w-auto { - width: auto; -} - -.w-8 { - width: 2rem; -} - -.w-1\/100 { - width: 1%; -} - -.w-4\.5 { - width: 1.125rem; -} - -.w-4 { - width: 1rem; -} - -.w-9 { - width: 2.25rem; -} - -.w-2\/3 { - width: 66.666667%; -} - -.w-12 { - width: 3rem; -} - -.w-1\/2 { - width: 50%; -} - -.w-1\/4 { - width: 25%; -} - -.w-5 { - width: 1.25rem; -} - -.w-3\/4 { - width: 75%; -} - -.w-3\/5 { - width: 60%; -} - -.w-9\/10 { - width: 90%; -} - -.w-3\/10 { - width: 30%; -} - -.w-7\/12 { - width: 58.333333%; -} - -.w-5\/12 { - width: 41.666667%; -} - -.w-6 { - width: 1.5rem; -} - -.w-2 { - width: 0.5rem; -} - -.w-30 { - width: 7.5rem; -} - -.w-1\/10 { - width: 10%; -} - -.w-2\/5 { - width: 40%; -} - -.w-6\.5 { - width: 1.625rem; -} - -.w-90 { - width: 22.5rem; -} - -.w-5\.75 { - width: 1.4375rem; -} - -.w-10 { - width: 2.5rem; -} - -.w-1\/5 { - width: 20%; -} - -.w-16 { - width: 4rem; -} - -.w-6\.35 { - width: 1.5875rem; -} - -.w-18\.5 { - width: 4.625rem; -} - -.w-4\/5 { - width: 80%; -} - -.w-5\.5 { - width: 1.375rem; -} - -.w-8\/12 { - width: 66.666667%; -} - -.w-3\/12 { - width: 25%; -} - -.w-4\.92 { - width: 1.23rem; -} - -.w-0 { - width: 0px; -} - -.min-w-0 { - min-width: 0px; -} - -.min-w-44 { - min-width: 11rem; -} - -.max-w-62\.5 { - max-width: 15.625rem; -} - -.max-w-full { - max-width: 100%; -} - -.max-w-none { - max-width: none; -} - -.max-w-screen-2xl { - max-width: 1320px; -} - -.flex-auto { - flex: 1 1 auto; -} - -.flex-none { - flex: none; -} - -.flex-0 { - flex: 0 0 auto; -} - -.flex-shrink-0 { - flex-shrink: 0; -} - -.shrink-0 { - flex-shrink: 0; -} - -.flex-grow { - flex-grow: 1; -} - -.grow { - flex-grow: 1; -} - -.basis-full { - flex-basis: 100%; -} - -.basis-1\/3 { - flex-basis: 33.333333%; -} - -.origin-top { - transform-origin: top; -} - -.origin-10-10 { - transform-origin: 10% 10%; -} - -.origin-10-90 { - transform-origin: 10% 90%; -} - -.-translate-x-full { - --tw-translate-x: -100%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.-translate-x-1\/2 { - --tw-translate-x: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.translate-x-full { - --tw-translate-x: 100%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.translate-x-1\/2 { - --tw-translate-x: 50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.translate-x-0 { - --tw-translate-x: 0px; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.-translate-x-\[5px\] { - --tw-translate-x: -5px; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.translate-x-\[5px\] { - --tw-translate-x: 5px; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.rotate-45 { - --tw-rotate: 45deg; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.-rotate-45 { - --tw-rotate: -45deg; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.-skew-x-10 { - --tw-skew-x: -10deg; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.skew-x-10 { - --tw-skew-x: 10deg; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.transform { - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.cursor-pointer { - cursor: pointer; -} - -.select-none { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; -} - -.resize { - resize: both; -} - -.list-none { - list-style-type: none; -} - -.appearance-none { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} - -.flex-row { - flex-direction: row; -} - -.flex-col { - flex-direction: column; -} - -.flex-wrap { - flex-wrap: wrap; -} - -.items-start { - align-items: flex-start; -} - -.items-end { - align-items: flex-end; -} - -.items-center { - align-items: center; -} - -.items-stretch { - align-items: stretch; -} - -.justify-end { - justify-content: flex-end; -} - -.justify-center { - justify-content: center; -} - -.justify-between { - justify-content: space-between; -} - -.overflow-auto { - overflow: auto; -} - -.overflow-hidden { - overflow: hidden; -} - -.overflow-visible { - overflow: visible; -} - -.overflow-x-auto { - overflow-x: auto; -} - -.overflow-y-auto { - overflow-y: auto; -} - -.text-ellipsis { - text-overflow: ellipsis; -} - -.whitespace-nowrap { - white-space: nowrap; -} - -.break-words { - overflow-wrap: break-word; -} - -.rounded-2xl { - border-radius: 1rem; -} - -.rounded-lg { - border-radius: 0.5rem; -} - -.rounded-xl { - border-radius: 0.75rem; -} - -.rounded-sm { - border-radius: 0.125rem; -} - -.rounded { - border-radius: 0.25rem; -} - -.rounded-full { - border-radius: 9999px; -} - -.rounded-circle { - border-radius: 50%; -} - -.rounded-none { - border-radius: 0px; -} - -.rounded-10 { - border-radius: 2.5rem; -} - -.rounded-3\.5xl { - border-radius: 1.875rem; -} - -.rounded-3 { - border-radius: 0.75rem; -} - -.rounded-blur { - border-radius: 40px; -} - -.rounded-xs { - border-radius: 0.0625rem; -} - -.rounded-1\.4 { - border-radius: 0.35rem; -} - -.rounded-1 { - border-radius: 0.25rem; -} - -.rounded-1\.8 { - border-radius: 0.45rem; -} - -.rounded-t-2xl { - border-top-left-radius: 1rem; - border-top-right-radius: 1rem; -} - -.rounded-t-inherit { - border-top-left-radius: inherit; - border-top-right-radius: inherit; -} - -.rounded-b-inherit { - border-bottom-right-radius: inherit; - border-bottom-left-radius: inherit; -} - -.rounded-t-lg { - border-top-left-radius: 0.5rem; - border-top-right-radius: 0.5rem; -} - -.rounded-b-lg { - border-bottom-right-radius: 0.5rem; - border-bottom-left-radius: 0.5rem; -} - -.rounded-b-2xl { - border-bottom-right-radius: 1rem; - border-bottom-left-radius: 1rem; -} - -.rounded-tr-none { - border-top-right-radius: 0px; -} - -.rounded-br-none { - border-bottom-right-radius: 0px; -} - -.rounded-tl-none { - border-top-left-radius: 0px; -} - -.rounded-bl-none { - border-bottom-left-radius: 0px; -} - -.rounded-bl-xl { - border-bottom-left-radius: 0.75rem; -} - -.border-0 { - border-width: 0px; -} - -.border { - border-width: 1px; -} - -.border-2 { - border-width: 2px; -} - -.border-r-0 { - border-right-width: 0px; -} - -.border-b-0 { - border-bottom-width: 0px; -} - -.border-b { - border-bottom-width: 1px; -} - -.border-t-0 { - border-top-width: 0px; -} - -.border-l-0 { - border-left-width: 0px; -} - -.border-solid { - border-style: solid; -} - -.border-blue-900 { - --tw-border-opacity: 1; - border-color: rgb(0 0 125 / var(--tw-border-opacity)); -} - -.border-white { - --tw-border-opacity: 1; - border-color: rgb(255 255 255 / var(--tw-border-opacity)); -} - -.border-transparent { - border-color: transparent; -} - -.border-gray-300 { - --tw-border-opacity: 1; - border-color: rgb(210 214 218 / var(--tw-border-opacity)); -} - -.border-fuchsia-500 { - --tw-border-opacity: 1; - border-color: rgb(203 12 159 / var(--tw-border-opacity)); -} - -.border-black\/12\.5 { - border-color: rgb(0 0 0 / 0.125); -} - -.border-gray-200 { - --tw-border-opacity: 1; - border-color: rgb(233 236 239 / var(--tw-border-opacity)); -} - -.border-slate-700 { - --tw-border-opacity: 1; - border-color: rgb(52 71 103 / var(--tw-border-opacity)); -} - -.border-slate-100 { - --tw-border-opacity: 1; - border-color: rgb(222 226 230 / var(--tw-border-opacity)); -} - -.border-red-600 { - --tw-border-opacity: 1; - border-color: rgb(234 6 6 / var(--tw-border-opacity)); -} - -.border-lime-500 { - --tw-border-opacity: 1; - border-color: rgb(130 214 22 / var(--tw-border-opacity)); -} - -.border-white\/75 { - border-color: rgb(255 255 255 / 0.75); -} - -.border-slate-200 { - --tw-border-opacity: 1; - border-color: rgb(203 211 218 / var(--tw-border-opacity)); -} - -.border-b-gray-200 { - --tw-border-opacity: 1; - border-bottom-color: rgb(233 236 239 / var(--tw-border-opacity)); -} - -.border-b-transparent { - border-bottom-color: transparent; -} - -.bg-gray-50 { - --tw-bg-opacity: 1; - background-color: rgb(248 249 250 / var(--tw-bg-opacity)); -} - -.bg-white { - --tw-bg-opacity: 1; - background-color: rgb(255 255 255 / var(--tw-bg-opacity)); -} - -.bg-transparent { - background-color: transparent; -} - -.bg-slate-500 { - --tw-bg-opacity: 1; - background-color: rgb(103 116 142 / var(--tw-bg-opacity)); -} - -.bg-gray-200 { - --tw-bg-opacity: 1; - background-color: rgb(233 236 239 / var(--tw-bg-opacity)); -} - -.bg-slate-700 { - --tw-bg-opacity: 1; - background-color: rgb(52 71 103 / var(--tw-bg-opacity)); -} - -.bg-black { - --tw-bg-opacity: 1; - background-color: rgb(0 0 0 / var(--tw-bg-opacity)); -} - -.bg-inherit { - background-color: inherit; -} - -.bg-fuchsia-500 { - --tw-bg-opacity: 1; - background-color: rgb(203 12 159 / var(--tw-bg-opacity)); -} - -.bg-slate-800\/10 { - background-color: rgb(58 65 111 / 0.1); -} - -.bg-white\/10 { - background-color: rgb(255 255 255 / 0.1); -} - -.bg-white\/80 { - background-color: rgb(255 255 255 / 0.8); -} - -.bg-gray-600 { - --tw-bg-opacity: 1; - background-color: rgb(108 117 125 / var(--tw-bg-opacity)); -} - -.bg-\[hsla\(0\2c 0\%\2c 100\%\2c 0\.8\)\] { - background-color: hsla(0,0%,100%,0.8); -} - -.bg-gradient-to-r { - background-image: linear-gradient(to right, var(--tw-gradient-stops)); -} - -.bg-gradient-to-tl { - background-image: linear-gradient(to top left, var(--tw-gradient-stops)); -} - -.bg-none { - background-image: none; -} - -.from-transparent { - --tw-gradient-from: transparent; - --tw-gradient-to: rgb(0 0 0 / 0); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); -} - -.from-purple-700 { - --tw-gradient-from: #7928ca; - --tw-gradient-to: rgb(121 40 202 / 0); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); -} - -.from-slate-600 { - --tw-gradient-from: #627594; - --tw-gradient-to: rgb(98 117 148 / 0); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); -} - -.from-gray-900 { - --tw-gradient-from: #141727; - --tw-gradient-to: rgb(20 23 39 / 0); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); -} - -.from-blue-600 { - --tw-gradient-from: #2152ff; - --tw-gradient-to: rgb(33 82 255 / 0); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); -} - -.from-red-500 { - --tw-gradient-from: #f53939; - --tw-gradient-to: rgb(245 57 57 / 0); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); -} - -.from-red-600 { - --tw-gradient-from: #ea0606; - --tw-gradient-to: rgb(234 6 6 / 0); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); -} - -.from-green-600 { - --tw-gradient-from: #17ad37; - --tw-gradient-to: rgb(23 173 55 / 0); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); -} - -.from-gray-400 { - --tw-gradient-from: #ced4da; - --tw-gradient-to: rgb(206 212 218 / 0); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); -} - -.via-black\/40 { - --tw-gradient-to: rgb(0 0 0 / 0); - --tw-gradient-stops: var(--tw-gradient-from), rgb(0 0 0 / 0.4), var(--tw-gradient-to); -} - -.via-white { - --tw-gradient-to: rgb(255 255 255 / 0); - --tw-gradient-stops: var(--tw-gradient-from), #fff, var(--tw-gradient-to); -} - -.to-transparent { - --tw-gradient-to: transparent; -} - -.to-pink-500 { - --tw-gradient-to: #ff0080; -} - -.to-slate-300 { - --tw-gradient-to: #a8b8d8; -} - -.to-slate-800 { - --tw-gradient-to: #3a416f; -} - -.to-cyan-400 { - --tw-gradient-to: #21d4fd; -} - -.to-yellow-400 { - --tw-gradient-to: #fbcf33; -} - -.to-rose-400 { - --tw-gradient-to: #ff667c; -} - -.to-lime-400 { - --tw-gradient-to: #98ec2d; -} - -.to-gray-100 { - --tw-gradient-to: #ebeff4; -} - -.bg-cover { - background-size: cover; -} - -.bg-150 { - background-size: 150%; -} - -.bg-contain { - background-size: contain; -} - -.bg-clip-border { - background-clip: border-box; -} - -.bg-clip-padding { - background-clip: padding-box; -} - -.bg-clip-text { - -webkit-background-clip: text; - background-clip: text; -} - -.bg-center { - background-position: center; -} - -.bg-x-25 { - background-position: 25% 0; -} - -.bg-left { - background-position: left; -} - -.bg-right { - background-position: right; -} - -.bg-no-repeat { - background-repeat: no-repeat; -} - -.fill-slate-800 { - fill: #3a416f; -} - -.fill-current { - fill: currentColor; -} - -.fill-transparent { - fill: transparent; -} - -.stroke-0 { - stroke-width: 0; -} - -.p-0 { - padding: 0px; -} - -.p-4 { - padding: 1rem; -} - -.p-6 { - padding: 1.5rem; -} - -.p-2 { - padding: 0.5rem; -} - -.p-1\.2 { - padding: 0.3rem; -} - -.p-1 { - padding: 0.25rem; -} - -.px-8 { - padding-left: 2rem; - padding-right: 2rem; -} - -.py-6 { - padding-top: 1.5rem; - padding-bottom: 1.5rem; -} - -.py-2\.7 { - padding-top: 0.675rem; - padding-bottom: 0.675rem; -} - -.px-4 { - padding-left: 1rem; - padding-right: 1rem; -} - -.py-2 { - padding-top: 0.5rem; - padding-bottom: 0.5rem; -} - -.px-6 { - padding-left: 1.5rem; - padding-right: 1.5rem; -} - -.py-3 { - padding-top: 0.75rem; - padding-bottom: 0.75rem; -} - -.px-0 { - padding-left: 0px; - padding-right: 0px; -} - -.py-1 { - padding-top: 0.25rem; - padding-bottom: 0.25rem; -} - -.px-2\.5 { - padding-left: 0.625rem; - padding-right: 0.625rem; -} - -.px-2 { - padding-left: 0.5rem; - padding-right: 0.5rem; -} - -.py-4 { - padding-top: 1rem; - padding-bottom: 1rem; -} - -.py-1\.2 { - padding-top: 0.3rem; - padding-bottom: 0.3rem; -} - -.px-3 { - padding-left: 0.75rem; - padding-right: 0.75rem; -} - -.px-16 { - padding-left: 4rem; - padding-right: 4rem; -} - -.py-3\.5 { - padding-top: 0.875rem; - padding-bottom: 0.875rem; -} - -.py-0 { - padding-top: 0px; - padding-bottom: 0px; -} - -.px-1 { - padding-left: 0.25rem; - padding-right: 0.25rem; -} - -.py-2\.375 { - padding-top: .59375rem; - padding-bottom: .59375rem; -} - -.py-12 { - padding-top: 3rem; - padding-bottom: 3rem; -} - -.pl-0 { - padding-left: 0px; -} - -.pl-6 { - padding-left: 1.5rem; -} - -.pt-1 { - padding-top: 0.25rem; -} - -.pl-2 { - padding-left: 0.5rem; -} - -.pl-8\.75 { - padding-left: 2.1875rem; -} - -.pr-3 { - padding-right: 0.75rem; -} - -.pl-8 { - padding-left: 2rem; -} - -.pl-4 { - padding-left: 1rem; -} - -.pr-2 { - padding-right: 0.5rem; -} - -.pt-2 { - padding-top: 0.5rem; -} - -.pt-6 { - padding-top: 1.5rem; -} - -.pr-1 { - padding-right: 0.25rem; -} - -.pb-0 { - padding-bottom: 0px; -} - -.pr-6 { - padding-right: 1.5rem; -} - -.pb-2 { - padding-bottom: 0.5rem; -} - -.pt-1\.4 { - padding-top: 0.35rem; -} - -.pt-4 { - padding-top: 1rem; -} - -.pt-0 { - padding-top: 0px; -} - -.pb-1 { - padding-bottom: 0.25rem; -} - -.pr-0 { - padding-right: 0px; -} - -.pr-4 { - padding-right: 1rem; -} - -.pl-1 { - padding-left: 0.25rem; -} - -.pr-8\.75 { - padding-right: 2.1875rem; -} - -.pr-8 { - padding-right: 2rem; -} - -.pr-10 { - padding-right: 2.5rem; -} - -.pl-3 { - padding-left: 0.75rem; -} - -.pl-12 { - padding-left: 3rem; -} - -.pt-12 { - padding-top: 3rem; -} - -.pb-56 { - padding-bottom: 14rem; -} - -.pl-6\.92 { - padding-left: 1.73rem; -} - -.pt-48 { - padding-top: 12rem; -} - -.text-left { - text-align: left; -} - -.text-center { - text-align: center; -} - -.text-right { - text-align: right; -} - -.text-start { - text-align: start; -} - -.align-baseline { - vertical-align: baseline; -} - -.align-top { - vertical-align: top; -} - -.align-middle { - vertical-align: middle; -} - -.align-bottom { - vertical-align: bottom; -} - -.font-sans { - font-family: Open Sans; -} - -.text-base { - font-size: 1rem; - line-height: 1.5rem; -} - -.text-sm { - font-size: 0.875rem; - line-height: 1.5rem; -} - -.text-xs { - font-size: 0.75rem; - line-height: 1rem; -} - -.text-lg { - font-size: 1.125rem; - line-height: 1.75rem; -} - -.text-xxs { - font-size: 0.65rem; - line-height: 1rem; -} - -.text-xl { - font-size: 1.25rem; - line-height: 1.75rem; -} - -.text-inherit { - font-size: inherit; -} - -.text-3xs { - font-size: 0.5rem; - line-height: 1rem; -} - -.text-banner-calculate { - font-size: calc(1.625rem+4.5vw); -} - -.font-normal { - font-weight: 400; -} - -.font-semibold { - font-weight: 600; -} - -.font-bold { - font-weight: 700; -} - -.uppercase { - text-transform: uppercase; -} - -.capitalize { - text-transform: capitalize; -} - -.leading-default { - line-height: 1.6; -} - -.leading-tight { - line-height: 1.25; -} - -.leading-pro { - line-height: 1.4; -} - -.leading-normal { - line-height: 1.5; -} - -.leading-5\.6 { - line-height: 1.4rem; -} - -.leading-5 { - line-height: 1.25rem; -} - -.leading-none { - line-height: 1; -} - -.leading-tighter { - line-height: 1.2; -} - -.tracking-tight-soft { - letter-spacing: -0.025rem; -} - -.tracking-normal { - letter-spacing: 0em; -} - -.tracking-none { - letter-spacing: 0; -} - -.tracking-tight { - letter-spacing: -0.025em; -} - -.text-slate-500 { - --tw-text-opacity: 1; - color: rgb(103 116 142 / var(--tw-text-opacity)); -} - -.text-slate-400 { - --tw-text-opacity: 1; - color: rgb(131 146 171 / var(--tw-text-opacity)); -} - -.text-slate-700 { - --tw-text-opacity: 1; - color: rgb(52 71 103 / var(--tw-text-opacity)); -} - -.text-gray-800 { - --tw-text-opacity: 1; - color: rgb(37 47 64 / var(--tw-text-opacity)); -} - -.text-red-500 { - --tw-text-opacity: 1; - color: rgb(245 57 57 / var(--tw-text-opacity)); -} - -.text-red-600 { - --tw-text-opacity: 1; - color: rgb(234 6 6 / var(--tw-text-opacity)); -} - -.text-lime-500 { - --tw-text-opacity: 1; - color: rgb(130 214 22 / var(--tw-text-opacity)); -} - -.text-cyan-500 { - --tw-text-opacity: 1; - color: rgb(23 193 232 / var(--tw-text-opacity)); -} - -.text-fuchsia-500 { - --tw-text-opacity: 1; - color: rgb(203 12 159 / var(--tw-text-opacity)); -} - -.text-white { - --tw-text-opacity: 1; - color: rgb(255 255 255 / var(--tw-text-opacity)); -} - -.text-transparent { - color: transparent; -} - -.text-black { - --tw-text-opacity: 1; - color: rgb(0 0 0 / var(--tw-text-opacity)); -} - -.text-gray-700 { - --tw-text-opacity: 1; - color: rgb(73 80 87 / var(--tw-text-opacity)); -} - -.text-neutral-900 { - --tw-text-opacity: 1; - color: rgb(17 17 17 / var(--tw-text-opacity)); -} - -.text-inherit { - color: inherit; -} - -.text-blue-800 { - --tw-text-opacity: 1; - color: rgb(52 78 134 / var(--tw-text-opacity)); -} - -.text-sky-600 { - --tw-text-opacity: 1; - color: rgb(62 161 236 / var(--tw-text-opacity)); -} - -.text-sky-900 { - --tw-text-opacity: 1; - color: rgb(14 69 109 / var(--tw-text-opacity)); -} - -.text-slate-800 { - --tw-text-opacity: 1; - color: rgb(58 65 111 / var(--tw-text-opacity)); -} - -.text-gray-200 { - --tw-text-opacity: 1; - color: rgb(233 236 239 / var(--tw-text-opacity)); -} - -.underline { - -webkit-text-decoration-line: underline; - text-decoration-line: underline; -} - -.antialiased { - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.opacity-50 { - opacity: 0.5; -} - -.opacity-60 { - opacity: 0.6; -} - -.opacity-100 { - opacity: 1; -} - -.opacity-80 { - opacity: 0.8; -} - -.opacity-0 { - opacity: 0; -} - -.opacity-70 { - opacity: 0.7; -} - -.shadow-none { - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.shadow-soft-xl { - --tw-shadow: 0 20px 27px 0 rgba(0,0,0,0.05); - --tw-shadow-colored: 0 20px 27px 0 var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.shadow-soft-2xl { - --tw-shadow: 0 .3125rem .625rem 0 rgba(0,0,0,.12); - --tw-shadow-colored: 0 .3125rem .625rem 0 var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.shadow-soft-md { - --tw-shadow: 0 4px 7px -1px rgba(0,0,0,.11),0 2px 4px -1px rgba(0,0,0,.07); - --tw-shadow-colored: 0 4px 7px -1px var(--tw-shadow-color), 0 2px 4px -1px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.shadow-soft-3xl { - --tw-shadow: 0 8px 26px -4px hsla(0,0%,8%,.15),0 8px 9px -5px hsla(0,0%,8%,.06); - --tw-shadow-colored: 0 8px 26px -4px var(--tw-shadow-color), 0 8px 9px -5px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.shadow-soft-lg { - --tw-shadow: 0 2px 12px 0 rgba(0,0,0,.16); - --tw-shadow-colored: 0 2px 12px 0 var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.shadow-xl { - --tw-shadow: 0 23px 45px -11px hsla(0,0%,8%,.25); - --tw-shadow-colored: 0 23px 45px -11px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.shadow-blur { - --tw-shadow: inset 0 0 1px 1px hsla(0,0%,100%,.9),0 20px 27px 0 rgba(0,0,0,.05); - --tw-shadow-colored: inset 0 0 1px 1px var(--tw-shadow-color), 0 20px 27px 0 var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.shadow-soft-sm { - --tw-shadow: 0 .25rem .375rem -.0625rem hsla(0,0%,8%,.12),0 .125rem .25rem -.0625rem hsla(0,0%,8%,.07); - --tw-shadow-colored: 0 .25rem .375rem -.0625rem var(--tw-shadow-color), 0 .125rem .25rem -.0625rem var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.shadow-soft-xxs { - --tw-shadow: 0 1px 5px 1px #ddd; - --tw-shadow-colored: 0 1px 5px 1px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.shadow-transparent { - --tw-shadow-color: transparent; - --tw-shadow: var(--tw-shadow-colored); -} - -.blur { - --tw-blur: blur(8px); - filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); -} - -.filter { - filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); -} - -.backdrop-blur-2xl { - --tw-backdrop-blur: blur(30px); - -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); - backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); -} - -.backdrop-blur-\[30px\] { - --tw-backdrop-blur: blur(30px); - -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); - backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); -} - -.backdrop-saturate-200 { - --tw-backdrop-saturate: saturate(2); - -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); - backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); -} - -.backdrop-saturate-\[200\%\] { - --tw-backdrop-saturate: saturate(200%); - -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); - backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); -} - -.transition-transform { - transition-property: transform; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; -} - -.transition-all { - transition-property: all; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; -} - -.transition-colors { - transition-property: color, background-color, border-color, fill, stroke, -webkit-text-decoration-color; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, -webkit-text-decoration-color; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; -} - -.transition { - transition-property: color, background-color, border-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-text-decoration-color, -webkit-backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-text-decoration-color, -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; -} - -.duration-200 { - transition-duration: 200ms; -} - -.duration-300 { - transition-duration: 300ms; -} - -.duration-250 { - transition-duration: 250ms; -} - -.duration-600 { - transition-duration: 600ms; -} - -.duration-500 { - transition-duration: 500ms; -} - -.duration-350 { - transition-duration: 350ms; -} - -.ease-soft { - transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1); -} - -.ease-in { - transition-timing-function: cubic-bezier(0.4, 0, 1, 1); -} - -.ease-soft-in-out { - transition-timing-function: cubic-bezier(0.42, 0, 0.58, 1); -} - -.ease-soft-in { - transition-timing-function: cubic-bezier(0.42, 0, 1, 1); -} - -.ease-bounce { - transition-timing-function: cubic-bezier(0.34, 1.61, 0.7, 1.3); -} - -.ease-soft-out { - transition-timing-function: cubic-bezier(0, 0, 0.58, 1); -} - -.transform3d { - transform: perspective(999px) rotateX(0deg) translateZ(0); -} - -.transform-dropdown { - transform: perspective(999px) rotateX(-10deg) translateZ(0) translate3d(0,37px,0); -} - -.transform-dropdown-show { - transform: perspective(999px) rotateX(0deg) translateZ(0) translate3d(0,37px,5px); -} - -.flex-wrap-inherit { - flex-wrap: inherit; -} - -.placeholder\:text-gray-500::-moz-placeholder { - --tw-text-opacity: 1; - color: rgb(173 181 189 / var(--tw-text-opacity)); -} - -.placeholder\:text-gray-500::placeholder { - --tw-text-opacity: 1; - color: rgb(173 181 189 / var(--tw-text-opacity)); -} - -.before\:visible::before { - content: var(--tw-content); - visibility: visible; -} - -.before\:absolute::before { - content: var(--tw-content); - position: absolute; -} - -.before\:right-2::before { - content: var(--tw-content); - right: 0.5rem; -} - -.before\:left-auto::before { - content: var(--tw-content); - left: auto; -} - -.before\:top-0::before { - content: var(--tw-content); - top: 0px; -} - -.before\:right-7::before { - content: var(--tw-content); - right: 1.75rem; -} - -.before\:left-4::before { - content: var(--tw-content); - left: 1rem; -} - -.before\:right-auto::before { - content: var(--tw-content); - right: auto; -} - -.before\:left-2::before { - content: var(--tw-content); - left: 0.5rem; -} - -.before\:left-7::before { - content: var(--tw-content); - left: 1.75rem; -} - -.before\:right-4::before { - content: var(--tw-content); - right: 1rem; -} - -.before\:-top-5::before { - content: var(--tw-content); - top: -1.25rem; -} - -.before\:z-50::before { - content: var(--tw-content); - z-index: 50; -} - -.before\:z-40::before { - content: var(--tw-content); - z-index: 40; -} - -.before\:float-right::before { - content: var(--tw-content); - float: right; -} - -.before\:float-left::before { - content: var(--tw-content); - float: left; -} - -.before\:inline-block::before { - content: var(--tw-content); - display: inline-block; -} - -.before\:h-2::before { - content: var(--tw-content); - height: 0.5rem; -} - -.before\:h-full::before { - content: var(--tw-content); - height: 100%; -} - -.before\:w-2::before { - content: var(--tw-content); - width: 0.5rem; -} - -.before\:rotate-45::before { - content: var(--tw-content); - --tw-rotate: 45deg; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.before\:border-r-2::before { - content: var(--tw-content); - border-right-width: 2px; -} - -.before\:border-l-2::before { - content: var(--tw-content); - border-left-width: 2px; -} - -.before\:border-r-slate-100::before { - content: var(--tw-content); - --tw-border-opacity: 1; - border-right-color: rgb(222 226 230 / var(--tw-border-opacity)); -} - -.before\:border-l-slate-100::before { - content: var(--tw-content); - --tw-border-opacity: 1; - border-left-color: rgb(222 226 230 / var(--tw-border-opacity)); -} - -.before\:bg-inherit::before { - content: var(--tw-content); - background-color: inherit; -} - -.before\:pr-2::before { - content: var(--tw-content); - padding-right: 0.5rem; -} - -.before\:pl-2::before { - content: var(--tw-content); - padding-left: 0.5rem; -} - -.before\:font-awesome::before { - content: var(--tw-content); - font-family: FontAwesome; -} - -.before\:text-5\.5::before { - content: var(--tw-content); - font-size: 1.375rem; -} - -.before\:text-5::before { - content: var(--tw-content); - font-size: 1.25rem; -} - -.before\:font-normal::before { - content: var(--tw-content); - font-weight: 400; -} - -.before\:leading-default::before { - content: var(--tw-content); - line-height: 1.6; -} - -.before\:text-gray-600::before { - content: var(--tw-content); - --tw-text-opacity: 1; - color: rgb(108 117 125 / var(--tw-text-opacity)); -} - -.before\:text-white::before { - content: var(--tw-content); - --tw-text-opacity: 1; - color: rgb(255 255 255 / var(--tw-text-opacity)); -} - -.before\:antialiased::before { - content: var(--tw-content); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.before\:transition-all::before { - content: var(--tw-content); - transition-property: all; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; -} - -.before\:duration-350::before { - content: var(--tw-content); - transition-duration: 350ms; -} - -.before\:ease-soft::before { - content: var(--tw-content); - transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1); -} - -.before\:content-\[\'\/\'\]::before { - --tw-content: '/'; - content: var(--tw-content); -} - -.before\:content-\[\'\\f0d8\'\]::before { - --tw-content: '\f0d8'; - content: var(--tw-content); -} - -.before\:content-\[\'\'\]::before { - --tw-content: ''; - content: var(--tw-content); -} - -.after\:absolute::after { - content: var(--tw-content); - position: absolute; -} - -.after\:top-0::after { - content: var(--tw-content); - top: 0px; -} - -.after\:bottom-0::after { - content: var(--tw-content); - bottom: 0px; -} - -.after\:left-0::after { - content: var(--tw-content); - left: 0px; -} - -.after\:top-px::after { - content: var(--tw-content); - top: 1px; -} - -.after\:z-10::after { - content: var(--tw-content); - z-index: 10; -} - -.after\:clear-both::after { - content: var(--tw-content); - clear: both; -} - -.after\:block::after { - content: var(--tw-content); - display: block; -} - -.after\:flex::after { - content: var(--tw-content); - display: flex; -} - -.after\:table::after { - content: var(--tw-content); - display: table; -} - -.after\:h-full::after { - content: var(--tw-content); - height: 100%; -} - -.after\:h-4::after { - content: var(--tw-content); - height: 1rem; -} - -.after\:w-full::after { - content: var(--tw-content); - width: 100%; -} - -.after\:w-4::after { - content: var(--tw-content); - width: 1rem; -} - -.after\:translate-x-px::after { - content: var(--tw-content); - --tw-translate-x: 1px; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.after\:-translate-x-px::after { - content: var(--tw-content); - --tw-translate-x: -1px; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.after\:items-center::after { - content: var(--tw-content); - align-items: center; -} - -.after\:justify-center::after { - content: var(--tw-content); - justify-content: center; -} - -.after\:rounded-2xl::after { - content: var(--tw-content); - border-radius: 1rem; -} - -.after\:rounded-circle::after { - content: var(--tw-content); - border-radius: 50%; -} - -.after\:bg-white::after { - content: var(--tw-content); - --tw-bg-opacity: 1; - background-color: rgb(255 255 255 / var(--tw-bg-opacity)); -} - -.after\:bg-gradient-to-tl::after { - content: var(--tw-content); - background-image: linear-gradient(to top left, var(--tw-gradient-stops)); -} - -.after\:from-gray-900::after { - content: var(--tw-content); - --tw-gradient-from: #141727; - --tw-gradient-to: rgb(20 23 39 / 0); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); -} - -.after\:from-blue-600::after { - content: var(--tw-content); - --tw-gradient-from: #2152ff; - --tw-gradient-to: rgb(33 82 255 / 0); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); -} - -.after\:from-red-500::after { - content: var(--tw-content); - --tw-gradient-from: #f53939; - --tw-gradient-to: rgb(245 57 57 / 0); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); -} - -.after\:from-green-600::after { - content: var(--tw-content); - --tw-gradient-from: #17ad37; - --tw-gradient-to: rgb(23 173 55 / 0); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); -} - -.after\:from-red-600::after { - content: var(--tw-content); - --tw-gradient-from: #ea0606; - --tw-gradient-to: rgb(234 6 6 / 0); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); -} - -.after\:from-slate-600::after { - content: var(--tw-content); - --tw-gradient-from: #627594; - --tw-gradient-to: rgb(98 117 148 / 0); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); -} - -.after\:from-purple-700::after { - content: var(--tw-content); - --tw-gradient-from: #7928ca; - --tw-gradient-to: rgb(121 40 202 / 0); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); -} - -.after\:to-slate-800::after { - content: var(--tw-content); - --tw-gradient-to: #3a416f; -} - -.after\:to-cyan-400::after { - content: var(--tw-content); - --tw-gradient-to: #21d4fd; -} - -.after\:to-yellow-400::after { - content: var(--tw-content); - --tw-gradient-to: #fbcf33; -} - -.after\:to-lime-400::after { - content: var(--tw-content); - --tw-gradient-to: #98ec2d; -} - -.after\:to-rose-400::after { - content: var(--tw-content); - --tw-gradient-to: #ff667c; -} - -.after\:to-slate-300::after { - content: var(--tw-content); - --tw-gradient-to: #a8b8d8; -} - -.after\:to-pink-500::after { - content: var(--tw-content); - --tw-gradient-to: #ff0080; -} - -.after\:font-awesome::after { - content: var(--tw-content); - font-family: FontAwesome; -} - -.after\:text-xxs::after { - content: var(--tw-content); - font-size: 0.65rem; - line-height: 1rem; -} - -.after\:text-white::after { - content: var(--tw-content); - --tw-text-opacity: 1; - color: rgb(255 255 255 / var(--tw-text-opacity)); -} - -.after\:opacity-65::after { - content: var(--tw-content); - opacity: 0.65; -} - -.after\:opacity-0::after { - content: var(--tw-content); - opacity: 0; -} - -.after\:opacity-85::after { - content: var(--tw-content); - opacity: 0.85; -} - -.after\:shadow-soft-2xl::after { - content: var(--tw-content); - --tw-shadow: 0 .3125rem .625rem 0 rgba(0,0,0,.12); - --tw-shadow-colored: 0 .3125rem .625rem 0 var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.after\:transition-all::after { - content: var(--tw-content); - transition-property: all; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; -} - -.after\:duration-250::after { - content: var(--tw-content); - transition-duration: 250ms; -} - -.after\:ease-soft-in-out::after { - content: var(--tw-content); - transition-timing-function: cubic-bezier(0.42, 0, 0.58, 1); -} - -.after\:content-\[\'\'\]::after { - --tw-content: ''; - content: var(--tw-content); -} - -.after\:content-\[\'\\f00c\'\]::after { - --tw-content: '\f00c'; - content: var(--tw-content); -} - -.checked\:border-0:checked { - border-width: 0px; -} - -.checked\:border-slate-800\/95:checked { - border-color: rgb(58 65 111 / 0.95); -} - -.checked\:border-transparent:checked { - border-color: transparent; -} - -.checked\:bg-slate-800\/95:checked { - background-color: rgb(58 65 111 / 0.95); -} - -.checked\:bg-transparent:checked { - background-color: transparent; -} - -.checked\:bg-none:checked { - background-image: none; -} - -.checked\:bg-gradient-to-tl:checked { - background-image: linear-gradient(to top left, var(--tw-gradient-stops)); -} - -.checked\:from-gray-900:checked { - --tw-gradient-from: #141727; - --tw-gradient-to: rgb(20 23 39 / 0); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); -} - -.checked\:to-slate-800:checked { - --tw-gradient-to: #3a416f; -} - -.checked\:bg-right:checked { - background-position: right; -} - -.checked\:after\:translate-x-5\.25:checked::after { - content: var(--tw-content); - --tw-translate-x: 1.3125rem; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.checked\:after\:translate-x-5:checked::after { - content: var(--tw-content); - --tw-translate-x: 1.25rem; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.checked\:after\:-translate-x-5\.25:checked::after { - content: var(--tw-content); - --tw-translate-x: -1.3125rem; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.checked\:after\:-translate-x-5:checked::after { - content: var(--tw-content); - --tw-translate-x: -1.25rem; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.checked\:after\:opacity-100:checked::after { - content: var(--tw-content); - opacity: 1; -} - -.hover\:z-30:hover { - z-index: 30; -} - -.hover\:scale-102:hover { - --tw-scale-x: 1.02; - --tw-scale-y: 1.02; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.hover\:border-fuchsia-500:hover { - --tw-border-opacity: 1; - border-color: rgb(203 12 159 / var(--tw-border-opacity)); -} - -.hover\:border-slate-700:hover { - --tw-border-opacity: 1; - border-color: rgb(52 71 103 / var(--tw-border-opacity)); -} - -.hover\:border-white:hover { - --tw-border-opacity: 1; - border-color: rgb(255 255 255 / var(--tw-border-opacity)); -} - -.hover\:border-white\/75:hover { - border-color: rgb(255 255 255 / 0.75); -} - -.hover\:bg-transparent:hover { - background-color: transparent; -} - -.hover\:bg-gray-200:hover { - --tw-bg-opacity: 1; - background-color: rgb(233 236 239 / var(--tw-bg-opacity)); -} - -.hover\:bg-white\/10:hover { - background-color: rgb(255 255 255 / 0.1); -} - -.hover\:bg-slate-700:hover { - --tw-bg-opacity: 1; - background-color: rgb(52 71 103 / var(--tw-bg-opacity)); -} - -.hover\:text-fuchsia-500:hover { - --tw-text-opacity: 1; - color: rgb(203 12 159 / var(--tw-text-opacity)); -} - -.hover\:text-slate-700:hover { - --tw-text-opacity: 1; - color: rgb(52 71 103 / var(--tw-text-opacity)); -} - -.hover\:text-white:hover { - --tw-text-opacity: 1; - color: rgb(255 255 255 / var(--tw-text-opacity)); -} - -.hover\:text-fuchsia-800:hover { - --tw-text-opacity: 1; - color: rgb(131 8 102 / var(--tw-text-opacity)); -} - -.hover\:opacity-75:hover { - opacity: 0.75; -} - -.hover\:shadow-soft-2xl:hover { - --tw-shadow: 0 .3125rem .625rem 0 rgba(0,0,0,.12); - --tw-shadow-colored: 0 .3125rem .625rem 0 var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.hover\:shadow-none:hover { - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.hover\:shadow-soft-xs:hover { - --tw-shadow: 0 3px 5px -1px rgba(0,0,0,.09),0 2px 3px -1px rgba(0,0,0,.07); - --tw-shadow-colored: 0 3px 5px -1px var(--tw-shadow-color), 0 2px 3px -1px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.hover\:transform3d-hover:hover { - transform: perspective(999px) rotateX(7deg) translate3d(0,-4px,5px); -} - -.focus\:border-fuchsia-300:focus { - --tw-border-opacity: 1; - border-color: rgb(226 147 211 / var(--tw-border-opacity)); -} - -.focus\:bg-white:focus { - --tw-bg-opacity: 1; - background-color: rgb(255 255 255 / var(--tw-bg-opacity)); -} - -.focus\:text-gray-700:focus { - --tw-text-opacity: 1; - color: rgb(73 80 87 / var(--tw-text-opacity)); -} - -.focus\:shadow-soft-primary-outline:focus { - --tw-shadow: 0 0 0 2px #e9aede; - --tw-shadow-colored: 0 0 0 2px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.focus\:outline-none:focus { - outline: 2px solid transparent; - outline-offset: 2px; -} - -.focus\:transition-shadow:focus { - transition-property: box-shadow; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; -} - -.active\:scale-100:active { - --tw-scale-x: 1; - --tw-scale-y: 1; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.active\:border-fuchsia-500:active { - --tw-border-opacity: 1; - border-color: rgb(203 12 159 / var(--tw-border-opacity)); -} - -.active\:border-white\/75:active { - border-color: rgb(255 255 255 / 0.75); -} - -.active\:bg-fuchsia-500:active { - --tw-bg-opacity: 1; - background-color: rgb(203 12 159 / var(--tw-bg-opacity)); -} - -.active\:bg-slate-700:active { - --tw-bg-opacity: 1; - background-color: rgb(52 71 103 / var(--tw-bg-opacity)); -} - -.active\:bg-white:active { - --tw-bg-opacity: 1; - background-color: rgb(255 255 255 / var(--tw-bg-opacity)); -} - -.active\:text-white:active { - --tw-text-opacity: 1; - color: rgb(255 255 255 / var(--tw-text-opacity)); -} - -.active\:text-black:active { - --tw-text-opacity: 1; - color: rgb(0 0 0 / var(--tw-text-opacity)); -} - -.active\:opacity-85:active { - opacity: 0.85; -} - -.active\:shadow-soft-xs:active { - --tw-shadow: 0 3px 5px -1px rgba(0,0,0,.09),0 2px 3px -1px rgba(0,0,0,.07); - --tw-shadow-colored: 0 3px 5px -1px var(--tw-shadow-color), 0 2px 3px -1px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.hover\:active\:scale-102:active:hover { - --tw-scale-x: 1.02; - --tw-scale-y: 1.02; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.active\:hover\:scale-102:hover:active { - --tw-scale-x: 1.02; - --tw-scale-y: 1.02; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.active\:hover\:border-fuchsia-500:hover:active { - --tw-border-opacity: 1; - border-color: rgb(203 12 159 / var(--tw-border-opacity)); -} - -.active\:hover\:border-white\/75:hover:active { - border-color: rgb(255 255 255 / 0.75); -} - -.active\:hover\:bg-transparent:hover:active { - background-color: transparent; -} - -.active\:hover\:bg-white\/10:hover:active { - background-color: rgb(255 255 255 / 0.1); -} - -.active\:hover\:text-fuchsia-500:hover:active { - --tw-text-opacity: 1; - color: rgb(203 12 159 / var(--tw-text-opacity)); -} - -.active\:hover\:text-slate-700:hover:active { - --tw-text-opacity: 1; - color: rgb(52 71 103 / var(--tw-text-opacity)); -} - -.active\:hover\:text-white:hover:active { - --tw-text-opacity: 1; - color: rgb(255 255 255 / var(--tw-text-opacity)); -} - -.active\:hover\:opacity-75:hover:active { - opacity: 0.75; -} - -.active\:hover\:shadow-none:hover:active { - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.group:hover .group-hover\:translate-x-1\.25 { - --tw-translate-x: 0.3125rem; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.group:hover .group-hover\:translate-x-1 { - --tw-translate-x: 0.25rem; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.group:hover .group-hover\:-translate-x-1\.25 { - --tw-translate-x: -0.3125rem; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.group:hover .group-hover\:-translate-x-1 { - --tw-translate-x: -0.25rem; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -@media (min-width: 576px) { - .sm\:my-6 { - margin-top: 1.5rem; - margin-bottom: 1.5rem; - } - - .sm\:my-auto { - margin-top: auto; - margin-bottom: auto; - } - - .sm\:mr-16 { - margin-right: 4rem; - } - - .sm\:mt-0 { - margin-top: 0px; - } - - .sm\:mr-6 { - margin-right: 1.5rem; - } - - .sm\:mr-1 { - margin-right: 0.25rem; - } - - .sm\:-mr-6 { - margin-right: -1.5rem; - } - - .sm\:ml-2 { - margin-left: 0.5rem; - } - - .sm\:mr-0 { - margin-right: 0px; - } - - .sm\:mb-0 { - margin-bottom: 0px; - } - - .sm\:inline { - display: inline; - } - - .sm\:w-1\/2 { - width: 50%; - } - - .sm\:flex-none { - flex: none; - } - - .sm\:px-6 { - padding-left: 1.5rem; - padding-right: 1.5rem; - } - - .sm\:pt-4 { - padding-top: 1rem; - } - - .before\:sm\:right-7\.5::before { - content: var(--tw-content); - right: 1.875rem; - } - - .before\:sm\:right-7::before { - content: var(--tw-content); - right: 1.75rem; - } - - .before\:sm\:left-3::before { - content: var(--tw-content); - left: 0.75rem; - } -} - -@media (min-width: 768px) { - .md\:mr-0 { - margin-right: 0px; - } - - .md\:ml-auto { - margin-left: auto; - } - - .md\:mb-0 { - margin-bottom: 0px; - } - - .md\:mt-0 { - margin-top: 0px; - } - - .md\:-mt-56 { - margin-top: -14rem; - } - - .md\:block { - display: block; - } - - .md\:w-1\/2 { - width: 50%; - } - - .md\:w-8\/12 { - width: 66.666667%; - } - - .md\:w-7\/12 { - width: 58.333333%; - } - - .md\:w-5\/12 { - width: 41.666667%; - } - - .md\:w-4\/12 { - width: 33.333333%; - } - - .md\:w-6\/12 { - width: 50%; - } - - .md\:w-1\/12 { - width: 8.333333%; - } - - .md\:w-11\/12 { - width: 91.666667%; - } - - .md\:flex-none { - flex: none; - } - - .md\:flex-0 { - flex: 0 0 auto; - } - - .md\:scale-70 { - --tw-scale-x: .7; - --tw-scale-y: .7; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); - } - - .md\:pr-4 { - padding-right: 1rem; - } -} - -@media (max-width: 768px) { - .md-max\:w-full { - width: 100%; - } -} - -@media (min-width: 992px) { - .lg\:absolute { - position: absolute; - } - - .lg\:right-0 { - right: 0px; - } - - .lg\:left-auto { - left: auto; - } - - .lg\:float-right { - float: right; - } - - .lg\:mt-2 { - margin-top: 0.5rem; - } - - .lg\:mb-0 { - margin-bottom: 0px; - } - - .lg\:mt-0 { - margin-top: 0px; - } - - .lg\:ml-0 { - margin-left: 0px; - } - - .lg\:-mt-48 { - margin-top: -12rem; - } - - .lg\:ml-12 { - margin-left: 3rem; - } - - .lg\:-mt-6 { - margin-top: -1.5rem; - } - - .lg\:block { - display: block; - } - - .lg\:flex { - display: flex; - } - - .lg\:hidden { - display: none; - } - - .lg\:w-7\/12 { - width: 58.333333%; - } - - .lg\:w-1\/2 { - width: 50%; - } - - .lg\:w-5\/12 { - width: 41.666667%; - } - - .lg\:w-2\/3 { - width: 66.666667%; - } - - .lg\:w-1\/3 { - width: 33.333333%; - } - - .lg\:w-full { - width: 100%; - } - - .lg\:w-4\/12 { - width: 33.333333%; - } - - .lg\:w-8\/12 { - width: 66.666667%; - } - - .lg\:max-w-120 { - max-width: 30rem; - } - - .lg\:flex-none { - flex: none; - } - - .lg\:flex-0 { - flex: 0 0 auto; - } - - .lg\:basis-auto { - flex-basis: auto; - } - - .lg\:cursor-pointer { - cursor: pointer; - } - - .lg\:flex-row { - flex-direction: row; - } - - .lg\:flex-nowrap { - flex-wrap: nowrap; - } - - .lg\:justify-start { - justify-content: flex-start; - } - - .lg\:justify-end { - justify-content: flex-end; - } - - .lg\:justify-between { - justify-content: space-between; - } - - .lg\:px-2 { - padding-left: 0.5rem; - padding-right: 0.5rem; - } - - .lg\:pt-0 { - padding-top: 0px; - } - - .lg\:text-left { - text-align: left; - } - - .lg\:text-right { - text-align: right; - } - - .lg\:shadow-soft-3xl { - --tw-shadow: 0 8px 26px -4px hsla(0,0%,8%,.15),0 8px 9px -5px hsla(0,0%,8%,.06); - --tw-shadow-colored: 0 8px 26px -4px var(--tw-shadow-color), 0 8px 9px -5px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); - } - - .lg\:transition-colors { - transition-property: color, background-color, border-color, fill, stroke, -webkit-text-decoration-color; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, -webkit-text-decoration-color; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; - } - - .lg\:duration-300 { - transition-duration: 300ms; - } - - .lg\:ease-soft { - transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1); - } - - .before\:lg\:-ml-px::before { - content: var(--tw-content); - margin-left: -1px; - } - - .before\:lg\:-mr-px::before { - content: var(--tw-content); - margin-right: -1px; - } - - .lg\:hover\:text-white\/75:hover { - color: rgb(255 255 255 / 0.75); - } -} - -@media (max-width: 992px) { - .lg-max\:mt-6 { - margin-top: 1.5rem; - } - - .lg-max\:max-h-0 { - max-height: 0px; - } - - .lg-max\:max-h-54 { - max-height: 13.5rem; - } - - .lg-max\:overflow-hidden { - overflow: hidden; - } - - .lg-max\:bg-white { - --tw-bg-opacity: 1; - background-color: rgb(255 255 255 / var(--tw-bg-opacity)); - } - - .lg-max\:text-slate-700 { - --tw-text-opacity: 1; - color: rgb(52 71 103 / var(--tw-text-opacity)); - } - - .lg-max\:opacity-0 { - opacity: 0; - } -} - -@media (min-width: 1200px) { - .xl\:left-0 { - left: 0px; - } - - .xl\:right-0 { - right: 0px; - } - - .xl\:left-\[18\%\] { - left: 18%; - } - - .xl\:ml-68\.5 { - margin-left: 17.125rem; - } - - .xl\:ml-68 { - margin-left: 17rem; - } - - .xl\:mb-0 { - margin-bottom: 0px; - } - - .xl\:mr-68\.5 { - margin-right: 17.125rem; - } - - .xl\:mr-68 { - margin-right: 17rem; - } - - .xl\:ml-auto { - margin-left: auto; - } - - .xl\:mr-12 { - margin-right: 3rem; - } - - .xl\:ml-4 { - margin-left: 1rem; - } - - .xl\:hidden { - display: none; - } - - .xl\:w-1\/4 { - width: 25%; - } - - .xl\:w-1\/2 { - width: 50%; - } - - .xl\:w-4\/12 { - width: 33.333333%; - } - - .xl\:w-3\/12 { - width: 25%; - } - - .xl\:flex-none { - flex: none; - } - - .xl\:translate-x-0 { - --tw-translate-x: 0px; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); - } - - .xl\:scale-60 { - --tw-scale-x: .6; - --tw-scale-y: .6; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); - } - - @-webkit-keyframes fade-up { - from { - opacity: 0; - transform: translateY(100%); - } - - to { - opacity: 1; - } - } - - @keyframes fade-up { - from { - opacity: 0; - transform: translateY(100%); - } - - to { - opacity: 1; - } - } - - .xl\:animate-fade-up { - -webkit-animation: fade-up 1.5s both; - animation: fade-up 1.5s both; - } - - .xl\:bg-transparent { - background-color: transparent; - } - - .xl\:bg-white { - --tw-bg-opacity: 1; - background-color: rgb(255 255 255 / var(--tw-bg-opacity)); - } - - .xl\:p-2\.5 { - padding: 0.625rem; - } - - .xl\:p-2 { - padding: 0.5rem; - } - - .xl\:px-12 { - padding-left: 3rem; - padding-right: 3rem; - } - - .xl\:text-8xl { - font-size: 5rem; - line-height: 1; - } -} - -@media (max-width: 1200px) { - .xl-max\:pointer-events-none { - pointer-events: none; - } - - .xl-max\:cursor-not-allowed { - cursor: not-allowed; - } - - .xl-max\:border-0 { - border-width: 0px; - } - - .xl-max\:bg-gradient-to-tl { - background-image: linear-gradient(to top left, var(--tw-gradient-stops)); - } - - .xl-max\:from-purple-700 { - --tw-gradient-from: #7928ca; - --tw-gradient-to: rgb(121 40 202 / 0); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); - } - - .xl-max\:to-pink-500 { - --tw-gradient-to: #ff0080; - } - - .xl-max\:text-white { - --tw-text-opacity: 1; - color: rgb(255 255 255 / var(--tw-text-opacity)); - } - - .xl-max\:opacity-65 { - opacity: 0.65; - } -} diff --git a/src/main/resources/static/css/soft-ui-dashboard-tailwind.min.css b/src/main/resources/static/css/soft-ui-dashboard-tailwind.min.css deleted file mode 100644 index 1a929fd..0000000 --- a/src/main/resources/static/css/soft-ui-dashboard-tailwind.min.css +++ /dev/null @@ -1,28 +0,0 @@ -/*! - -========================================================= -* Soft UI Dashboard Tailwind - v1.0.4 -========================================================= - -* Product Page: https://www.creative-tim.com/product/soft-ui-dashboard-tailwind -* Copyright 2022 Creative Tim (https://www.creative-tim.com) -* Licensed under MIT (site.license) - -* Coded by www.creative-tim.com - -========================================================= - -* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -*/ - -/*! tailwindcss v3.1.6 | MIT License | https://tailwindcss.com - -*/ - -/* -1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) -2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) -*/ - -*, ::before, ::after {box-sizing: border-box;border-width: 0;border-style: solid;border-color: #e9ecef;}::before, ::after {--tw-content: '';}html {line-height: 1.5;-webkit-text-size-adjust: 100%;-moz-tab-size: 4;-o-tab-size: 4;tab-size: 4;font-family: Open Sans;}body {margin: 0;line-height: inherit;}hr {height: 0;color: inherit;border-top-width: 1px;}abbr:where([title]) {-webkit-text-decoration: underline dotted;text-decoration: underline dotted;}h1, h2, h3, h4, h5, h6 {font-size: inherit;font-weight: inherit;}a {color: inherit;text-decoration: inherit;}b, strong {font-weight: bolder;}code, kbd, samp, pre {font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;font-size: 1em;}small {font-size: 80%;}sub, sup {font-size: 75%;line-height: 0;position: relative;vertical-align: baseline;}sub {bottom: -0.25em;}sup {top: -0.5em;}table {text-indent: 0;border-color: inherit;border-collapse: collapse;}button, input, optgroup, select, textarea {font-family: inherit;font-size: 100%;font-weight: inherit;line-height: inherit;color: inherit;margin: 0;padding: 0;}button, select {text-transform: none;}button, [type='button'], [type='reset'], [type='submit'] {-webkit-appearance: button;background-color: transparent;background-image: none;}:-moz-focusring {outline: auto;}:-moz-ui-invalid {box-shadow: none;}progress {vertical-align: baseline;}::-webkit-inner-spin-button, ::-webkit-outer-spin-button {height: auto;}[type='search'] {-webkit-appearance: textfield;outline-offset: -2px;}::-webkit-search-decoration {-webkit-appearance: none;}::-webkit-file-upload-button {-webkit-appearance: button;font: inherit;}summary {display: list-item;}blockquote, dl, dd, h1, h2, h3, h4, h5, h6, hr, figure, p, pre {margin: 0;}fieldset {margin: 0;padding: 0;}legend {padding: 0;}ol, ul, menu {list-style: none;margin: 0;padding: 0;}textarea {resize: vertical;}input::-moz-placeholder, textarea::-moz-placeholder {opacity: 1;color: #ced4da;}input::placeholder, textarea::placeholder {opacity: 1;color: #ced4da;}button, [role="button"] {cursor: pointer;}:disabled {cursor: default;}img, svg, video, canvas, audio, iframe, embed, object {display: block;vertical-align: middle;}img, video {max-width: 100%;height: auto;}*, ::before, ::after {--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / 0.5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;}::-webkit-backdrop {--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / 0.5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;}::backdrop {--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / 0.5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;}.container {width: 100%;margin-right: auto;margin-left: auto;padding-right: 1.5rem;padding-left: 1.5rem;}@media (min-width: 576px) {.container {max-width: 576px;}}@media (min-width: 768px) {.container {max-width: 768px;}}@media (min-width: 992px) {.container {max-width: 992px;}}@media (min-width: 1200px) {.container {max-width: 1200px;}}@media (min-width: 1320px) {.container {max-width: 1320px;}}a {letter-spacing: -0.025rem;}hr {margin: 1rem 0;border: 0;opacity: .25;}img {max-width: none;}label {display: inline-block;}p {line-height: 1.625;font-weight: 400;margin-bottom: 1rem;}small {font-size: .875em;}svg {display: inline;}table {border-collapse: inherit;}h1, h2, h3, h4, h5, h6 {margin-bottom: .5rem;color: #344767;}h1, h2, h3, h4 {letter-spacing: -0.05rem;}h1, h2, h3 {font-weight: 700;}h4, h5, h6 {font-weight: 600;}h1 {font-size: 3rem;line-height: 1.25;}h2 {font-size: 2.25rem;line-height: 1.3;}h3 {font-size: 1.875rem;line-height: 1.375;}h4 {font-size: 1.5rem;line-height: 1.375;}h5 {font-size: 1.25rem;line-height: 1.375;}h6 {font-size: 1rem;line-height: 1.625;}.pointer-events-none {pointer-events: none;}.visible {visibility: visible;}.invisible {visibility: hidden;}.fixed {position: fixed;}.absolute {position: absolute;}.relative {position: relative;}.sticky {position: -webkit-sticky;position: sticky;}.inset-y-0 {top: 0px;bottom: 0px;}.inset-x-0 {left: 0px;right: 0px;}.top-0 {top: 0px;}.right-0 {right: 0px;}.top-3\.5 {top: 0.875rem;}.top-3 {top: 0.75rem;}.left-0 {left: 0px;}.left-4 {left: 1rem;}.-top-1\.5 {top: -0.375rem;}.-top-1 {top: -0.25rem;}.bottom-7\.5 {bottom: 1.875rem;}.right-7\.5 {right: 1.875rem;}.bottom-7 {bottom: 1.75rem;}.right-7 {right: 1.75rem;}.-right-90 {right: -22.5rem;}.left-auto {left: auto;}.bottom-0 {bottom: 0px;}.top-auto {top: auto;}.top-31\/100 {top: 31%;}.right-4 {right: 1rem;}.left-7\.5 {left: 1.875rem;}.right-auto {right: auto;}.left-7 {left: 1.75rem;}.-left-90 {left: -22.5rem;}.-right-40 {right: -10rem;}.top-\[1\%\] {top: 1%;}.z-990 {z-index: 990;}.z-20 {z-index: 20;}.z-10 {z-index: 10;}.z-50 {z-index: 50;}.z-100 {z-index: 100;}.z-sticky {z-index: 1020;}.z-30 {z-index: 30;}.z-0 {z-index: 0;}.z-110 {z-index: 110;}.float-right {float: right;}.float-left {float: left;}.clear-both {clear: both;}.m-0 {margin: 0px;}.m-4 {margin: 1rem;}.my-4 {margin-top: 1rem;margin-bottom: 1rem;}.my-0 {margin-top: 0px;margin-bottom: 0px;}.mx-4 {margin-left: 1rem;margin-right: 1rem;}.mx-6 {margin-left: 1.5rem;margin-right: 1.5rem;}.mx-auto {margin-left: auto;margin-right: auto;}.my-auto {margin-top: auto;margin-bottom: auto;}.-mx-3 {margin-left: -0.75rem;margin-right: -0.75rem;}.my-6 {margin-top: 1.5rem;margin-bottom: 1.5rem;}.mx-0 {margin-left: 0px;margin-right: 0px;}.my-1 {margin-top: 0.25rem;margin-bottom: 0.25rem;}.my-2 {margin-top: 0.5rem;margin-bottom: 0.5rem;}.my-56 {margin-top: 14rem;margin-bottom: 14rem;}.mx-2 {margin-left: 0.5rem;margin-right: 0.5rem;}.ml-4 {margin-left: 1rem;}.ml-1 {margin-left: 0.25rem;}.mt-0 {margin-top: 0px;}.mb-0 {margin-bottom: 0px;}.mt-0\.5 {margin-top: 0.125rem;}.mr-2 {margin-right: 0.5rem;}.mt-4 {margin-top: 1rem;}.ml-2 {margin-left: 0.5rem;}.mb-7\.5 {margin-bottom: 1.875rem;}.mb-7 {margin-bottom: 1.75rem;}.mb-4 {margin-bottom: 1rem;}.mr-12 {margin-right: 3rem;}.mt-2 {margin-top: 0.5rem;}.-ml-px {margin-left: -1px;}.mr-4 {margin-right: 1rem;}.mb-0\.75 {margin-bottom: 0.1875rem;}.mb-2 {margin-bottom: 0.5rem;}.mb-1 {margin-bottom: 0.25rem;}.mr-1 {margin-right: 0.25rem;}.mb-6 {margin-bottom: 1.5rem;}.mt-6 {margin-top: 1.5rem;}.mb-12 {margin-bottom: 3rem;}.mt-auto {margin-top: auto;}.mt-12 {margin-top: 3rem;}.ml-auto {margin-left: auto;}.mt-1 {margin-top: 0.25rem;}.-mt-0\.38 {margin-top: -0.095rem;}.-mt-0 {margin-top: -0px;}.-ml-34 {margin-left: -8.5rem;}.-ml-4 {margin-left: -1rem;}.ml-11\.252 {margin-left: 2.813rem;}.ml-11 {margin-left: 2.75rem;}.mr-1\.25 {margin-right: 0.3125rem;}.mb-0\.5 {margin-bottom: 0.125rem;}.mr-6 {margin-right: 1.5rem;}.ml-6 {margin-left: 1.5rem;}.-mt-16 {margin-top: -4rem;}.mt-0\.54 {margin-top: 0.135rem;}.-mr-px {margin-right: -1px;}.ml-0 {margin-left: 0px;}.mr-auto {margin-right: auto;}.-mr-34 {margin-right: -8.5rem;}.-mr-4 {margin-right: -1rem;}.mr-11\.252 {margin-right: 2.813rem;}.mr-11 {margin-right: 2.75rem;}.mt-1\.75 {margin-top: 0.4375rem;}.mt-32 {margin-top: 8rem;}.-ml-12 {margin-left: -3rem;}.-mr-32 {margin-right: -8rem;}.-ml-16 {margin-left: -4rem;}.mb-32 {margin-bottom: 8rem;}.-mt-48 {margin-top: -12rem;}.-ml-6\.92 {margin-left: -1.73rem;}.-ml-6 {margin-left: -1.5rem;}.-mt-6 {margin-top: -1.5rem;}.-mt-2 {margin-top: -0.5rem;}.mt-0\.75 {margin-top: 0.1875rem;}.block {display: block;}.inline-block {display: inline-block;}.inline {display: inline;}.flex {display: flex;}.inline-flex {display: inline-flex;}.table {display: table;}.grid {display: grid;}.hidden {display: none;}.h-19\.5 {height: 4.875rem;}.h-full {height: 100%;}.h-px {height: 1px;}.h-sidenav {height: calc(100vh - 370px);}.h-8 {height: 2rem;}.h-0\.5 {height: 0.125rem;}.h-0 {height: 0px;}.h-9 {height: 2.25rem;}.h-12 {height: 3rem;}.h-5 {height: 1.25rem;}.h-0\.75 {height: 0.1875rem;}.h-1\.5 {height: 0.375rem;}.h-1 {height: 0.25rem;}.h-6 {height: 1.5rem;}.h-2 {height: 0.5rem;}.h-6\.5 {height: 1.625rem;}.h-5\.75 {height: 1.4375rem;}.h-\[80vh\] {height: 80vh;}.h-16 {height: 4rem;}.h-6\.35 {height: 1.5875rem;}.h-18\.5 {height: 4.625rem;}.h-4\.92 {height: 1.23rem;}.h-4 {height: 1rem;}.max-h-8 {max-height: 2rem;}.max-h-screen {max-height: 100vh;}.min-h-6 {min-height: 1.5rem;}.min-h-75 {min-height: 18.75rem;}.min-h-75-screen {min-height: 75vh;}.min-h-screen {min-height: 100vh;}.min-h-50-screen {min-height: 50vh;}.min-h-85-screen {min-height: 85vh;}.w-full {width: 100%;}.w-auto {width: auto;}.w-8 {width: 2rem;}.w-1\/100 {width: 1%;}.w-4\.5 {width: 1.125rem;}.w-4 {width: 1rem;}.w-9 {width: 2.25rem;}.w-2\/3 {width: 66.666667%;}.w-12 {width: 3rem;}.w-1\/2 {width: 50%;}.w-1\/4 {width: 25%;}.w-5 {width: 1.25rem;}.w-3\/4 {width: 75%;}.w-3\/5 {width: 60%;}.w-9\/10 {width: 90%;}.w-3\/10 {width: 30%;}.w-7\/12 {width: 58.333333%;}.w-5\/12 {width: 41.666667%;}.w-6 {width: 1.5rem;}.w-2 {width: 0.5rem;}.w-30 {width: 7.5rem;}.w-1\/10 {width: 10%;}.w-2\/5 {width: 40%;}.w-6\.5 {width: 1.625rem;}.w-90 {width: 22.5rem;}.w-5\.75 {width: 1.4375rem;}.w-10 {width: 2.5rem;}.w-1\/5 {width: 20%;}.w-16 {width: 4rem;}.w-6\.35 {width: 1.5875rem;}.w-18\.5 {width: 4.625rem;}.w-4\/5 {width: 80%;}.w-5\.5 {width: 1.375rem;}.w-8\/12 {width: 66.666667%;}.w-3\/12 {width: 25%;}.w-4\.92 {width: 1.23rem;}.w-0 {width: 0px;}.min-w-0 {min-width: 0px;}.min-w-44 {min-width: 11rem;}.max-w-62\.5 {max-width: 15.625rem;}.max-w-full {max-width: 100%;}.max-w-none {max-width: none;}.max-w-screen-2xl {max-width: 1320px;}.flex-auto {flex: 1 1 auto;}.flex-none {flex: none;}.flex-0 {flex: 0 0 auto;}.flex-shrink-0 {flex-shrink: 0;}.shrink-0 {flex-shrink: 0;}.flex-grow {flex-grow: 1;}.grow {flex-grow: 1;}.basis-full {flex-basis: 100%;}.basis-1\/3 {flex-basis: 33.333333%;}.origin-top {transform-origin: top;}.origin-10-10 {transform-origin: 10% 10%;}.origin-10-90 {transform-origin: 10% 90%;}.-translate-x-full {--tw-translate-x: -100%;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.-translate-x-1\/2 {--tw-translate-x: -50%;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.translate-x-full {--tw-translate-x: 100%;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.translate-x-1\/2 {--tw-translate-x: 50%;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.translate-x-0 {--tw-translate-x: 0px;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.-translate-x-\[5px\] {--tw-translate-x: -5px;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.translate-x-\[5px\] {--tw-translate-x: 5px;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.rotate-45 {--tw-rotate: 45deg;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.-rotate-45 {--tw-rotate: -45deg;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.-skew-x-10 {--tw-skew-x: -10deg;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.skew-x-10 {--tw-skew-x: 10deg;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.transform {transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.cursor-pointer {cursor: pointer;}.select-none {-webkit-user-select: none;-moz-user-select: none;user-select: none;}.resize {resize: both;}.list-none {list-style-type: none;}.appearance-none {-webkit-appearance: none;-moz-appearance: none;appearance: none;}.flex-row {flex-direction: row;}.flex-col {flex-direction: column;}.flex-wrap {flex-wrap: wrap;}.items-start {align-items: flex-start;}.items-end {align-items: flex-end;}.items-center {align-items: center;}.items-stretch {align-items: stretch;}.justify-end {justify-content: flex-end;}.justify-center {justify-content: center;}.justify-between {justify-content: space-between;}.overflow-auto {overflow: auto;}.overflow-hidden {overflow: hidden;}.overflow-visible {overflow: visible;}.overflow-x-auto {overflow-x: auto;}.overflow-y-auto {overflow-y: auto;}.text-ellipsis {text-overflow: ellipsis;}.whitespace-nowrap {white-space: nowrap;}.break-words {overflow-wrap: break-word;}.rounded-2xl {border-radius: 1rem;}.rounded-lg {border-radius: 0.5rem;}.rounded-xl {border-radius: 0.75rem;}.rounded-sm {border-radius: 0.125rem;}.rounded {border-radius: 0.25rem;}.rounded-full {border-radius: 9999px;}.rounded-circle {border-radius: 50%;}.rounded-none {border-radius: 0px;}.rounded-10 {border-radius: 2.5rem;}.rounded-3\.5xl {border-radius: 1.875rem;}.rounded-3 {border-radius: 0.75rem;}.rounded-blur {border-radius: 40px;}.rounded-xs {border-radius: 0.0625rem;}.rounded-1\.4 {border-radius: 0.35rem;}.rounded-1 {border-radius: 0.25rem;}.rounded-1\.8 {border-radius: 0.45rem;}.rounded-t-2xl {border-top-left-radius: 1rem;border-top-right-radius: 1rem;}.rounded-t-inherit {border-top-left-radius: inherit;border-top-right-radius: inherit;}.rounded-b-inherit {border-bottom-right-radius: inherit;border-bottom-left-radius: inherit;}.rounded-t-lg {border-top-left-radius: 0.5rem;border-top-right-radius: 0.5rem;}.rounded-b-lg {border-bottom-right-radius: 0.5rem;border-bottom-left-radius: 0.5rem;}.rounded-b-2xl {border-bottom-right-radius: 1rem;border-bottom-left-radius: 1rem;}.rounded-tr-none {border-top-right-radius: 0px;}.rounded-br-none {border-bottom-right-radius: 0px;}.rounded-tl-none {border-top-left-radius: 0px;}.rounded-bl-none {border-bottom-left-radius: 0px;}.rounded-bl-xl {border-bottom-left-radius: 0.75rem;}.border-0 {border-width: 0px;}.border {border-width: 1px;}.border-2 {border-width: 2px;}.border-r-0 {border-right-width: 0px;}.border-b-0 {border-bottom-width: 0px;}.border-b {border-bottom-width: 1px;}.border-t-0 {border-top-width: 0px;}.border-l-0 {border-left-width: 0px;}.border-solid {border-style: solid;}.border-blue-900 {--tw-border-opacity: 1;border-color: rgb(0 0 125 / var(--tw-border-opacity));}.border-white {--tw-border-opacity: 1;border-color: rgb(255 255 255 / var(--tw-border-opacity));}.border-transparent {border-color: transparent;}.border-gray-300 {--tw-border-opacity: 1;border-color: rgb(210 214 218 / var(--tw-border-opacity));}.border-fuchsia-500 {--tw-border-opacity: 1;border-color: rgb(203 12 159 / var(--tw-border-opacity));}.border-black\/12\.5 {border-color: rgb(0 0 0 / 0.125);}.border-gray-200 {--tw-border-opacity: 1;border-color: rgb(233 236 239 / var(--tw-border-opacity));}.border-slate-700 {--tw-border-opacity: 1;border-color: rgb(52 71 103 / var(--tw-border-opacity));}.border-slate-100 {--tw-border-opacity: 1;border-color: rgb(222 226 230 / var(--tw-border-opacity));}.border-red-600 {--tw-border-opacity: 1;border-color: rgb(234 6 6 / var(--tw-border-opacity));}.border-lime-500 {--tw-border-opacity: 1;border-color: rgb(130 214 22 / var(--tw-border-opacity));}.border-white\/75 {border-color: rgb(255 255 255 / 0.75);}.border-slate-200 {--tw-border-opacity: 1;border-color: rgb(203 211 218 / var(--tw-border-opacity));}.border-b-gray-200 {--tw-border-opacity: 1;border-bottom-color: rgb(233 236 239 / var(--tw-border-opacity));}.border-b-transparent {border-bottom-color: transparent;}.bg-gray-50 {--tw-bg-opacity: 1;background-color: rgb(248 249 250 / var(--tw-bg-opacity));}.bg-white {--tw-bg-opacity: 1;background-color: rgb(255 255 255 / var(--tw-bg-opacity));}.bg-transparent {background-color: transparent;}.bg-slate-500 {--tw-bg-opacity: 1;background-color: rgb(103 116 142 / var(--tw-bg-opacity));}.bg-gray-200 {--tw-bg-opacity: 1;background-color: rgb(233 236 239 / var(--tw-bg-opacity));}.bg-slate-700 {--tw-bg-opacity: 1;background-color: rgb(52 71 103 / var(--tw-bg-opacity));}.bg-black {--tw-bg-opacity: 1;background-color: rgb(0 0 0 / var(--tw-bg-opacity));}.bg-inherit {background-color: inherit;}.bg-fuchsia-500 {--tw-bg-opacity: 1;background-color: rgb(203 12 159 / var(--tw-bg-opacity));}.bg-slate-800\/10 {background-color: rgb(58 65 111 / 0.1);}.bg-white\/10 {background-color: rgb(255 255 255 / 0.1);}.bg-white\/80 {background-color: rgb(255 255 255 / 0.8);}.bg-gray-600 {--tw-bg-opacity: 1;background-color: rgb(108 117 125 / var(--tw-bg-opacity));}.bg-\[hsla\(0\2c 0\%\2c 100\%\2c 0\.8\)\] {background-color: hsla(0,0%,100%,0.8);}.bg-gradient-to-r {background-image: linear-gradient(to right, var(--tw-gradient-stops));}.bg-gradient-to-tl {background-image: linear-gradient(to top left, var(--tw-gradient-stops));}.bg-none {background-image: none;}.from-transparent {--tw-gradient-from: transparent;--tw-gradient-to: rgb(0 0 0 / 0);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);}.from-purple-700 {--tw-gradient-from: #7928ca;--tw-gradient-to: rgb(121 40 202 / 0);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);}.from-slate-600 {--tw-gradient-from: #627594;--tw-gradient-to: rgb(98 117 148 / 0);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);}.from-gray-900 {--tw-gradient-from: #141727;--tw-gradient-to: rgb(20 23 39 / 0);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);}.from-blue-600 {--tw-gradient-from: #2152ff;--tw-gradient-to: rgb(33 82 255 / 0);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);}.from-red-500 {--tw-gradient-from: #f53939;--tw-gradient-to: rgb(245 57 57 / 0);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);}.from-red-600 {--tw-gradient-from: #ea0606;--tw-gradient-to: rgb(234 6 6 / 0);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);}.from-green-600 {--tw-gradient-from: #17ad37;--tw-gradient-to: rgb(23 173 55 / 0);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);}.from-gray-400 {--tw-gradient-from: #ced4da;--tw-gradient-to: rgb(206 212 218 / 0);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);}.via-black\/40 {--tw-gradient-to: rgb(0 0 0 / 0);--tw-gradient-stops: var(--tw-gradient-from), rgb(0 0 0 / 0.4), var(--tw-gradient-to);}.via-white {--tw-gradient-to: rgb(255 255 255 / 0);--tw-gradient-stops: var(--tw-gradient-from), #fff, var(--tw-gradient-to);}.to-transparent {--tw-gradient-to: transparent;}.to-pink-500 {--tw-gradient-to: #ff0080;}.to-slate-300 {--tw-gradient-to: #a8b8d8;}.to-slate-800 {--tw-gradient-to: #3a416f;}.to-cyan-400 {--tw-gradient-to: #21d4fd;}.to-yellow-400 {--tw-gradient-to: #fbcf33;}.to-rose-400 {--tw-gradient-to: #ff667c;}.to-lime-400 {--tw-gradient-to: #98ec2d;}.to-gray-100 {--tw-gradient-to: #ebeff4;}.bg-cover {background-size: cover;}.bg-150 {background-size: 150%;}.bg-contain {background-size: contain;}.bg-clip-border {background-clip: border-box;}.bg-clip-padding {background-clip: padding-box;}.bg-clip-text {-webkit-background-clip: text;background-clip: text;}.bg-center {background-position: center;}.bg-x-25 {background-position: 25% 0;}.bg-left {background-position: left;}.bg-right {background-position: right;}.bg-no-repeat {background-repeat: no-repeat;}.fill-slate-800 {fill: #3a416f;}.fill-current {fill: currentColor;}.fill-transparent {fill: transparent;}.stroke-0 {stroke-width: 0;}.p-0 {padding: 0px;}.p-4 {padding: 1rem;}.p-6 {padding: 1.5rem;}.p-2 {padding: 0.5rem;}.p-1\.2 {padding: 0.3rem;}.p-1 {padding: 0.25rem;}.px-8 {padding-left: 2rem;padding-right: 2rem;}.py-6 {padding-top: 1.5rem;padding-bottom: 1.5rem;}.py-2\.7 {padding-top: 0.675rem;padding-bottom: 0.675rem;}.px-4 {padding-left: 1rem;padding-right: 1rem;}.py-2 {padding-top: 0.5rem;padding-bottom: 0.5rem;}.px-6 {padding-left: 1.5rem;padding-right: 1.5rem;}.py-3 {padding-top: 0.75rem;padding-bottom: 0.75rem;}.px-0 {padding-left: 0px;padding-right: 0px;}.py-1 {padding-top: 0.25rem;padding-bottom: 0.25rem;}.px-2\.5 {padding-left: 0.625rem;padding-right: 0.625rem;}.px-2 {padding-left: 0.5rem;padding-right: 0.5rem;}.py-4 {padding-top: 1rem;padding-bottom: 1rem;}.py-1\.2 {padding-top: 0.3rem;padding-bottom: 0.3rem;}.px-3 {padding-left: 0.75rem;padding-right: 0.75rem;}.px-16 {padding-left: 4rem;padding-right: 4rem;}.py-3\.5 {padding-top: 0.875rem;padding-bottom: 0.875rem;}.py-0 {padding-top: 0px;padding-bottom: 0px;}.px-1 {padding-left: 0.25rem;padding-right: 0.25rem;}.py-2\.375 {padding-top: .59375rem;padding-bottom: .59375rem;}.py-12 {padding-top: 3rem;padding-bottom: 3rem;}.pl-0 {padding-left: 0px;}.pl-6 {padding-left: 1.5rem;}.pt-1 {padding-top: 0.25rem;}.pl-2 {padding-left: 0.5rem;}.pl-8\.75 {padding-left: 2.1875rem;}.pr-3 {padding-right: 0.75rem;}.pl-8 {padding-left: 2rem;}.pl-4 {padding-left: 1rem;}.pr-2 {padding-right: 0.5rem;}.pt-2 {padding-top: 0.5rem;}.pt-6 {padding-top: 1.5rem;}.pr-1 {padding-right: 0.25rem;}.pb-0 {padding-bottom: 0px;}.pr-6 {padding-right: 1.5rem;}.pb-2 {padding-bottom: 0.5rem;}.pt-1\.4 {padding-top: 0.35rem;}.pt-4 {padding-top: 1rem;}.pt-0 {padding-top: 0px;}.pb-1 {padding-bottom: 0.25rem;}.pr-0 {padding-right: 0px;}.pr-4 {padding-right: 1rem;}.pl-1 {padding-left: 0.25rem;}.pr-8\.75 {padding-right: 2.1875rem;}.pr-8 {padding-right: 2rem;}.pr-10 {padding-right: 2.5rem;}.pl-3 {padding-left: 0.75rem;}.pl-12 {padding-left: 3rem;}.pt-12 {padding-top: 3rem;}.pb-56 {padding-bottom: 14rem;}.pl-6\.92 {padding-left: 1.73rem;}.pt-48 {padding-top: 12rem;}.text-left {text-align: left;}.text-center {text-align: center;}.text-right {text-align: right;}.text-start {text-align: start;}.align-baseline {vertical-align: baseline;}.align-top {vertical-align: top;}.align-middle {vertical-align: middle;}.align-bottom {vertical-align: bottom;}.font-sans {font-family: Open Sans;}.text-base {font-size: 1rem;line-height: 1.5rem;}.text-sm {font-size: 0.875rem;line-height: 1.5rem;}.text-xs {font-size: 0.75rem;line-height: 1rem;}.text-lg {font-size: 1.125rem;line-height: 1.75rem;}.text-xxs {font-size: 0.65rem;line-height: 1rem;}.text-xl {font-size: 1.25rem;line-height: 1.75rem;}.text-inherit {font-size: inherit;}.text-3xs {font-size: 0.5rem;line-height: 1rem;}.text-banner-calculate {font-size: calc(1.625rem+4.5vw);}.font-normal {font-weight: 400;}.font-semibold {font-weight: 600;}.font-bold {font-weight: 700;}.uppercase {text-transform: uppercase;}.capitalize {text-transform: capitalize;}.leading-default {line-height: 1.6;}.leading-tight {line-height: 1.25;}.leading-pro {line-height: 1.4;}.leading-normal {line-height: 1.5;}.leading-5\.6 {line-height: 1.4rem;}.leading-5 {line-height: 1.25rem;}.leading-none {line-height: 1;}.leading-tighter {line-height: 1.2;}.tracking-tight-soft {letter-spacing: -0.025rem;}.tracking-normal {letter-spacing: 0em;}.tracking-none {letter-spacing: 0;}.tracking-tight {letter-spacing: -0.025em;}.text-slate-500 {--tw-text-opacity: 1;color: rgb(103 116 142 / var(--tw-text-opacity));}.text-slate-400 {--tw-text-opacity: 1;color: rgb(131 146 171 / var(--tw-text-opacity));}.text-slate-700 {--tw-text-opacity: 1;color: rgb(52 71 103 / var(--tw-text-opacity));}.text-gray-800 {--tw-text-opacity: 1;color: rgb(37 47 64 / var(--tw-text-opacity));}.text-red-500 {--tw-text-opacity: 1;color: rgb(245 57 57 / var(--tw-text-opacity));}.text-red-600 {--tw-text-opacity: 1;color: rgb(234 6 6 / var(--tw-text-opacity));}.text-lime-500 {--tw-text-opacity: 1;color: rgb(130 214 22 / var(--tw-text-opacity));}.text-cyan-500 {--tw-text-opacity: 1;color: rgb(23 193 232 / var(--tw-text-opacity));}.text-fuchsia-500 {--tw-text-opacity: 1;color: rgb(203 12 159 / var(--tw-text-opacity));}.text-white {--tw-text-opacity: 1;color: rgb(255 255 255 / var(--tw-text-opacity));}.text-transparent {color: transparent;}.text-black {--tw-text-opacity: 1;color: rgb(0 0 0 / var(--tw-text-opacity));}.text-gray-700 {--tw-text-opacity: 1;color: rgb(73 80 87 / var(--tw-text-opacity));}.text-neutral-900 {--tw-text-opacity: 1;color: rgb(17 17 17 / var(--tw-text-opacity));}.text-inherit {color: inherit;}.text-blue-800 {--tw-text-opacity: 1;color: rgb(52 78 134 / var(--tw-text-opacity));}.text-sky-600 {--tw-text-opacity: 1;color: rgb(62 161 236 / var(--tw-text-opacity));}.text-sky-900 {--tw-text-opacity: 1;color: rgb(14 69 109 / var(--tw-text-opacity));}.text-slate-800 {--tw-text-opacity: 1;color: rgb(58 65 111 / var(--tw-text-opacity));}.text-gray-200 {--tw-text-opacity: 1;color: rgb(233 236 239 / var(--tw-text-opacity));}.underline {-webkit-text-decoration-line: underline;text-decoration-line: underline;}.antialiased {-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;}.opacity-50 {opacity: 0.5;}.opacity-60 {opacity: 0.6;}.opacity-100 {opacity: 1;}.opacity-80 {opacity: 0.8;}.opacity-0 {opacity: 0;}.opacity-70 {opacity: 0.7;}.shadow-none {--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);}.shadow-soft-xl {--tw-shadow: 0 20px 27px 0 rgba(0,0,0,0.05);--tw-shadow-colored: 0 20px 27px 0 var(--tw-shadow-color);box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);}.shadow-soft-2xl {--tw-shadow: 0 .3125rem .625rem 0 rgba(0,0,0,.12);--tw-shadow-colored: 0 .3125rem .625rem 0 var(--tw-shadow-color);box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);}.shadow-soft-md {--tw-shadow: 0 4px 7px -1px rgba(0,0,0,.11),0 2px 4px -1px rgba(0,0,0,.07);--tw-shadow-colored: 0 4px 7px -1px var(--tw-shadow-color), 0 2px 4px -1px var(--tw-shadow-color);box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);}.shadow-soft-3xl {--tw-shadow: 0 8px 26px -4px hsla(0,0%,8%,.15),0 8px 9px -5px hsla(0,0%,8%,.06);--tw-shadow-colored: 0 8px 26px -4px var(--tw-shadow-color), 0 8px 9px -5px var(--tw-shadow-color);box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);}.shadow-soft-lg {--tw-shadow: 0 2px 12px 0 rgba(0,0,0,.16);--tw-shadow-colored: 0 2px 12px 0 var(--tw-shadow-color);box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);}.shadow-xl {--tw-shadow: 0 23px 45px -11px hsla(0,0%,8%,.25);--tw-shadow-colored: 0 23px 45px -11px var(--tw-shadow-color);box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);}.shadow-blur {--tw-shadow: inset 0 0 1px 1px hsla(0,0%,100%,.9),0 20px 27px 0 rgba(0,0,0,.05);--tw-shadow-colored: inset 0 0 1px 1px var(--tw-shadow-color), 0 20px 27px 0 var(--tw-shadow-color);box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);}.shadow-soft-sm {--tw-shadow: 0 .25rem .375rem -.0625rem hsla(0,0%,8%,.12),0 .125rem .25rem -.0625rem hsla(0,0%,8%,.07);--tw-shadow-colored: 0 .25rem .375rem -.0625rem var(--tw-shadow-color), 0 .125rem .25rem -.0625rem var(--tw-shadow-color);box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);}.shadow-soft-xxs {--tw-shadow: 0 1px 5px 1px #ddd;--tw-shadow-colored: 0 1px 5px 1px var(--tw-shadow-color);box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);}.shadow-transparent {--tw-shadow-color: transparent;--tw-shadow: var(--tw-shadow-colored);}.blur {--tw-blur: blur(8px);filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);}.filter {filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);}.backdrop-blur-2xl {--tw-backdrop-blur: blur(30px);-webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);}.backdrop-blur-\[30px\] {--tw-backdrop-blur: blur(30px);-webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);}.backdrop-saturate-200 {--tw-backdrop-saturate: saturate(2);-webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);}.backdrop-saturate-\[200\%\] {--tw-backdrop-saturate: saturate(200%);-webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);}.transition-transform {transition-property: transform;transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);transition-duration: 150ms;}.transition-all {transition-property: all;transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);transition-duration: 150ms;}.transition-colors {transition-property: color, background-color, border-color, fill, stroke, -webkit-text-decoration-color;transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, -webkit-text-decoration-color;transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);transition-duration: 150ms;}.transition {transition-property: color, background-color, border-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-text-decoration-color, -webkit-backdrop-filter;transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-text-decoration-color, -webkit-backdrop-filter;transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);transition-duration: 150ms;}.duration-200 {transition-duration: 200ms;}.duration-300 {transition-duration: 300ms;}.duration-250 {transition-duration: 250ms;}.duration-600 {transition-duration: 600ms;}.duration-500 {transition-duration: 500ms;}.duration-350 {transition-duration: 350ms;}.ease-soft {transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);}.ease-in {transition-timing-function: cubic-bezier(0.4, 0, 1, 1);}.ease-soft-in-out {transition-timing-function: cubic-bezier(0.42, 0, 0.58, 1);}.ease-soft-in {transition-timing-function: cubic-bezier(0.42, 0, 1, 1);}.ease-bounce {transition-timing-function: cubic-bezier(0.34, 1.61, 0.7, 1.3);}.ease-soft-out {transition-timing-function: cubic-bezier(0, 0, 0.58, 1);}.transform3d {transform: perspective(999px) rotateX(0deg) translateZ(0);}.transform-dropdown {transform: perspective(999px) rotateX(-10deg) translateZ(0) translate3d(0,37px,0);}.transform-dropdown-show {transform: perspective(999px) rotateX(0deg) translateZ(0) translate3d(0,37px,5px);}.flex-wrap-inherit {flex-wrap: inherit;}.placeholder\:text-gray-500::-moz-placeholder {--tw-text-opacity: 1;color: rgb(173 181 189 / var(--tw-text-opacity));}.placeholder\:text-gray-500::placeholder {--tw-text-opacity: 1;color: rgb(173 181 189 / var(--tw-text-opacity));}.before\:visible::before {content: var(--tw-content);visibility: visible;}.before\:absolute::before {content: var(--tw-content);position: absolute;}.before\:right-2::before {content: var(--tw-content);right: 0.5rem;}.before\:left-auto::before {content: var(--tw-content);left: auto;}.before\:top-0::before {content: var(--tw-content);top: 0px;}.before\:right-7::before {content: var(--tw-content);right: 1.75rem;}.before\:left-4::before {content: var(--tw-content);left: 1rem;}.before\:right-auto::before {content: var(--tw-content);right: auto;}.before\:left-2::before {content: var(--tw-content);left: 0.5rem;}.before\:left-7::before {content: var(--tw-content);left: 1.75rem;}.before\:right-4::before {content: var(--tw-content);right: 1rem;}.before\:-top-5::before {content: var(--tw-content);top: -1.25rem;}.before\:z-50::before {content: var(--tw-content);z-index: 50;}.before\:z-40::before {content: var(--tw-content);z-index: 40;}.before\:float-right::before {content: var(--tw-content);float: right;}.before\:float-left::before {content: var(--tw-content);float: left;}.before\:inline-block::before {content: var(--tw-content);display: inline-block;}.before\:h-2::before {content: var(--tw-content);height: 0.5rem;}.before\:h-full::before {content: var(--tw-content);height: 100%;}.before\:w-2::before {content: var(--tw-content);width: 0.5rem;}.before\:rotate-45::before {content: var(--tw-content);--tw-rotate: 45deg;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.before\:border-r-2::before {content: var(--tw-content);border-right-width: 2px;}.before\:border-l-2::before {content: var(--tw-content);border-left-width: 2px;}.before\:border-r-slate-100::before {content: var(--tw-content);--tw-border-opacity: 1;border-right-color: rgb(222 226 230 / var(--tw-border-opacity));}.before\:border-l-slate-100::before {content: var(--tw-content);--tw-border-opacity: 1;border-left-color: rgb(222 226 230 / var(--tw-border-opacity));}.before\:bg-inherit::before {content: var(--tw-content);background-color: inherit;}.before\:pr-2::before {content: var(--tw-content);padding-right: 0.5rem;}.before\:pl-2::before {content: var(--tw-content);padding-left: 0.5rem;}.before\:font-awesome::before {content: var(--tw-content);font-family: FontAwesome;}.before\:text-5\.5::before {content: var(--tw-content);font-size: 1.375rem;}.before\:text-5::before {content: var(--tw-content);font-size: 1.25rem;}.before\:font-normal::before {content: var(--tw-content);font-weight: 400;}.before\:leading-default::before {content: var(--tw-content);line-height: 1.6;}.before\:text-gray-600::before {content: var(--tw-content);--tw-text-opacity: 1;color: rgb(108 117 125 / var(--tw-text-opacity));}.before\:text-white::before {content: var(--tw-content);--tw-text-opacity: 1;color: rgb(255 255 255 / var(--tw-text-opacity));}.before\:antialiased::before {content: var(--tw-content);-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;}.before\:transition-all::before {content: var(--tw-content);transition-property: all;transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);transition-duration: 150ms;}.before\:duration-350::before {content: var(--tw-content);transition-duration: 350ms;}.before\:ease-soft::before {content: var(--tw-content);transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);}.before\:content-\[\'\/\'\]::before {--tw-content: '/';content: var(--tw-content);}.before\:content-\[\'\\f0d8\'\]::before {--tw-content: '\f0d8';content: var(--tw-content);}.before\:content-\[\'\'\]::before {--tw-content: '';content: var(--tw-content);}.after\:absolute::after {content: var(--tw-content);position: absolute;}.after\:top-0::after {content: var(--tw-content);top: 0px;}.after\:bottom-0::after {content: var(--tw-content);bottom: 0px;}.after\:left-0::after {content: var(--tw-content);left: 0px;}.after\:top-px::after {content: var(--tw-content);top: 1px;}.after\:z-10::after {content: var(--tw-content);z-index: 10;}.after\:clear-both::after {content: var(--tw-content);clear: both;}.after\:block::after {content: var(--tw-content);display: block;}.after\:flex::after {content: var(--tw-content);display: flex;}.after\:table::after {content: var(--tw-content);display: table;}.after\:h-full::after {content: var(--tw-content);height: 100%;}.after\:h-4::after {content: var(--tw-content);height: 1rem;}.after\:w-full::after {content: var(--tw-content);width: 100%;}.after\:w-4::after {content: var(--tw-content);width: 1rem;}.after\:translate-x-px::after {content: var(--tw-content);--tw-translate-x: 1px;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.after\:-translate-x-px::after {content: var(--tw-content);--tw-translate-x: -1px;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.after\:items-center::after {content: var(--tw-content);align-items: center;}.after\:justify-center::after {content: var(--tw-content);justify-content: center;}.after\:rounded-2xl::after {content: var(--tw-content);border-radius: 1rem;}.after\:rounded-circle::after {content: var(--tw-content);border-radius: 50%;}.after\:bg-white::after {content: var(--tw-content);--tw-bg-opacity: 1;background-color: rgb(255 255 255 / var(--tw-bg-opacity));}.after\:bg-gradient-to-tl::after {content: var(--tw-content);background-image: linear-gradient(to top left, var(--tw-gradient-stops));}.after\:from-gray-900::after {content: var(--tw-content);--tw-gradient-from: #141727;--tw-gradient-to: rgb(20 23 39 / 0);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);}.after\:from-blue-600::after {content: var(--tw-content);--tw-gradient-from: #2152ff;--tw-gradient-to: rgb(33 82 255 / 0);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);}.after\:from-red-500::after {content: var(--tw-content);--tw-gradient-from: #f53939;--tw-gradient-to: rgb(245 57 57 / 0);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);}.after\:from-green-600::after {content: var(--tw-content);--tw-gradient-from: #17ad37;--tw-gradient-to: rgb(23 173 55 / 0);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);}.after\:from-red-600::after {content: var(--tw-content);--tw-gradient-from: #ea0606;--tw-gradient-to: rgb(234 6 6 / 0);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);}.after\:from-slate-600::after {content: var(--tw-content);--tw-gradient-from: #627594;--tw-gradient-to: rgb(98 117 148 / 0);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);}.after\:from-purple-700::after {content: var(--tw-content);--tw-gradient-from: #7928ca;--tw-gradient-to: rgb(121 40 202 / 0);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);}.after\:to-slate-800::after {content: var(--tw-content);--tw-gradient-to: #3a416f;}.after\:to-cyan-400::after {content: var(--tw-content);--tw-gradient-to: #21d4fd;}.after\:to-yellow-400::after {content: var(--tw-content);--tw-gradient-to: #fbcf33;}.after\:to-lime-400::after {content: var(--tw-content);--tw-gradient-to: #98ec2d;}.after\:to-rose-400::after {content: var(--tw-content);--tw-gradient-to: #ff667c;}.after\:to-slate-300::after {content: var(--tw-content);--tw-gradient-to: #a8b8d8;}.after\:to-pink-500::after {content: var(--tw-content);--tw-gradient-to: #ff0080;}.after\:font-awesome::after {content: var(--tw-content);font-family: FontAwesome;}.after\:text-xxs::after {content: var(--tw-content);font-size: 0.65rem;line-height: 1rem;}.after\:text-white::after {content: var(--tw-content);--tw-text-opacity: 1;color: rgb(255 255 255 / var(--tw-text-opacity));}.after\:opacity-65::after {content: var(--tw-content);opacity: 0.65;}.after\:opacity-0::after {content: var(--tw-content);opacity: 0;}.after\:opacity-85::after {content: var(--tw-content);opacity: 0.85;}.after\:shadow-soft-2xl::after {content: var(--tw-content);--tw-shadow: 0 .3125rem .625rem 0 rgba(0,0,0,.12);--tw-shadow-colored: 0 .3125rem .625rem 0 var(--tw-shadow-color);box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);}.after\:transition-all::after {content: var(--tw-content);transition-property: all;transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);transition-duration: 150ms;}.after\:duration-250::after {content: var(--tw-content);transition-duration: 250ms;}.after\:ease-soft-in-out::after {content: var(--tw-content);transition-timing-function: cubic-bezier(0.42, 0, 0.58, 1);}.after\:content-\[\'\'\]::after {--tw-content: '';content: var(--tw-content);}.after\:content-\[\'\\f00c\'\]::after {--tw-content: '\f00c';content: var(--tw-content);}.checked\:border-0:checked {border-width: 0px;}.checked\:border-slate-800\/95:checked {border-color: rgb(58 65 111 / 0.95);}.checked\:border-transparent:checked {border-color: transparent;}.checked\:bg-slate-800\/95:checked {background-color: rgb(58 65 111 / 0.95);}.checked\:bg-transparent:checked {background-color: transparent;}.checked\:bg-none:checked {background-image: none;}.checked\:bg-gradient-to-tl:checked {background-image: linear-gradient(to top left, var(--tw-gradient-stops));}.checked\:from-gray-900:checked {--tw-gradient-from: #141727;--tw-gradient-to: rgb(20 23 39 / 0);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);}.checked\:to-slate-800:checked {--tw-gradient-to: #3a416f;}.checked\:bg-right:checked {background-position: right;}.checked\:after\:translate-x-5\.25:checked::after {content: var(--tw-content);--tw-translate-x: 1.3125rem;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.checked\:after\:translate-x-5:checked::after {content: var(--tw-content);--tw-translate-x: 1.25rem;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.checked\:after\:-translate-x-5\.25:checked::after {content: var(--tw-content);--tw-translate-x: -1.3125rem;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.checked\:after\:-translate-x-5:checked::after {content: var(--tw-content);--tw-translate-x: -1.25rem;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.checked\:after\:opacity-100:checked::after {content: var(--tw-content);opacity: 1;}.hover\:z-30:hover {z-index: 30;}.hover\:scale-102:hover {--tw-scale-x: 1.02;--tw-scale-y: 1.02;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.hover\:border-fuchsia-500:hover {--tw-border-opacity: 1;border-color: rgb(203 12 159 / var(--tw-border-opacity));}.hover\:border-slate-700:hover {--tw-border-opacity: 1;border-color: rgb(52 71 103 / var(--tw-border-opacity));}.hover\:border-white:hover {--tw-border-opacity: 1;border-color: rgb(255 255 255 / var(--tw-border-opacity));}.hover\:border-white\/75:hover {border-color: rgb(255 255 255 / 0.75);}.hover\:bg-transparent:hover {background-color: transparent;}.hover\:bg-gray-200:hover {--tw-bg-opacity: 1;background-color: rgb(233 236 239 / var(--tw-bg-opacity));}.hover\:bg-white\/10:hover {background-color: rgb(255 255 255 / 0.1);}.hover\:bg-slate-700:hover {--tw-bg-opacity: 1;background-color: rgb(52 71 103 / var(--tw-bg-opacity));}.hover\:text-fuchsia-500:hover {--tw-text-opacity: 1;color: rgb(203 12 159 / var(--tw-text-opacity));}.hover\:text-slate-700:hover {--tw-text-opacity: 1;color: rgb(52 71 103 / var(--tw-text-opacity));}.hover\:text-white:hover {--tw-text-opacity: 1;color: rgb(255 255 255 / var(--tw-text-opacity));}.hover\:text-fuchsia-800:hover {--tw-text-opacity: 1;color: rgb(131 8 102 / var(--tw-text-opacity));}.hover\:opacity-75:hover {opacity: 0.75;}.hover\:shadow-soft-2xl:hover {--tw-shadow: 0 .3125rem .625rem 0 rgba(0,0,0,.12);--tw-shadow-colored: 0 .3125rem .625rem 0 var(--tw-shadow-color);box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);}.hover\:shadow-none:hover {--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);}.hover\:shadow-soft-xs:hover {--tw-shadow: 0 3px 5px -1px rgba(0,0,0,.09),0 2px 3px -1px rgba(0,0,0,.07);--tw-shadow-colored: 0 3px 5px -1px var(--tw-shadow-color), 0 2px 3px -1px var(--tw-shadow-color);box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);}.hover\:transform3d-hover:hover {transform: perspective(999px) rotateX(7deg) translate3d(0,-4px,5px);}.focus\:border-fuchsia-300:focus {--tw-border-opacity: 1;border-color: rgb(226 147 211 / var(--tw-border-opacity));}.focus\:bg-white:focus {--tw-bg-opacity: 1;background-color: rgb(255 255 255 / var(--tw-bg-opacity));}.focus\:text-gray-700:focus {--tw-text-opacity: 1;color: rgb(73 80 87 / var(--tw-text-opacity));}.focus\:shadow-soft-primary-outline:focus {--tw-shadow: 0 0 0 2px #e9aede;--tw-shadow-colored: 0 0 0 2px var(--tw-shadow-color);box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);}.focus\:outline-none:focus {outline: 2px solid transparent;outline-offset: 2px;}.focus\:transition-shadow:focus {transition-property: box-shadow;transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);transition-duration: 150ms;}.active\:scale-100:active {--tw-scale-x: 1;--tw-scale-y: 1;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.active\:border-fuchsia-500:active {--tw-border-opacity: 1;border-color: rgb(203 12 159 / var(--tw-border-opacity));}.active\:border-white\/75:active {border-color: rgb(255 255 255 / 0.75);}.active\:bg-fuchsia-500:active {--tw-bg-opacity: 1;background-color: rgb(203 12 159 / var(--tw-bg-opacity));}.active\:bg-slate-700:active {--tw-bg-opacity: 1;background-color: rgb(52 71 103 / var(--tw-bg-opacity));}.active\:bg-white:active {--tw-bg-opacity: 1;background-color: rgb(255 255 255 / var(--tw-bg-opacity));}.active\:text-white:active {--tw-text-opacity: 1;color: rgb(255 255 255 / var(--tw-text-opacity));}.active\:text-black:active {--tw-text-opacity: 1;color: rgb(0 0 0 / var(--tw-text-opacity));}.active\:opacity-85:active {opacity: 0.85;}.active\:shadow-soft-xs:active {--tw-shadow: 0 3px 5px -1px rgba(0,0,0,.09),0 2px 3px -1px rgba(0,0,0,.07);--tw-shadow-colored: 0 3px 5px -1px var(--tw-shadow-color), 0 2px 3px -1px var(--tw-shadow-color);box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);}.hover\:active\:scale-102:active:hover {--tw-scale-x: 1.02;--tw-scale-y: 1.02;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.active\:hover\:scale-102:hover:active {--tw-scale-x: 1.02;--tw-scale-y: 1.02;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.active\:hover\:border-fuchsia-500:hover:active {--tw-border-opacity: 1;border-color: rgb(203 12 159 / var(--tw-border-opacity));}.active\:hover\:border-white\/75:hover:active {border-color: rgb(255 255 255 / 0.75);}.active\:hover\:bg-transparent:hover:active {background-color: transparent;}.active\:hover\:bg-white\/10:hover:active {background-color: rgb(255 255 255 / 0.1);}.active\:hover\:text-fuchsia-500:hover:active {--tw-text-opacity: 1;color: rgb(203 12 159 / var(--tw-text-opacity));}.active\:hover\:text-slate-700:hover:active {--tw-text-opacity: 1;color: rgb(52 71 103 / var(--tw-text-opacity));}.active\:hover\:text-white:hover:active {--tw-text-opacity: 1;color: rgb(255 255 255 / var(--tw-text-opacity));}.active\:hover\:opacity-75:hover:active {opacity: 0.75;}.active\:hover\:shadow-none:hover:active {--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);}.group:hover .group-hover\:translate-x-1\.25 {--tw-translate-x: 0.3125rem;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.group:hover .group-hover\:translate-x-1 {--tw-translate-x: 0.25rem;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.group:hover .group-hover\:-translate-x-1\.25 {--tw-translate-x: -0.3125rem;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.group:hover .group-hover\:-translate-x-1 {--tw-translate-x: -0.25rem;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}@media (min-width: 576px) {.sm\:my-6 {margin-top: 1.5rem;margin-bottom: 1.5rem;}.sm\:my-auto {margin-top: auto;margin-bottom: auto;}.sm\:mr-16 {margin-right: 4rem;}.sm\:mt-0 {margin-top: 0px;}.sm\:mr-6 {margin-right: 1.5rem;}.sm\:mr-1 {margin-right: 0.25rem;}.sm\:-mr-6 {margin-right: -1.5rem;}.sm\:ml-2 {margin-left: 0.5rem;}.sm\:mr-0 {margin-right: 0px;}.sm\:mb-0 {margin-bottom: 0px;}.sm\:inline {display: inline;}.sm\:w-1\/2 {width: 50%;}.sm\:flex-none {flex: none;}.sm\:px-6 {padding-left: 1.5rem;padding-right: 1.5rem;}.sm\:pt-4 {padding-top: 1rem;}.before\:sm\:right-7\.5::before {content: var(--tw-content);right: 1.875rem;}.before\:sm\:right-7::before {content: var(--tw-content);right: 1.75rem;}.before\:sm\:left-3::before {content: var(--tw-content);left: 0.75rem;}}@media (min-width: 768px) {.md\:mr-0 {margin-right: 0px;}.md\:ml-auto {margin-left: auto;}.md\:mb-0 {margin-bottom: 0px;}.md\:mt-0 {margin-top: 0px;}.md\:-mt-56 {margin-top: -14rem;}.md\:block {display: block;}.md\:w-1\/2 {width: 50%;}.md\:w-8\/12 {width: 66.666667%;}.md\:w-7\/12 {width: 58.333333%;}.md\:w-5\/12 {width: 41.666667%;}.md\:w-4\/12 {width: 33.333333%;}.md\:w-6\/12 {width: 50%;}.md\:w-1\/12 {width: 8.333333%;}.md\:w-11\/12 {width: 91.666667%;}.md\:flex-none {flex: none;}.md\:flex-0 {flex: 0 0 auto;}.md\:scale-70 {--tw-scale-x: .7;--tw-scale-y: .7;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.md\:pr-4 {padding-right: 1rem;}}@media (max-width: 768px) {.md-max\:w-full {width: 100%;}}@media (min-width: 992px) {.lg\:absolute {position: absolute;}.lg\:right-0 {right: 0px;}.lg\:left-auto {left: auto;}.lg\:float-right {float: right;}.lg\:mt-2 {margin-top: 0.5rem;}.lg\:mb-0 {margin-bottom: 0px;}.lg\:mt-0 {margin-top: 0px;}.lg\:ml-0 {margin-left: 0px;}.lg\:-mt-48 {margin-top: -12rem;}.lg\:ml-12 {margin-left: 3rem;}.lg\:-mt-6 {margin-top: -1.5rem;}.lg\:block {display: block;}.lg\:flex {display: flex;}.lg\:hidden {display: none;}.lg\:w-7\/12 {width: 58.333333%;}.lg\:w-1\/2 {width: 50%;}.lg\:w-5\/12 {width: 41.666667%;}.lg\:w-2\/3 {width: 66.666667%;}.lg\:w-1\/3 {width: 33.333333%;}.lg\:w-full {width: 100%;}.lg\:w-4\/12 {width: 33.333333%;}.lg\:w-8\/12 {width: 66.666667%;}.lg\:max-w-120 {max-width: 30rem;}.lg\:flex-none {flex: none;}.lg\:flex-0 {flex: 0 0 auto;}.lg\:basis-auto {flex-basis: auto;}.lg\:cursor-pointer {cursor: pointer;}.lg\:flex-row {flex-direction: row;}.lg\:flex-nowrap {flex-wrap: nowrap;}.lg\:justify-start {justify-content: flex-start;}.lg\:justify-end {justify-content: flex-end;}.lg\:justify-between {justify-content: space-between;}.lg\:px-2 {padding-left: 0.5rem;padding-right: 0.5rem;}.lg\:pt-0 {padding-top: 0px;}.lg\:text-left {text-align: left;}.lg\:text-right {text-align: right;}.lg\:shadow-soft-3xl {--tw-shadow: 0 8px 26px -4px hsla(0,0%,8%,.15),0 8px 9px -5px hsla(0,0%,8%,.06);--tw-shadow-colored: 0 8px 26px -4px var(--tw-shadow-color), 0 8px 9px -5px var(--tw-shadow-color);box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);}.lg\:transition-colors {transition-property: color, background-color, border-color, fill, stroke, -webkit-text-decoration-color;transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, -webkit-text-decoration-color;transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);transition-duration: 150ms;}.lg\:duration-300 {transition-duration: 300ms;}.lg\:ease-soft {transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);}.before\:lg\:-ml-px::before {content: var(--tw-content);margin-left: -1px;}.before\:lg\:-mr-px::before {content: var(--tw-content);margin-right: -1px;}.lg\:hover\:text-white\/75:hover {color: rgb(255 255 255 / 0.75);}}@media (max-width: 992px) {.lg-max\:mt-6 {margin-top: 1.5rem;}.lg-max\:max-h-0 {max-height: 0px;}.lg-max\:max-h-54 {max-height: 13.5rem;}.lg-max\:overflow-hidden {overflow: hidden;}.lg-max\:bg-white {--tw-bg-opacity: 1;background-color: rgb(255 255 255 / var(--tw-bg-opacity));}.lg-max\:text-slate-700 {--tw-text-opacity: 1;color: rgb(52 71 103 / var(--tw-text-opacity));}.lg-max\:opacity-0 {opacity: 0;}}@media (min-width: 1200px) {.xl\:left-0 {left: 0px;}.xl\:right-0 {right: 0px;}.xl\:left-\[18\%\] {left: 18%;}.xl\:ml-68\.5 {margin-left: 17.125rem;}.xl\:ml-68 {margin-left: 17rem;}.xl\:mb-0 {margin-bottom: 0px;}.xl\:mr-68\.5 {margin-right: 17.125rem;}.xl\:mr-68 {margin-right: 17rem;}.xl\:ml-auto {margin-left: auto;}.xl\:mr-12 {margin-right: 3rem;}.xl\:ml-4 {margin-left: 1rem;}.xl\:hidden {display: none;}.xl\:w-1\/4 {width: 25%;}.xl\:w-1\/2 {width: 50%;}.xl\:w-4\/12 {width: 33.333333%;}.xl\:w-3\/12 {width: 25%;}.xl\:flex-none {flex: none;}.xl\:translate-x-0 {--tw-translate-x: 0px;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}.xl\:scale-60 {--tw-scale-x: .6;--tw-scale-y: .6;transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));}@-webkit-keyframes fade-up {from {opacity: 0;transform: translateY(100%);}to {opacity: 1;}}@keyframes fade-up {from {opacity: 0;transform: translateY(100%);}to {opacity: 1;}}.xl\:animate-fade-up {-webkit-animation: fade-up 1.5s both;animation: fade-up 1.5s both;}.xl\:bg-transparent {background-color: transparent;}.xl\:bg-white {--tw-bg-opacity: 1;background-color: rgb(255 255 255 / var(--tw-bg-opacity));}.xl\:p-2\.5 {padding: 0.625rem;}.xl\:p-2 {padding: 0.5rem;}.xl\:px-12 {padding-left: 3rem;padding-right: 3rem;}.xl\:text-8xl {font-size: 5rem;line-height: 1;}}@media (max-width: 1200px) {.xl-max\:pointer-events-none {pointer-events: none;}.xl-max\:cursor-not-allowed {cursor: not-allowed;}.xl-max\:border-0 {border-width: 0px;}.xl-max\:bg-gradient-to-tl {background-image: linear-gradient(to top left, var(--tw-gradient-stops));}.xl-max\:from-purple-700 {--tw-gradient-from: #7928ca;--tw-gradient-to: rgb(121 40 202 / 0);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);}.xl-max\:to-pink-500 {--tw-gradient-to: #ff0080;}.xl-max\:text-white {--tw-text-opacity: 1;color: rgb(255 255 255 / var(--tw-text-opacity));}.xl-max\:opacity-65 {opacity: 0.65;}} \ No newline at end of file diff --git a/src/main/resources/static/css/tooltips.css b/src/main/resources/static/css/tooltips.css deleted file mode 100644 index 57295ea..0000000 --- a/src/main/resources/static/css/tooltips.css +++ /dev/null @@ -1,15 +0,0 @@ -[data-target="tooltip"][data-popper-placement^="top"] > [data-popper-arrow] { - bottom: -4px; -} - -[data-target="tooltip"][data-popper-placement^="bottom"] > [data-popper-arrow] { - top: -4px; -} - -[data-target="tooltip"][data-popper-placement^="left"] > [data-popper-arrow] { - right: -0px; -} - -[data-target="tooltip"][data-popper-placement^="right"] > [data-popper-arrow] { - left: -4px; -} diff --git a/src/main/resources/static/css/user.css b/src/main/resources/static/css/user.css deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/resources/static/fonts/nucleo-icons.eot b/src/main/resources/static/fonts/nucleo-icons.eot deleted file mode 100644 index ab96810adf6e7862e0297f03958a1b18e0607c37..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18516 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/static/fonts/nucleo-icons.ttf b/src/main/resources/static/fonts/nucleo-icons.ttf deleted file mode 100644 index 1a5598505e4324b19be3e4dee9d2e10df541cc13..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18292 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 SNnay)d%pv%BAh=e_Wlokkczqh diff --git a/src/main/resources/static/fonts/nucleo-icons.woff2 b/src/main/resources/static/fonts/nucleo-icons.woff2 deleted file mode 100644 index e294e0801c9ef9e50268f58c3a82a9e378c8b983..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8580 zcmV-~A$#6;Pew8T0RR9103n0`3jhEB07rBH03kI10RR9100000000000000000000 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 KgDInf{Z|pm-=OIL diff --git a/src/main/resources/static/fonts/nucleo.eot b/src/main/resources/static/fonts/nucleo.eot deleted file mode 100644 index 8609095550154362574be44b0f2cc5834d525c9e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26524 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 Sx4;|gl1b4=E1Mgnwf;YFur9Lz diff --git a/src/main/resources/static/fonts/nucleo.woff b/src/main/resources/static/fonts/nucleo.woff deleted file mode 100644 index 20fecf0d558c8cb40bd5ffa718c0b3cf2e6f2632..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15168 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