diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 13f8d2d..a35c2aa 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,4 +8,4 @@ updates: - package-ecosystem: "gradle" # See documentation for possible values directory: "/" # Location of package manifests schedule: - interval: "daily" + interval: "weekly" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 29396d3..0000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,77 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: ["main"] - pull_request: - # The branches below must be a subset of the branches above - branches: ["main"] - schedule: - - cron: "26 2 * * 3" - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: ["java", "javascript"] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Setup JDK 17 - uses: actions/setup-java@v3 - with: - distribution: "zulu" - java-version: "17" - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 diff --git a/.vscode/settings.json b/.vscode/settings.json index 37a9c69..752d806 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,8 @@ "java.configuration.updateBuildConfiguration": "automatic", "java.compile.nullAnalysis.mode": "automatic", "java.transport": "stdio", - "java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx2G -Xms100m -javaagent:\"/Users/devon/.vscode/extensions/gabrielbb.vscode-lombok-1.0.1/server/lombok.jar\"" + "java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx2G -Xms100m -javaagent:\"/Users/devon/.vscode/extensions/gabrielbb.vscode-lombok-1.0.1/server/lombok.jar\"", + "debug.javascript.defaultRuntimeExecutable": { + "pwa-node": "/Users/devon/.local/share/mise/shims/node" + } } diff --git a/config/README.md b/CHANGELOG.md similarity index 100% rename from config/README.md rename to CHANGELOG.md diff --git a/CONFIG.md b/CONFIG.md index 93a78f4..3d5c712 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -23,9 +23,6 @@ Welcome to the User Framework SpringBoot Configuration Guide! This document outl - **DDL Auto (`spring.jpa.hibernate.ddl-auto`)**: Hibernate schema generation strategy, defaults to `update`. - **Dialect (`spring.jpa.properties.hibernate.dialect`)**: Set this to the appropriate dialect for your database, defaults to `org.hibernate.dialect.MariaDBDialect`. -### Application Properties - -- **Name (`spring.application.name`)**: Set your application's name, defaults to `User Framework`. ## User Settings @@ -47,18 +44,12 @@ Welcome to the User Framework SpringBoot Configuration Guide! This document outl - **From Address (`spring.mail.fromAddress`)**: The email address used as the sender in outgoing emails. -## Copyright - -- **First Year (`spring.copyrightFirstYear`)**: The starting year for the copyright notice. ## Role and Privileges - **Roles and Privileges (`spring.roles-and-privileges`)**: Map out roles to their respective privileges. - **Role Hierarchy (`spring.role-hierarchy`)**: Define the hierarchy and inheritance of roles. -## New Relic Monitoring - -- **API Key and Account ID (`management.newrelic.metrics.export`)**: Required if you're integrating with New Relic for monitoring. ## Server and Session Settings diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 4ab0162..0000000 --- a/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM amazoncorretto:11-alpine-jdk -COPY build/libs/user-1.0.0-SNAPSHOT.jar user-1.0.0-SNAPSHOT.jar -ENTRYPOINT ["java","-jar","/user-1.0.0-SNAPSHOT.jar"] diff --git a/PUBLISH.md b/PUBLISH.md new file mode 100644 index 0000000..9545ebc --- /dev/null +++ b/PUBLISH.md @@ -0,0 +1,34 @@ +# Maven Publishing Guide + +# Build and Publish Command Reference + +## Building the Project + +To build the project, run: + +```sh +./gradlew build +``` + + +## Publish to Local Maven + +```shell +gradle publishLocal +``` + +## Publish to Private Maven repository + +```shell +gradle publishReposilite +``` + + +## Publish to Maven Central + +```shell +gradle publishMavenCentral +``` + + + diff --git a/QUICKSTART.md b/QUICKSTART.md deleted file mode 100644 index d706700..0000000 --- a/QUICKSTART.md +++ /dev/null @@ -1,33 +0,0 @@ -# Quickstart Guide - -## Prerequisites - - Java Development Kit (JDK) 17 or later - -## Quick Note - -This Framework is intended to be copied and used as a template for new projects. It is not intended to be used as a dependency. - -While it would be nice to vend this as a library through Maven or Gradle, I don't belive it's possible to do so. In order for this framework to be useful (for my needs) it needs to provide the front end pages, JS, and set Spring configurations. - -If anyone knows a way to do this as a dependancy, please let me knowm, or submit a PR. - - -## Getting Started - -1. Download this project as a zip file and extract it to a new folder. -2. Open the project in your favorite IDE. I use VSCode. -3. Copy the `src/main/resources/application-local.yml-example` file to `src/main/resources/application-local.yml` -4. Edit the `src/main/resources/application-local.yml` file to set your configurations for things like SMTP server, Facebook or Google OAuth information, etc. If you need to override any defaults from `application.yml` you can do so here. -5. Create the local database: `docker run -p 127.0.0.1:3306:3306 --name springuserframework -e MARIADB_ROOT_PASSWORD=springuserroot -e MARIADB_DATABASE=springuser -e MARIADB_USER=springuser -e MARIADB_PASSWORD=springuser -d mariadb:latest` -6. If you are using a public hostname for OAuth (Google or Facebook), you will need to setup an [ngrok tunnel](https://medium.com/@Demipo/exposing-a-local-spring-boot-app-with-ngrok-819250ef75f) or [CloudFlare tunnel](https://vitobotta.com/2022/02/27/free-ngrok-alternative-with-cloudflare-tunnels/) -7. Run the project. You can do this from the command line with `./gradlew bootRun` -8. Open a browser and go to `http://localhost:8080` to see the home page. -9. If things are working, you can now develop your own application on top of this framework - - -## Bugs, Gaps, Questions -If you find any issues, gaps in documentation or features, or have any questions, please open an issue on GitHub! - - - -Back to [README.md](README.md) diff --git a/README.md b/README.md index 354b89b..0723547 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,24 @@ -# SpringUserFramework +## Table of Contents +- [SpringUserFramework](#springuserframework) + - [Summary](#summary) + - [Features](#features) + - [How To Get Started](#how-to-get-started) + - [Refer to the Demo Project](#refer-to-the-demo-project) + - [Configuring Your Local Environment](#configuring-your-local-environment) + - [Database](#database) + - [Mail Sending (SMTP)](#mail-sending-smtp) + - [SSO OAuth2 with Google and Facebook](#sso-oauth2-with-google-and-facebook) + - [Overriding Spring Security Messages](#overriding-spring-security-messages) + - [Notes](#notes) + +# SpringUserFramework -> âš ī¸ **WARNING:** This project is about to undergo a major change and be refactored into a library for easier use by your application. If you aren't already using this project, I suggest waiting a couple weeks for the massive new update coming soon! +SpringUserFramework is a Java Spring Boot User Management Framework designed to simplify the implementation of user management features in your SpringBoot web application. It is built on top of [Spring Security](https://spring.io/projects/spring-security) and provides out-of-the-box support for registration, login, logout, and forgot password flows. It also supports SSO with Google and Facebook. -SpringUserFramework is a Java Spring Boot User Management Framework designed to simplify the implementation of user management features in your Spring-based web application. It is built on top of [Spring Security](https://spring.io/projects/spring-security) and provides out-of-the-box support for registration, login, logout, and forgot password flows. The framework includes basic example pages that are unstyled, allowing for seamless integration into your application. +The framework includes basic example pages that are unstyled, allowing for seamless integration into your application. ## Summary @@ -26,6 +39,8 @@ The framework provides support for the following features: - Login and logout functionality. - Forgot password flow. - Database-backed user store using Spring JPA. +- SSO support for Google +- SSO support for Facebook - Configuration options to control anonymous access, whitelist URIs, and protect specific URIs requiring a logged-in user session. - CSRF protection enabled by default, with example jQuery AJAX calls passing the CSRF token from the Thymeleaf page context. - Audit event framework for recording and logging security events, customizable to store audit events in a database or publish them via a REST API. @@ -36,85 +51,87 @@ The framework provides support for the following features: ## How To Get Started -### Quickstart Guide -You can jump right in and get started by following the [Quickstart Guide](QUICKSTART.md). - -For more information, read on. - -### Configuring Your Local Environment -There is an example configuration file in /src/main/resources called application-local.yml-example. By default this project's gradle bootRun command runs Spring using the "local" profile. So you can just copy that file to application-local.yml and replace the values (keys, URLs, etc..) with your values. If you are using a different profile to run (such as default) you will just need to ensure the same configs are in place in your active configuration file(s). - -You can read more about the required configuration values in the [Configuration Guide](CONFIG.md). - -Missing or incorrect configuration values will make this framework not work correctly. +This Framework is now available as a library on Maven Central. You can add it to your Gradle project by adding the following dependency to your `build.gradle` file: -### Database -This framework uses a database as a user store. By buildling on top of Spring JPA it is easy to use which ever datastore you like. The example configuration in application.yml is for a [MariaDB](https://mariadb.com) 10.5 database. You will need to create a user and a database and configure the database name, username, and password. +```groovy +implementation 'com.digitalsanctuary:ds-spring-user-framework:3.0.0' +``` -You can do this using docker with a command like this: +Or to your Maven project by adding it to your `pom.xml` file: -``` -docker run -p 127.0.0.1:3306:3306 --name springuserframework -e MARIADB_ROOT_PASSWORD=springuserroot -e MARIADB_DATABASE=springuser -e MARIADB_USER=springuser -e MARIADB_PASSWORD=springuser -d mariadb:latest +```xml + + com.digitalsanctuary + ds-spring-user-framework + 3.0.0 + ``` -Or on Apple Silicon: +Please check for the latest version on [Maven Central](https://central.sonatype.com/artifact/com.digitalsanctuary/ds-spring-user-framework) (this README may not always be up to date). +When upgrading to a new version, please check the [CHANGELOG](CHANGELOG.md) for any breaking changes or new features. -``` -docker run -p 127.0.0.1:3306:3306 --name springuserframework -e MARIADB_ROOT_PASSWORD=springuserroot -e MARIADB_DATABASE=springuser -e MARIADB_USER=springuser -e MARIADB_PASSWORD=springuser -d arm64v8/mariadb:latest -``` -### Mail Sending (SMTP) -The framework sends emails for verficiation links, forgot password flow, etc... so you need to configure the outbound SMTP server and authentication information. +### Refer to the Demo Project +I have created a demo project that uses this framework. You can find it here: [SpringUserFrameworkDemo](https://github.com/devondragon/SpringUserFrameworkDemoApp). This demo project is a full SpringBoot application that uses this framework as a library. You can use it as a reference for how to use this framework in your own project. It demonstrates all of the configuration values and how to override them in your own `application.yml` file. It also has functioning examples for all front end pages, javascript, etc... -### SSO OAuth2 with Google and Facebook -The framework supports SSO OAuth2 with Google and Facebook. To enable this you need to configure the client id and secret for each provider. +In addition to being a fully functional reference, you can also use the demo project as a starting point for your own project. Just clone the repo and start building your own application on top of it. -For public OAuth you will need a public hostname and HTTPS enabled. You can use ngrok to create a public hostname and tunnel to your local machine. You can then use the ngrok hostname in your Google and Facebook developer console configuration. +### Configuring Your Local Environment -### New Relic -Out of the box the project includes the New Relic Telemetry module, and as such requires a New Relic account id, and associated API key. If you don't use New Relic you can remove the dependancy from the build.gradle file and ignore the configuration values. +You can read more about the required configuration values in the [Configuration Guide](CONFIG.md). -Beyond that the default configurations should be all you need, although of course you can customize things however you like. +Missing or incorrect configuration values will make this framework not work correctly. -## Docker +### Database +This framework uses a database as a user store. By building on top of Spring JPA it is easy to use whichever database you like. -After running gradle build, you can build a simple Docker image of the application using the provided Dockerfile. Please note that this Dockerfile is basic and does not incorporate advanced features such as layering or buildpacks that you may require for production applications. +If you set your JPA Hibernate ddl-auto property to "create" it will create the tables for you. If you set it to "update" it will update the tables for you. If you set it to "none" you will need to create the tables yourself. -Additionally, a docker-compose file is included, which launches a stack with the Spring Boot Application, MariaDB Database, and Postfix Mail Server. The configurations in the docker-compose file are set to make everything work smoothly. However, please be aware that sending emails from your computer (via the docker Postfix Mail Server) may be blocked by email providers due to spam checks. You can use temporary email addresses from [10MinuteMail.com](https://10minutemail.com) for testing purposes, but for real use, it is recommended to configure the Spring Boot application to use a real mail server for outbound transactional emails. +If you are not using automatic schema updates or Flyway, you can set up your database manually using the provided `schema.sql` file: +```bash +mysql -u username -p database_name < src/main/resources/schema.sql +``` +Flyway support will be coming soon. This will allow you to automatically update your database schema as you deploy new versions of your application. -## Overriding Spring Security Messages -You may want to override the default Spring Security user facing messages. You can do this in your messages.properties file, by adding any of the message keys from Spring Security (found here: [Spring Security Messages](https://github.com/spring-projects/spring-security/blob/main/core/src/main/resources/org/springframework/security/messages.properties)) and providing your own values. +### Mail Sending (SMTP) +The framework sends emails for verification links, forgot password flow, etc... so you need to configure the outbound SMTP server and authentication information. This is done in the `application.yml` file. You can see the example configuration in the Demo Project's `application.yml` file. Please also refer to the [Spring Boot Mail Properties](https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#mail-properties) for more information on the available properties. -## Dev Tools +### SSO OAuth2 with Google and Facebook +The framework supports SSO OAuth2 with Google and Facebook. To enable this you need to configure the client id and secret for each provider. This is done in the application.yml (or application.properties) file using the [Spring Security OAuth2 properties](https://docs.spring.io/spring-security/reference/servlet/oauth2/login/core.html). You can see the example configuration in the Demo Project's `application.yml` file. + +Here is a quick example for your reference: + +```yaml +spring: + security: + oauth2: + client: + registration: + google: + client-id: YOUR_GOOGLE_CLIENT_ID + client-secret: YOUR_GOOGLE_CLIENT_SECRET + redirect-uri: "{baseUrl}/login/oauth2/code/google" + facebook: + client-id: YOUR_FACEBOOK_CLIENT_ID + client-secret: YOUR_FACEBOOK_CLIENT_SECRET + redirect-uri: "{baseUrl}/login/oauth2/code/facebook" +``` -### SpringBoot DevTools Auto Restart and Live Reload -Read the following articles: - - https://www.digitalsanctuary.com/java/springboot-devtools-auto-restart-and-live-reload.html - - https://www.digitalsanctuary.com/java/how-to-get-springboot-livereload-working-over-https.html +For public OAuth you will need a public hostname and HTTPS enabled. You can use ngrok or Cloudflare tunnels to create a public hostname and tunnel to your local machine during development. You can then use the ngrok hostname in your Google and Facebook developer console configuration. -### Live Reload over HTTPS Setup -If you are running your local dev env using HTTPS or referencing it from a ngrok tunnel using HTTPS, you will need to make a few changes to get Live Reload to work. First you need to tell the application to use HTTPS by setting the following properties in your application.yml file: -``` -spring.devtools.livereload.https=true -``` -You then need to install mitmproxy and configure it to intercept the HTTPS traffic. You can do this by running the following command: -``` -mitmproxy --mode reverse:http://localhost:35729 -p 35739 -``` +## Overriding Spring Security Messages + +You may want to override the default Spring Security user facing messages. You can do this in your messages.properties file, by adding any of the message keys from Spring Security (found here: [Spring Security Messages](https://github.com/spring-projects/spring-security/blob/main/core/src/main/resources/org/springframework/security/messages.properties)) and providing your own values. -By default, mitmproxy uses self-signed SSL certificates, so you need to tell your browser to trust them before this will work. You can do this by opening https://localhost:35739/livereload.js in your browser and going through the steps to trust the server and certificate. -Alternatively, you can configure mitmproxy to use real certificates and avoid this step. Follow these directions: https://docs.mitmproxy.org/stable/concepts-certificates/ ## Notes -Much of this is based on the [Baeldung course on Spring Security](https://www.baeldung.com/learn-spring-security-course). If you want to learn more about Spring Security or would like to add SSO integration or 2FA to your application, that guide is a great place to start. - Please note that there is no warranty or guarantee of functionality, quality, performance, or security made by the author. The code is available freely, but you assume all responsibility and liability for its usage in your application. diff --git a/build.gradle b/build.gradle index b2886ad..947614f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,29 +1,31 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.3.5' - id 'io.spring.dependency-management' version '1.1.7' - id "com.github.ben-manes.versions" version "0.51.0" - + id 'org.springframework.boot' version '3.4.1' + id 'io.spring.dependency-management' version '1.1.7' + id 'com.github.ben-manes.versions' version '0.51.0' + id 'java-library' + id 'maven-publish' + id 'signing' + id 'com.vanniktech.maven.publish' version '0.30.0' + id 'net.researchgate.release' version '3.1.0' } -group = 'com.digitalsanctuary.spring' -version = '1.0.0-SNAPSHOT' -sourceCompatibility = '17' -targetCompatibility = '17' +import com.vanniktech.maven.publish.SonatypeHost +import com.vanniktech.maven.publish.JavaLibrary +import com.vanniktech.maven.publish.JavadocJar -// Define the configurations used in the project -configurations { - developmentOnly - runtimeOnly { - extendsFrom developmentOnly - } - testImplementation { - extendsFrom runtimeOnly - } - compileOnly { - extendsFrom annotationProcessor +group = 'com.digitalsanctuary.springuser' +// version = '3.0.0-SNAPSHOT' +description = 'Spring User Framework' + +ext { + springBootVersion = '3.4.1' + lombokVersion = '1.18.36' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) } - dev } repositories { @@ -32,39 +34,45 @@ repositories { dependencies { // Spring Boot starters - implementation 'org.springframework.boot:spring-boot-starter-actuator' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-jdbc' - implementation 'org.springframework.boot:spring-boot-starter-mail' - implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' - implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.3.RELEASE' - implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.3.0' - + compileOnly 'org.springframework.boot:spring-boot-starter-actuator' + compileOnly 'org.springframework.boot:spring-boot-starter-data-jpa' + compileOnly 'org.springframework.boot:spring-boot-starter-jdbc' + compileOnly 'org.springframework.boot:spring-boot-starter-mail' + compileOnly 'org.springframework.boot:spring-boot-starter-oauth2-client' + compileOnly 'org.springframework.boot:spring-boot-starter-security' + compileOnly 'org.springframework.boot:spring-boot-starter-thymeleaf' + compileOnly "org.springframework.boot:spring-boot-starter-web:$springBootVersion" + compileOnly 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.3.RELEASE' + compileOnly 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.3.0' // Other dependencies runtimeOnly 'org.springframework.boot:spring-boot-devtools' - // runtimeOnly 'io.micrometer:micrometer-registry-new-relic' runtimeOnly 'org.mariadb.jdbc:mariadb-java-client:3.5.1' runtimeOnly 'org.postgresql:postgresql' implementation 'org.passay:passay:1.6.6' implementation 'com.google.guava:guava:33.4.0-jre' - implementation 'org.springframework.boot:spring-boot-starter-actuator' + compileOnly 'org.springframework.boot:spring-boot-starter-actuator' + compileOnly 'jakarta.validation:jakarta.validation-api:3.1.0' + + // Lombok dependencies + compileOnly "org.projectlombok:lombok:$lombokVersion" + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor:$springBootVersion" + annotationProcessor "org.projectlombok:lombok:$lombokVersion" - compileOnly 'javax.validation:validation-api:2.0.1.Final' - compileOnly 'org.projectlombok:lombok' - annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' - annotationProcessor 'org.projectlombok:lombok' + // Lombok dependencies for test classes + testCompileOnly "org.projectlombok:lombok:$lombokVersion" + testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion" - // Test dependencies - testImplementation 'org.springframework.boot:spring-boot-starter-test' + // Test dependencies + testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion" + testImplementation "org.springframework.boot:spring-boot-starter-web:$springBootVersion" + testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa' testImplementation 'org.springframework.security:spring-security-test' testImplementation 'com.h2database:h2:2.3.232' - testImplementation group: 'com.codeborne', name: 'selenide', version: '7.7.0' - testImplementation group: 'io.github.bonigarcia', name: 'webdrivermanager', version: '5.9.2' +} +tasks.named('bootJar') { + enabled = false } test { @@ -74,21 +82,126 @@ test { } } +tasks.named('jar') { + enabled = true + archiveBaseName.set('ds-spring-ai-client') + archiveClassifier.set('') +} + +def registerJdkTestTask(name, jdkVersion) { + tasks.register(name, Test) { + javaLauncher.set(javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(jdkVersion) + }) + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath + useJUnitPlatform() + testLogging { + events "PASSED", "FAILED", "SKIPPED" + } + doFirst { + println("Running tests with JDK $jdkVersion") + } + } +} + +registerJdkTestTask('testJdk17', 17) +registerJdkTestTask('testJdk21', 21) + + +// Task that runs both test tasks +tasks.register('testAll') { + dependsOn(tasks.named('testJdk17'), tasks.named('testJdk21')) + doFirst { + println("Running tests with both JDK 17 and JDK 21") + } +} + +// Make the default 'test' task depend on 'testAll' +tasks.test { + dependsOn(tasks.named('testAll')) + doFirst { + println("Delegating to 'testAll'") + } + // Prevent the default test behavior + testClassesDirs = files() + classpath = files() +} + + +// Maven Central Publishing Tasks +mavenPublishing { + configure(new JavaLibrary(new JavadocJar.Javadoc(), true)) + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) + signAllPublications() + coordinates("com.digitalsanctuary", "ds-spring-user-framework", project.version) + + pom { + name = "DS Spring User Framework" + description = "Simple SpringBoot User Library built on top of Spring Security." + inceptionYear = "2024" + url = "https://github.com/devondragon/SpringUserFramework" + licenses { + license { + name = "The Apache License, Version 2.0" + url = "http://www.apache.org/licenses/LICENSE-2.0.txt" + distribution = "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + developers { + developer { + id = "devondragon" + name = "Devon Hillard" + url = "https://github.com/devondragon/" + } + } + scm { + url = "https://github.com/devondragon/SpringUserFramework" + connection = "scm:git:git@github.com:devondragon/SpringUserFramework.git" + developerConnection = "scm:git:ssh://git@github.com:devondragon/SpringUserFramework.git" + } + } +} + +tasks.named("publishMavenPublicationToMavenCentralRepository") { + dependsOn("signMavenPublication") +} + +publishing { + repositories { + maven { + name = 'reposiliteRepository' + url = uri('https://reposilite.tr0n.io/private') + credentials(PasswordCredentials) + authentication { + basic(BasicAuthentication) + } + } + // more repositories can go here + } +} + +// Maven Publishing Aliases + +tasks.register("publishReposilite") { + dependsOn("publishMavenPublicationToReposiliteRepository") +} + +tasks.register("publishMavenCentral") { + dependsOn("publishAndReleaseToMavenCentral") +} + +tasks.register("publishLocal") { + dependsOn("publishToMavenLocal") +} -bootJar { - launchScript { - properties 'confFolder': '/opt/app/conf/' - } +task generateAIChangelog(type: Exec) { + def newVersion = project.version + commandLine 'mise', 'x', '--', 'python', 'generate_changelog.py', newVersion } -bootRun { - // Use Spring Boot DevTool only when we run Gradle bootRun task - classpath = sourceSets.main.runtimeClasspath + configurations.developmentOnly - sourceResources sourceSets.main - if (project.hasProperty('profiles')) { - environment SPRING_PROFILES_ACTIVE: profiles - } else { - def profiles = 'local' - environment SPRING_PROFILES_ACTIVE: profiles - } +release { + beforeReleaseBuild.dependsOn generateAIChangelog + // afterReleaseBuild.dependsOn publishReposilite + afterReleaseBuild.dependsOn publishMavenCentral } diff --git a/config/application.properties.example b/config/application.properties.example deleted file mode 100644 index 0e573c0..0000000 --- a/config/application.properties.example +++ /dev/null @@ -1,8 +0,0 @@ -user.mail.fromAddress=your_email_address - -spring.mail.host=email-smtp.us-west-2.amazonaws.com -spring.mail.username=your_aws_smtp_username -spring.mail.password=your_aws_smtp_password - -management.metrics.export.newrelic.apiKey=new_relic_api_key -management.metrics.export.newrelic.accountId=new_relic_account_number diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 91c1ff2..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,74 +0,0 @@ -version: "3" - -services: - myapp-db: - image: mariadb:10.5 - volumes: - - userdb:/var/lib/mysql - environment: - - MYSQL_ROOT_PASSWORD=root - - MYSQL_DATABASE=springuser - - MYSQL_USER=springuser - - MYSQL_PASSWORD=springuser - ports: - - 3306:3306 - - mailserver: - image: docker.io/mailserver/docker-mailserver:latest - hostname: mailserver - domainname: local - container_name: mailserver - env_file: mailserver.env - ports: - - "25:25" - - "143:143" - - "587:587" - - "993:993" - volumes: - - maildata:/var/mail - - mailstate:/var/mail-state - - maillogs:/var/log/mail - - ./config/:/tmp/docker-mailserver/${SELINUX_LABEL} - environment: - - PERMIT_DOCKER=connected-networks - - ONE_DIR=1 - - DMS_DEBUG=1 - - SPOOF_PROTECTION=0 - - REPORT_RECIPIENT=1 - - ENABLE_SPAMASSASSIN=0 - - ENABLE_CLAMAV=0 - - ENABLE_FAIL2BAN=1 - - ENABLE_POSTGREY=0 - - SMTP_ONLY=1 -# restart: always - cap_add: [ "NET_ADMIN", "SYS_PTRACE" ] - - myapp-main: - image: user-framework -# restart: always - volumes: - - appvol:/opt/app - build: - context: . - depends_on: - - myapp-db - - mailserver - ports: - - 8080:8080 - environment: - - spring.datasource.url=jdbc:mysql://myapp-db:3306/springuser?createDatabaseIfNotExist=true - - spring.datasource.username=springuser - - spring.datasource.password=springuser - - SPRING_PROFILES_ACTIVE=dev - - spring.mail.host=mailserver - - spring.mail.properties.mail.smtp.port=25 - - spring.mail.properties.mail.smtp.auth=false - - spring.mail.properties.mail.smtp.starttls.enable=false - - spring.mail.properties.mail.smtp.starttls.required=false - -volumes: - maildata: - mailstate: - maillogs: - userdb: - appvol: diff --git a/generate_changelog.py b/generate_changelog.py new file mode 100644 index 0000000..73f650c --- /dev/null +++ b/generate_changelog.py @@ -0,0 +1,88 @@ +import os +import sys +import subprocess +from openai import OpenAI +from datetime import date + +def get_git_commits(): + # Get the last tag + last_tag = subprocess.check_output( + ["git", "describe", "--tags", "--abbrev=0"], text=True + ).strip() + + # Get commit messages since the last tag + commits = subprocess.check_output( + ["git", "log", f"{last_tag}..HEAD", "--pretty=format:%s"], text=True + ).strip() + + return last_tag, commits.split("\n") + +def generate_changelog(commits): + if not commits: + return "No commits to include in the changelog." + + prompt = f""" + You are a helpful assistant tasked with creating a changelog. Based on these Git commit messages, generate a clear, human-readable changelog: + + Commit messages: + {commits} + + Format the changelog as follows: + ### Features + - List features here + + ### Fixes + - List fixes here + + ### Breaking Changes + - List breaking changes here (if any) + """ + + client = OpenAI( + api_key=os.environ.get("OPENAI_API_TOKEN"), # This is the default and can be omitted + ) + response = client.chat.completions.create( + model="gpt-4", + messages=[ + {"role": "system", "content": "You are a helpful assistant for software development."}, + {"role": "user", "content": prompt}, + ], + ) + return response.choices[0].message.content.strip() + +def update_changelog(version, changelog_content): + changelog_file = "CHANGELOG.md" + today = date.today().strftime("%Y-%m-%d") + new_entry = f"## [{version}] - {today}\n{changelog_content}\n\n" + + if os.path.exists(changelog_file): + with open(changelog_file, "r+") as f: + old_content = f.read() + f.seek(0, 0) + f.write(new_entry + old_content) + else: + with open(changelog_file, "w") as f: + f.write(new_entry) + +if __name__ == "__main__": + last_tag, commits = get_git_commits() + if not commits: + print("No new commits found.") + exit() + + print("Generating changelog...") + changelog_content = generate_changelog("\n".join(commits)) + + print("\nGenerated Changelog:") + print(changelog_content) + + # Check if a version was passed as a command-line argument + if len(sys.argv) > 1: + new_version = sys.argv[1] + else: + # Prompt for a version if none was provided + new_version = input("Enter the new version (e.g., 1.0.0): ").strip() + + update_changelog(new_version, changelog_content) + + print(f"Changelog updated for version {new_version}!") diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..cd92d6b --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +version=3.0.0-SNAPSHOT diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 94113f2..cea7a79 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/mailserver.env b/mailserver.env deleted file mode 100644 index 941bfe7..0000000 --- a/mailserver.env +++ /dev/null @@ -1,446 +0,0 @@ -# ––––––––––––––––––––––––––––––––––––––––––––––– -# ––– Mailserver Environment Variables –––––––––– -# ––––––––––––––––––––––––––––––––––––––––––––––– - -# empty => uses the `hostname` command to get the mail server's canonical hostname -# => Specify a fully-qualified domainname to serve mail for. This is used for many of the config features so if you can't set your hostname (e.g. you're in a container platform that doesn't let you) specify it in this environment variable. -OVERRIDE_HOSTNAME= - -# 0 => Debug disabled -# 1 => Enables debug on startup -DMS_DEBUG=0 - -# critical => Only show critical messages -# error => Only show erroneous output -# **warn** => Show warnings -# info => Normal informational output -# debug => Also show debug messages -SUPERVISOR_LOGLEVEL= - -# 0 => mail state in default directories -# 1 => consolidate all states into a single directory (`/var/mail-state`) to allow persistence using docker volumes -ONE_DIR=1 - -# empty => postmaster@domain.com -# => Specify the postmaster address -POSTMASTER_ADDRESS= - -# Set different options for mynetworks option (can be overwrite in postfix-main.cf) -# **WARNING**: Adding the docker network's gateway to the list of trusted hosts, e.g. using the `network` or -# `connected-networks` option, can create an open relay -# https://github.com/docker-mailserver/docker-mailserver/issues/1405#issuecomment-590106498 -# empty => localhost only -# host => Add docker host (ipv4 only) -# network => Add all docker containers (ipv4 only) -# connected-networks => Add all connected docker networks (ipv4 only) -PERMIT_DOCKER= - -# In case you network interface differs from 'eth0', e.g. when you are using HostNetworking in Kubernetes, -# you can set NETWORK_INTERFACE to whatever interface you want. This interface will then be used. -# - **empty** => eth0 -NETWORK_INTERFACE= - -# empty => modern -# modern => Enables TLSv1.2 and modern ciphers only. (default) -# intermediate => Enables TLSv1, TLSv1.1 and TLSv1.2 and broad compatibility ciphers. -TLS_LEVEL= - -# Configures the handling of creating mails with forged sender addresses. -# -# empty => (not recommended, but default for backwards compatibility reasons) -# Mail address spoofing allowed. Any logged in user may create email messages with a forged sender address. -# See also https://en.wikipedia.org/wiki/Email_spoofing -# 1 => (recommended) Mail spoofing denied. Each user may only send with his own or his alias addresses. -# Addresses with extension delimiters(http://www.postfix.org/postconf.5.html#recipient_delimiter) are not able to send messages. -SPOOF_PROTECTION= - -# Enables the Sender Rewriting Scheme. SRS is needed if your mail server acts as forwarder. See [postsrsd](https://github.com/roehling/postsrsd/blob/master/README.md#sender-rewriting-scheme-crash-course) for further explanation. -# - **0** => Disabled -# - 1 => Enabled -ENABLE_SRS=0 - -# 1 => Enables POP3 service -# empty => disables POP3 -ENABLE_POP3= -ENABLE_CLAMAV=0 - -# If you enable Fail2Ban, don't forget to add the following lines to your `docker-compose.yml`: -# cap_add: -# - NET_ADMIN -# Otherwise, `iptables` won't be able to ban IPs. -ENABLE_FAIL2BAN=0 - -# 1 => Enables Managesieve on port 4190 -# empty => disables Managesieve -ENABLE_MANAGESIEVE= - -# **enforce** => Allow other tests to complete. Reject attempts to deliver mail with a 550 SMTP reply, and log the helo/sender/recipient information. Repeat this test the next time the client connects. -# drop => Drop the connection immediately with a 521 SMTP reply. Repeat this test the next time the client connects. -# ignore => Ignore the failure of this test. Allow other tests to complete. Repeat this test the next time the client connects. This option is useful for testing and collecting statistics without blocking mail. -POSTSCREEN_ACTION=enforce - -# empty => all daemons start -# 1 => only launch postfix smtp -SMTP_ONLY= - -# Please read [the SSL page in the wiki](https://github.com/docker-mailserver/docker-mailserver/wiki/Configure-SSL) for more information. -# -# empty => SSL disabled -# letsencrypt => Enables Let's Encrypt certificates -# custom => Enables custom certificates -# manual => Let's you manually specify locations of your SSL certificates for non-standard cases -# self-signed => Enables self-signed certificates -SSL_TYPE= - -# These are only supported with `SSL_TYPE=manual`. -# Provide the path to your cert and key files that you've mounted access to within the container. -SSL_CERT_PATH= -SSL_KEY_PATH= -# Optional: A 2nd certificate can be supported as fallback (dual cert support), eg ECDSA with an RSA fallback. -# Useful for additional compatibility with older MTA and MUA (eg pre-2015). -SSL_ALT_CERT_PATH= -SSL_ALT_KEY_PATH= - -# Set how many days a virusmail will stay on the server before being deleted -# empty => 7 days -VIRUSMAILS_DELETE_DELAY= - -# This Option is activating the Usage of POSTFIX_DAGENT to specify a lmtp client different from default dovecot socket. -# empty => disabled -# 1 => enabled -ENABLE_POSTFIX_VIRTUAL_TRANSPORT= - -# Enabled by ENABLE_POSTFIX_VIRTUAL_TRANSPORT. Specify the final delivery of postfix -# -# empty => fail -# `lmtp:unix:private/dovecot-lmtp` (use socket) -# `lmtps:inet::` (secure lmtp with starttls, take a look at https://sys4.de/en/blog/2014/11/17/sicheres-lmtp-mit-starttls-in-dovecot/) -# `lmtp::2003` (use kopano as mailstore) -# etc. -POSTFIX_DAGENT= - -# Set the mailbox size limit for all users. If set to zero, the size will be unlimited (default). -# -# empty => 0 -POSTFIX_MAILBOX_SIZE_LIMIT= - -# Set the message size limit for all users. If set to zero, the size will be unlimited (not recommended!) -# -# empty => 10240000 (~10 MB) -POSTFIX_MESSAGE_SIZE_LIMIT= - -# Enables regular pflogsumm mail reports. -# This is a new option. The old REPORT options are still supported for backwards compatibility. If this is not set and reports are enabled with the old options, logrotate will be used. -# -# not set => No report -# daily_cron => Daily report for the previous day -# logrotate => Full report based on the mail log when it is rotated -PFLOGSUMM_TRIGGER= - -# Recipient address for pflogsumm reports. -# -# not set => Use REPORT_RECIPIENT or POSTMASTER_ADDRESS -# => Specify the recipient address(es) -PFLOGSUMM_RECIPIENT= - -# From address for pflogsumm reports. -# -# not set => Use REPORT_SENDER or POSTMASTER_ADDRESS -# => Specify the sender address -PFLOGSUMM_SENDER= - -# Interval for logwatch report. -# -# none => No report is generated -# daily => Send a daily report -# weekly => Send a report every week -LOGWATCH_INTERVAL= - -# Recipient address for logwatch reports if they are enabled. -# -# not set => Use REPORT_RECIPIENT or POSTMASTER_ADDRESS -# => Specify the recipient address(es) -LOGWATCH_RECIPIENT= - -# Enables a report being sent (created by pflogsumm) on a regular basis. (deprecated) -# **0** => Report emails are disabled -# 1 => Using POSTMASTER_ADDRESS as the recipient -# => Specify the recipient address -REPORT_RECIPIENT=0 - -# Change the sending address for mail report (deprecated) -# **empty** => mailserver-report@hostname -# => Specify the report sender (From) address -REPORT_SENDER= - -# Changes the interval in which a report is being sent. (deprecated) -# **daily** => Send a daily report -# weekly => Send a report every week -# monthly => Send a report every month -# -# Note: This Variable actually controls logrotate inside the container and rotates the log depending on this setting. The main log output is still available in its entirety via `docker logs mail` (Or your respective container name). If you want to control logrotation for the docker generated logfile see: [Docker Logging Drivers](https://docs.docker.com/config/containers/logging/configure/) -REPORT_INTERVAL=daily - -# Choose TCP/IP protocols to use -# **all** => All possible protocols. -# ipv4 => Use only IPv4 traffic. Most likely you want this behind Docker. -# ipv6 => Use only IPv6 traffic. -# -# Note: More details in http://www.postfix.org/postconf.5.html#inet_protocols -POSTFIX_INET_PROTOCOLS=all - -# ––––––––––––––––––––––––––––––––––––––––––––––– -# ––– Spamassassin Section –––––––––––––––––––––– -# ––––––––––––––––––––––––––––––––––––––––––––––– - -ENABLE_SPAMASSASSIN=0 - -# deliver spam messages in the inbox (eventually tagged using SA_SPAM_SUBJECT) -SPAMASSASSIN_SPAM_TO_INBOX=1 - -# spam messages will be moved in the Junk folder (SPAMASSASSIN_SPAM_TO_INBOX=1 required) -MOVE_SPAM_TO_JUNK=1 - -# add spam info headers if at, or above that level: -SA_TAG=2.0 - -# add 'spam detected' headers at that level -SA_TAG2=6.31 - -# triggers spam evasive actions -SA_KILL=6.31 - -# add tag to subject if spam detected -SA_SPAM_SUBJECT=***SPAM***** - -# ––––––––––––––––––––––––––––––––––––––––––––––– -# ––– Fetchmail Section ––––––––––––––––––––––––– -# ––––––––––––––––––––––––––––––––––––––––––––––– - -ENABLE_FETCHMAIL=0 - -# The interval to fetch mail in seconds -FETCHMAIL_POLL=300 - -# ––––––––––––––––––––––––––––––––––––––––––––––– -# ––– LDAP Section –––––––––––––––––––––––––––––– -# ––––––––––––––––––––––––––––––––––––––––––––––– - -# A second container for the ldap service is necessary (i.e. https://github.com/osixia/docker-openldap) -# For preparing the ldap server to use in combination with this container this article may be helpful: http://acidx.net/wordpress/2014/06/installing-a-mailserver-with-postfix-dovecot-sasl-ldap-roundcube/ - -# empty => LDAP authentification is disabled -# 1 => LDAP authentification is enabled -ENABLE_LDAP= - -# empty => no -# yes => LDAP over TLS enabled for Postfix -LDAP_START_TLS= - -# If you going to use the mailserver in combination with docker-compose you can set the service name here -# empty => mail.domain.com -# Specify the dns-name/ip-address where the ldap-server -LDAP_SERVER_HOST= - -# empty => ou=people,dc=domain,dc=com -# => e.g. LDAP_SEARCH_BASE=dc=mydomain,dc=local -LDAP_SEARCH_BASE= - -# empty => cn=admin,dc=domain,dc=com -# => take a look at examples of SASL_LDAP_BIND_DN -LDAP_BIND_DN= - -# empty** => admin -# => Specify the password to bind against ldap -LDAP_BIND_PW= - -# e.g. `"(&(mail=%s)(mailEnabled=TRUE))"` -# => Specify how ldap should be asked for users -LDAP_QUERY_FILTER_USER= - -# e.g. `"(&(mailGroupMember=%s)(mailEnabled=TRUE))"` -# => Specify how ldap should be asked for groups -LDAP_QUERY_FILTER_GROUP= - -# e.g. `"(&(mailAlias=%s)(mailEnabled=TRUE))"` -# => Specify how ldap should be asked for aliases -LDAP_QUERY_FILTER_ALIAS= - -# e.g. `"(&(|(mail=*@%s)(mailalias=*@%s)(mailGroupMember=*@%s))(mailEnabled=TRUE))"` -# => Specify how ldap should be asked for domains -LDAP_QUERY_FILTER_DOMAIN= - -# ––––––––––––––––––––––––––––––––––––––––––––––– -# ––– Dovecot Section ––––––––––––––––––––––––––– -# ––––––––––––––––––––––––––––––––––––––––––––––– - -# empty => no -# yes => LDAP over TLS enabled for Dovecot -DOVECOT_TLS= - -# e.g. `"(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))"` -DOVECOT_USER_FILTER= - -# e.g. `"(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))"` -DOVECOT_PASS_FILTER= - -# Define the mailbox format to be used -# default is maildir, supported values are: sdbox, mdbox, maildir -DOVECOT_MAILBOX_FORMAT=maildir - -# empty => no -# yes => Allow bind authentication for LDAP -# https://wiki.dovecot.org/AuthDatabase/LDAP/AuthBinds -DOVECOT_AUTH_BIND= - -# ––––––––––––––––––––––––––––––––––––––––––––––– -# ––– Postgrey Section –––––––––––––––––––––––––– -# ––––––––––––––––––––––––––––––––––––––––––––––– - -ENABLE_POSTGREY=0 -# greylist for N seconds -POSTGREY_DELAY=300 -# delete entries older than N days since the last time that they have been seen -POSTGREY_MAX_AGE=35 -# response when a mail is greylisted -POSTGREY_TEXT=Delayed by Postgrey -# whitelist host after N successful deliveries (N=0 to disable whitelisting) -POSTGREY_AUTO_WHITELIST_CLIENTS=5 - -# ––––––––––––––––––––––––––––––––––––––––––––––– -# ––– SASL Section –––––––––––––––––––––––––––––– -# ––––––––––––––––––––––––––––––––––––––––––––––– - -ENABLE_SASLAUTHD=0 - -# empty => pam -# `ldap` => authenticate against ldap server -# `shadow` => authenticate against local user db -# `mysql` => authenticate against mysql db -# `rimap` => authenticate against imap server -# NOTE: can be a list of mechanisms like pam ldap shadow -SASLAUTHD_MECHANISMS= - -# empty => None -# e.g. with SASLAUTHD_MECHANISMS rimap you need to specify the ip-address/servername of the imap server ==> xxx.xxx.xxx.xxx -SASLAUTHD_MECH_OPTIONS= - -# empty => localhost -SASLAUTHD_LDAP_SERVER= - -# empty or 0 => `ldap://` will be used -# 1 => `ldaps://` will be used -SASLAUTHD_LDAP_SSL= - -# empty => anonymous bind -# specify an object with priviliges to search the directory tree -# e.g. active directory: SASLAUTHD_LDAP_BIND_DN=cn=Administrator,cn=Users,dc=mydomain,dc=net -# e.g. openldap: SASLAUTHD_LDAP_BIND_DN=cn=admin,dc=mydomain,dc=net -SASLAUTHD_LDAP_BIND_DN= - -# empty => anonymous bind -SASLAUTHD_LDAP_PASSWORD= - -# empty => Reverting to SASLAUTHD_MECHANISMS pam -# specify the search base -SASLAUTHD_LDAP_SEARCH_BASE= - -# empty => default filter `(&(uniqueIdentifier=%u)(mailEnabled=TRUE))` -# e.g. for active directory: `(&(sAMAccountName=%U)(objectClass=person))` -# e.g. for openldap: `(&(uid=%U)(objectClass=person))` -SASLAUTHD_LDAP_FILTER= - -# empty => no -# yes => LDAP over TLS enabled for SASL -# Must not be used together with SASLAUTHD_LDAP_SSL=1_ -SASLAUTHD_LDAP_START_TLS= - -# empty => no -# yes => Require and verify server certificate -# If yes you must/could specify SASLAUTHD_LDAP_TLS_CACERT_FILE or SASLAUTHD_LDAP_TLS_CACERT_DIR. -SASLAUTHD_LDAP_TLS_CHECK_PEER= - -# File containing CA (Certificate Authority) certificate(s). -# empty => Nothing is added to the configuration -# Any value => Fills the `ldap_tls_cacert_file` option -SASLAUTHD_LDAP_TLS_CACERT_FILE= - -# Path to directory with CA (Certificate Authority) certificates. -# empty => Nothing is added to the configuration -# Any value => Fills the `ldap_tls_cacert_dir` option -SASLAUTHD_LDAP_TLS_CACERT_DIR= - -# Specify what password attribute to use for password verification. -# empty => Nothing is added to the configuration but the documentation says it is `userPassword` by default. -# Any value => Fills the `ldap_password_attr` option -SASLAUTHD_LDAP_PASSWORD_ATTR= - -# empty => No sasl_passwd will be created -# string => `/etc/postfix/sasl_passwd` will be created with the string as password -SASL_PASSWD= - -# empty => `bind` will be used as a default value -# `fastbind` => The fastbind method is used -# `custom` => The custom method uses userPassword attribute to verify the password -SASLAUTHD_LDAP_AUTH_METHOD= - -# Specify the authentication mechanism for SASL bind -# empty => Nothing is added to the configuration -# Any value => Fills the `ldap_mech` option -SASLAUTHD_LDAP_MECH= - -# ––––––––––––––––––––––––––––––––––––––––––––––– -# ––– SRS Section ––––––––––––––––––––––––––––––– -# ––––––––––––––––––––––––––––––––––––––––––––––– - -# envelope_sender => Rewrite only envelope sender address (default) -# header_sender => Rewrite only header sender (not recommended) -# envelope_sender,header_sender => Rewrite both senders -# An email has an "envelope" sender (indicating the sending server) and a -# "header" sender (indicating who sent it). More strict SPF policies may require -# you to replace both instead of just the envelope sender. -SRS_SENDER_CLASSES=envelope_sender - -# empty => Envelope sender will be rewritten for all domains -# provide comma separated list of domains to exclude from rewriting -SRS_EXCLUDE_DOMAINS= - -# empty => generated when the image is built -# provide a secret to use in base64 -# you may specify multiple keys, comma separated. the first one is used for -# signing and the remaining will be used for verification. this is how you -# rotate and expire keys -SRS_SECRET= - -# ––––––––––––––––––––––––––––––––––––––––––––––– -# ––– Default Relay Host Section –––––––––––––––– -# ––––––––––––––––––––––––––––––––––––––––––––––– - -# Setup relaying all mail through a default relay host -# -# empty => don't configure default relay host -# default host and optional port to relay all mail through -DEFAULT_RELAY_HOST= - -# ––––––––––––––––––––––––––––––––––––––––––––––– -# ––– Multi-Domain Relay Section –––––––––––––––– -# ––––––––––––––––––––––––––––––––––––––––––––––– - -# Setup relaying for multiple domains based on the domain name of the sender -# optionally uses usernames and passwords in postfix-sasl-password.cf and relay host mappings in postfix-relaymap.cf -# -# empty => don't configure relay host -# default host to relay mail through -RELAY_HOST= - -# empty => 25 -# default port to relay mail -RELAY_PORT=25 - -# empty => no default -# default relay username (if no specific entry exists in postfix-sasl-password.cf) -RELAY_USER= - -# empty => no default -# password for default relay user -RELAY_PASSWORD= diff --git a/settings.gradle b/settings.gradle index 9d37ea2..c7f1de0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'user' +rootProject.name = 'ds-spring-user-framework' diff --git a/src/main/java/com/digitalsanctuary/spring/user/UserApplication.java b/src/main/java/com/digitalsanctuary/spring/user/UserApplication.java deleted file mode 100644 index 9f8864b..0000000 --- a/src/main/java/com/digitalsanctuary/spring/user/UserApplication.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.digitalsanctuary.spring.user; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.scheduling.annotation.EnableScheduling; -import lombok.extern.slf4j.Slf4j; - -/** - * The Class UserApplication. Basic Spring Boot Application Setup. Adds Async support and Scheduling support to the default Spring Boot stack. - */ -@Slf4j -@EnableAsync -@EnableScheduling -@SpringBootApplication -public class UserApplication { - - /** - * The main method. - * - * @param args the arguments - */ - public static void main(String[] args) { - log.info("Starting UserApplication..."); - SpringApplication.run(UserApplication.class, args); - log.info("UserApplication started."); - } - -} diff --git a/src/main/java/com/digitalsanctuary/spring/user/UserConfiguration.java b/src/main/java/com/digitalsanctuary/spring/user/UserConfiguration.java new file mode 100644 index 0000000..50c1c95 --- /dev/null +++ b/src/main/java/com/digitalsanctuary/spring/user/UserConfiguration.java @@ -0,0 +1,36 @@ +package com.digitalsanctuary.spring.user; + +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; + +/** + * The UserConfiguration class is a Spring Boot configuration class that provides configuration for the DigitalSanctuary Spring Boot User Framework + * Library. This class is used to configure the user framework library, including enabling asynchronous processing and scheduling, and scanning for + * components and repositories. + */ +@Slf4j +@Configuration +@EnableAsync +@EnableScheduling +@ComponentScan(basePackages = "com.digitalsanctuary.spring.user") +@EnableJpaRepositories(basePackages = "com.digitalsanctuary.spring.user.persistence.repository") +@EntityScan(basePackages = "com.digitalsanctuary.spring.user.persistence.model") +public class UserConfiguration { + + /** + * Method executed after the bean initialization. + *

+ * This method logs a message indicating that the DigitalSanctuary Spring Boot User Framework LIbrary has been loaded. + *

+ */ + @PostConstruct + public void onStartup() { + log.info("DigitalSanctuary SpringBoot User Framework Library loaded"); + } +} diff --git a/src/main/java/com/digitalsanctuary/spring/user/api/UserAPI.java b/src/main/java/com/digitalsanctuary/spring/user/api/UserAPI.java index 9413416..8eccdd5 100644 --- a/src/main/java/com/digitalsanctuary/spring/user/api/UserAPI.java +++ b/src/main/java/com/digitalsanctuary/spring/user/api/UserAPI.java @@ -1,8 +1,7 @@ package com.digitalsanctuary.spring.user.api; import java.util.Locale; -import java.util.Optional; -import javax.validation.Valid; +import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.MessageSource; @@ -10,17 +9,19 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.*; -import com.digitalsanctuary.spring.user.dto.PasswordDto; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.digitalsanctuary.spring.user.audit.AuditEvent; import com.digitalsanctuary.spring.user.dto.UserDto; -import com.digitalsanctuary.spring.user.event.AuditEvent; import com.digitalsanctuary.spring.user.event.OnRegistrationCompleteEvent; import com.digitalsanctuary.spring.user.exceptions.UserAlreadyExistException; import com.digitalsanctuary.spring.user.persistence.model.User; import com.digitalsanctuary.spring.user.service.DSUserDetails; import com.digitalsanctuary.spring.user.service.UserEmailService; import com.digitalsanctuary.spring.user.service.UserService; -import com.digitalsanctuary.spring.user.service.UserService.TokenValidationResult; import com.digitalsanctuary.spring.user.util.JSONResponse; import com.digitalsanctuary.spring.user.util.UserUtils; import jakarta.servlet.ServletException; @@ -29,322 +30,254 @@ import lombok.extern.slf4j.Slf4j; /** - * The UserAPI is the Controller for the REST API endpoints for the user management functionality. By default these endpoints are defined under the - * "/user" prefix. + * REST controller for managing user-related operations. This class handles user registration, account deletion, and other user-related endpoints. */ @Slf4j @RequiredArgsConstructor @RestController @RequestMapping(path = "/user", produces = "application/json") public class UserAPI { + private final UserService userService; private final UserEmailService userEmailService; private final MessageSource messages; private final ApplicationEventPublisher eventPublisher; - // URIs configured in application.properties - /** The registration pending URI. */ @Value("${user.security.registrationPendingURI}") private String registrationPendingURI; - /** The registration success URI. */ @Value("${user.security.registrationSuccessURI}") private String registrationSuccessURI; - /** The registration new verification URI. */ - @Value("${user.security.registrationNewVerificationURI}") - private String registrationNewVerificationURI; - - /** The forgot password pending URI. */ @Value("${user.security.forgotPasswordPendingURI}") private String forgotPasswordPendingURI; - /** The forgot password change URI. */ - @Value("${user.security.forgotPasswordChangeURI}") - private String forgotPasswordChangeURI; - @Value("${user.actuallyDeleteAccount:false}") private boolean actuallyDeleteAccount; - /** - * Register a new user account. + * Registers a new user account. * - * @param userDto the userDTO object is used for passing the form data in - * @param request the request - * @return A JSONResponse. In addition to success status, message, and code in the response body, this method also returns a 200 status on - * success, a 409 status if the email address is already in use, and a 502 if there is an error. + * @param userDto the user data transfer object containing user details + * @param request the HTTP servlet request + * @return a ResponseEntity containing a JSONResponse with the registration result */ @PostMapping("/registration") - public ResponseEntity registerUserAccount(@Valid final UserDto userDto, final HttpServletRequest request) { - log.debug("Registering user account with information: {}", userDto); - - User registeredUser = null; + public ResponseEntity registerUserAccount(@Valid @RequestBody UserDto userDto, HttpServletRequest request) { try { - registeredUser = userService.registerNewUserAccount(userDto); - - eventPublisher.publishEvent(OnRegistrationCompleteEvent.builder().user(registeredUser).locale(request.getLocale()) - .appUrl(UserUtils.getAppUrl(request)).build()); - - AuditEvent registrationAuditEvent = AuditEvent.builder().source(this).user(registeredUser).sessionId(request.getSession().getId()) - .ipAddress(UserUtils.getClientIP(request)).userAgent(request.getHeader("User-Agent")).action("Registration") - .actionStatus("Success").message("Registration Successful").build(); - eventPublisher.publishEvent(registrationAuditEvent); - } catch (UserAlreadyExistException uaee) { - log.warn("UserAPI.registerUserAccount:" + "UserAlreadyExistException on registration with email: {}!", userDto.getEmail()); - AuditEvent registrationAuditEvent = AuditEvent.builder().source(this).user(registeredUser).sessionId(request.getSession().getId()) - .ipAddress(UserUtils.getClientIP(request)).userAgent(request.getHeader("User-Agent")).action("Registration") - .actionStatus("Failure").message("User Already Exists").build(); - - eventPublisher.publishEvent(registrationAuditEvent); - - return new ResponseEntity( - JSONResponse.builder().success(false).code(02).message("An account already exists for the email address").build(), - HttpStatus.CONFLICT); - } catch (Exception e) { - log.error("UserAPI.registerUserAccount:" + "Exception!", e); - AuditEvent registrationAuditEvent = AuditEvent.builder().source(this).user(registeredUser).sessionId(request.getSession().getId()) - .ipAddress(UserUtils.getClientIP(request)).userAgent(request.getHeader("User-Agent")).action("Registration") - .actionStatus("Failure").message(e.getMessage()).build(); - - eventPublisher.publishEvent(registrationAuditEvent); - - return new ResponseEntity(JSONResponse.builder().success(false).redirectUrl(null).code(05).message("System Error!").build(), - HttpStatus.INTERNAL_SERVER_ERROR); - } - - // If there were no exceptions then the registration was a success! - String nextURL = registrationPendingURI; - if (registeredUser.isEnabled()) { - log.debug("UserAPI.registerUserAccount:" + "User is already enabled, skipping email verification and auto-logging them in."); - nextURL = registrationSuccessURI; - // Auto-login the user after registration (this is a UX choice, which is why it is in the controller) - userService.authWithoutPassword(registeredUser); + validateUserDto(userDto); + User registeredUser = userService.registerNewUserAccount(userDto); + publishRegistrationEvent(registeredUser, request); + logAuditEvent("Registration", "Success", "Registration Successful", registeredUser, request); + + String nextURL = registeredUser.isEnabled() ? handleAutoLogin(registeredUser) : registrationPendingURI; + + return buildSuccessResponse("Registration Successful!", nextURL); + } catch (UserAlreadyExistException ex) { + log.warn("User already exists with email: {}", userDto.getEmail()); + logAuditEvent("Registration", "Failure", "User Already Exists", null, request); + return buildErrorResponse("An account already exists for the email address", 2, HttpStatus.CONFLICT); + } catch (Exception ex) { + log.error("Unexpected error during registration.", ex); + logAuditEvent("Registration", "Failure", ex.getMessage(), null, request); + return buildErrorResponse("System Error!", 5, HttpStatus.INTERNAL_SERVER_ERROR); } - return new ResponseEntity( - JSONResponse.builder().success(true).redirectUrl(nextURL).code(0).message("Registration Successful!").build(), HttpStatus.OK); } /** - * Re-send registration verification token email. + * Resends the registration token. This is used when the user did not receive the initial registration email. * - * @param userDto the userDTO for passing in the email address from the form - * @param request the request - * @return the generic response + * @param userDto the user data transfer object containing user details + * @param request the HTTP servlet request + * @return a ResponseEntity containing a JSONResponse with the registration result */ @PostMapping("/resendRegistrationToken") - public ResponseEntity resendRegistrationToken(@Valid final UserDto userDto, final HttpServletRequest request) { - log.debug("UserAPI.resendRegistrationToken:" + "email: {}", userDto.getEmail()); - - // Lookup User by email + public ResponseEntity resendRegistrationToken(@Valid @RequestBody UserDto userDto, HttpServletRequest request) { User user = userService.findUserByEmail(userDto.getEmail()); - log.debug("UserAPI.resendRegistrationToken:" + "user: {}", user); - // If user exists if (user != null) { - // If user is enabled if (user.isEnabled()) { - log.debug("UserAPI.resendRegistrationToken:" + "user is already enabled."); - // Send response with message and recommendation to login/forgot password - return new ResponseEntity(JSONResponse.builder().success(false).code(1).message("Account is already verified.").build(), - HttpStatus.CONFLICT); - } else { - // Else send new token email - log.debug("UserAPI.resendRegistrationToken:" + "sending a new verification token email."); - String appUrl = UserUtils.getAppUrl(request); - userEmailService.sendRegistrationVerificationEmail(user, appUrl); - // Return happy path response - AuditEvent resendRegTokenAuditEvent = AuditEvent.builder().source(this).user(user).sessionId(request.getSession().getId()) - .ipAddress(UserUtils.getClientIP(request)).userAgent(request.getHeader("User-Agent")).action("Resend Reg Token") - .actionStatus("Success").message("Success").build(); - - eventPublisher.publishEvent(resendRegTokenAuditEvent); - return new ResponseEntity(JSONResponse.builder().success(true).redirectUrl(registrationPendingURI).code(0) - .message("Verification Email Resent Successfully!").build(), HttpStatus.OK); + return buildErrorResponse("Account is already verified.", 1, HttpStatus.CONFLICT); } + userEmailService.sendRegistrationVerificationEmail(user, UserUtils.getAppUrl(request)); + logAuditEvent("Resend Reg Token", "Success", "Verification Email Resent", user, request); + return buildSuccessResponse("Verification Email Resent Successfully!", registrationPendingURI); } - // Return generic error response (don't leak too much info) - return new ResponseEntity(JSONResponse.builder().success(false).code(2).message("System Error!").build(), - HttpStatus.INTERNAL_SERVER_ERROR); + return buildErrorResponse("System Error!", 2, HttpStatus.INTERNAL_SERVER_ERROR); } + /** + * Updates the user's password. This is used when the user is logged in and wants to change their password. + * + * @param userDetails the authenticated user details + * @param userDto the user data transfer object containing user details + * @param request the HTTP servlet request + * @param locale the locale + * @return a ResponseEntity containing a JSONResponse with the password update result + */ @PostMapping("/updateUser") - public ResponseEntity updateUserAccount(@AuthenticationPrincipal DSUserDetails userDetails, @Valid final UserDto userDto, - final HttpServletRequest request, final Locale locale) { - log.debug("UserAPI.updateUserAccount:" + "called with userDetails: {} and userDto: {}", userDetails, userDto); - // If the userDetails is not available, or if the user is not logged in, log an error and return a failure. - if (userDetails == null || SecurityContextHolder.getContext().getAuthentication() == null - || !SecurityContextHolder.getContext().getAuthentication().isAuthenticated()) { - log.error("UserAPI.updateUserAccount:" + "updateUser called without logged in user state!"); - return new ResponseEntity(JSONResponse.builder().success(false).message("User Not Logged In!").build(), HttpStatus.OK); - } - + public ResponseEntity updateUserAccount(@AuthenticationPrincipal DSUserDetails userDetails, @Valid @RequestBody UserDto userDto, + HttpServletRequest request, Locale locale) { + validateAuthenticatedUser(userDetails); User user = userDetails.getUser(); - user.setFirstName(userDto.getFirstName()); user.setLastName(userDto.getLastName()); userService.saveRegisteredUser(user); - AuditEvent userUpdateAuditEvent = AuditEvent.builder().source(this).user(userDetails.getUser()).sessionId(request.getSession().getId()) - .ipAddress(UserUtils.getClientIP(request)).userAgent(request.getHeader("User-Agent")).action("ProfileUpdate").actionStatus("Success") - .message("Success").build(); - - eventPublisher.publishEvent(userUpdateAuditEvent); - + logAuditEvent("ProfileUpdate", "Success", "User profile updated", user, request); - return new ResponseEntity( - JSONResponse.builder().success(true).message(messages.getMessage("message.updateUserSuccess", null, locale) + "

").build(), - HttpStatus.OK); + return buildSuccessResponse(messages.getMessage("message.update-user.success", null, locale), null); } /** - * Start of the forgot password flow. This API takes in an email address and, if the user exists, will send a password reset token email to them. + * This is used when the user has forgotten their password and wants to reset their password. This will send an email to the user with a link to + * reset their password. * - * @param userDto the userDTO for passing in the email address from the form - * @param request the request - * @return a generic success response, so as to not leak information about accounts existing or not. + * @param userDto the user data transfer object containing user details + * @param request the HTTP servlet request + * @return a ResponseEntity containing a JSONResponse with the password reset email send result */ @PostMapping("/resetPassword") - public ResponseEntity resetPassword(@Valid final UserDto userDto, final HttpServletRequest request) { - log.debug("UserAPI.resetPassword:" + "email: {}", userDto.getEmail()); - - // Lookup User by email + public ResponseEntity resetPassword(@Valid @RequestBody UserDto userDto, HttpServletRequest request) { User user = userService.findUserByEmail(userDto.getEmail()); - log.debug("UserAPI.resendRegistrationToken:" + "user: {}", user); - if (user != null) { - String appUrl = UserUtils.getAppUrl(request); - userEmailService.sendForgotPasswordVerificationEmail(user, appUrl); - - AuditEvent resetPasswordAuditEvent = - AuditEvent.builder().source(this).user(user).sessionId(request.getSession().getId()).ipAddress(UserUtils.getClientIP(request)) - .userAgent(request.getHeader("User-Agent")).action("Reset Password").actionStatus("Success").message("Success").build(); - - eventPublisher.publishEvent(resetPasswordAuditEvent); - - } else { - AuditEvent resetPasswordAuditEvent = AuditEvent.builder().source(this).user(user).sessionId(request.getSession().getId()) - .ipAddress(UserUtils.getClientIP(request)).userAgent(request.getHeader("User-Agent")).action("Reset Password") - .actionStatus("Failure").message("Invalid EMail Submitted").extraData("Email submitted: " + userDto.getEmail()).build(); - eventPublisher.publishEvent(resetPasswordAuditEvent); + userEmailService.sendForgotPasswordVerificationEmail(user, UserUtils.getAppUrl(request)); + logAuditEvent("Reset Password", "Success", "Password reset email sent", user, request); } - - return new ResponseEntity(JSONResponse.builder().success(true).redirectUrl(forgotPasswordPendingURI) - .message("If account exists, password reset email has been sent!").build(), HttpStatus.OK); + return buildSuccessResponse("If account exists, password reset email has been sent!", forgotPasswordPendingURI); } /** - * Saves a new password from a password reset flow based on a password reset token. + * Deletes the user's account. This is used when the user wants to delete their account. This will either delete the account or disable it based + * on the configuration of the actuallyDeleteAccount property. After the account is disabled or deleted, the user will be logged out. * - * @param locale the locale - * @param passwordDto the password dto - * @param request the request - * @return the generic response + * @param userDetails the authenticated user details + * @param request the HTTP servlet request + * @return a ResponseEntity containing a JSONResponse with the account deletion result */ - @PostMapping("/savePassword") - public ResponseEntity savePassword(@Valid PasswordDto passwordDto, final HttpServletRequest request, final Locale locale) { - log.debug("UserAPI.savePassword:" + "called with passwordDto: {}", passwordDto); - - final TokenValidationResult validationResult = userService.validatePasswordResetToken(passwordDto.getToken()); - log.debug("UserAPI.savePassword:" + "result: {}", validationResult); - if (validationResult == TokenValidationResult.VALID) { - Optional user = userService.getUserByPasswordResetToken(passwordDto.getToken()); - if (user.isPresent()) { - userService.changeUserPassword(user.get(), passwordDto.getNewPassword()); - log.debug("UserAPI.savePassword:" + "password updated!"); - - AuditEvent savePasswordAuditEvent = AuditEvent.builder().source(this).user(user.get()).sessionId(request.getSession().getId()) - .ipAddress(UserUtils.getClientIP(request)).userAgent(request.getHeader("User-Agent")).action("Reset Save Password") - .actionStatus("Success").message("Success").build(); - eventPublisher.publishEvent(savePasswordAuditEvent); + @DeleteMapping("/deleteAccount") + public ResponseEntity deleteAccount(@AuthenticationPrincipal DSUserDetails userDetails, HttpServletRequest request) { + validateAuthenticatedUser(userDetails); + User user = userDetails.getUser(); - // In this case we are returning a success, with multiple messages designed to be displayed on-page, - // instead of a redirect URL like most of the other calls. - return new ResponseEntity( - JSONResponse.builder().success(true).message(messages.getMessage("message.resetPasswordSuccess", null, locale)) - .message("Login").build(), - HttpStatus.OK); - } else { - log.debug("UserAPI.savePassword:" + "user could not be found!"); - return new ResponseEntity( - JSONResponse.builder().success(false).code(1).message(messages.getMessage("message.error", null, locale)).build(), - HttpStatus.OK); - } + if (actuallyDeleteAccount) { + userService.deleteUser(user); } else { - return new ResponseEntity( - JSONResponse.builder().success(false).code(2).message(messages.getMessage("message.error", null, locale)).build(), HttpStatus.OK); + user.setEnabled(false); + userService.saveRegisteredUser(user); } - } + logoutUser(request); + return buildSuccessResponse("Account Deleted", null); + } + // Helper Methods /** - * Updates a user's password. + * Validates the user data transfer object. * - * @param locale the locale - * @param passwordDto the password dto - * @return the generic response + * @param userDto the user data transfer object */ - // Change user password - @PostMapping("/updatePassword") - public ResponseEntity changeUserPassword(@AuthenticationPrincipal DSUserDetails userDetails, final Locale locale, - @Valid PasswordDto passwordDto, final HttpServletRequest request) { - if (userDetails == null || userDetails.getUser() == null) { - log.error("UserAPI.changeUserPassword:" + "changeUserPassword called with null userDetails or user."); - return new ResponseEntity( - JSONResponse.builder().success(false).code(2).message(messages.getMessage("message.error", null, locale)).build(), - HttpStatus.INTERNAL_SERVER_ERROR); + private void validateUserDto(UserDto userDto) { + if (isNullOrEmpty(userDto.getEmail())) { + throw new IllegalArgumentException("Email is required."); } - final User user = userDetails.getUser(); - // Check to see if the provided old password matches the current password - if (!userService.checkIfValidOldPassword(user, passwordDto.getOldPassword())) { - return new ResponseEntity(JSONResponse.builder().success(false).code(1).message("Invalid Old Password").build(), - HttpStatus.UNAUTHORIZED); - + if (isNullOrEmpty(userDto.getPassword())) { + throw new IllegalArgumentException("Password is required."); } - userService.changeUserPassword(user, passwordDto.getNewPassword()); - - AuditEvent updatePasswordAuditEvent = - AuditEvent.builder().source(this).user(user).sessionId(request.getSession().getId()).ipAddress(UserUtils.getClientIP(request)) - .userAgent(request.getHeader("User-Agent")).action("Update Save Password").actionStatus("Success").message("Success").build(); - - eventPublisher.publishEvent(updatePasswordAuditEvent); - - return new ResponseEntity( - JSONResponse.builder().success(true).code(0).message(messages.getMessage("message.updatePasswordSuccess", null, locale)).build(), - HttpStatus.OK); } /** - * Deletes the current user's account. + * Validates the authenticated user. * - * @param locale the locale - * @param request the request - * @return the generic response + * @param userDetails the authenticated user details */ - @DeleteMapping("/deleteAccount") - public ResponseEntity deleteAccount(@AuthenticationPrincipal DSUserDetails userDetails, final Locale locale, - final HttpServletRequest request) { - + private void validateAuthenticatedUser(DSUserDetails userDetails) { if (userDetails == null || userDetails.getUser() == null) { - log.error("UserAPI.deleteAccount:" + "deleteAccount called with null userDetails or user."); - return new ResponseEntity( - JSONResponse.builder().success(false).code(2).message(messages.getMessage("message.error", null, locale)).build(), - HttpStatus.INTERNAL_SERVER_ERROR); + throw new SecurityException("User not logged in."); } - final User user = userDetails.getUser(); + } - if (actuallyDeleteAccount) { - userService.deleteUser(user); - } else { - user.setEnabled(false); - userService.saveRegisteredUser(user); - } + /** + * Handles the auto login of the user after registration. + * + * @param user the registered user + * @return the URI to redirect to after registration + */ + private String handleAutoLogin(User user) { + userService.authWithoutPassword(user); + return registrationSuccessURI; + } + + /** + * Logs out the user. + * + * @param request the HTTP servlet request + */ + private void logoutUser(HttpServletRequest request) { try { SecurityContextHolder.clearContext(); request.logout(); } catch (ServletException e) { - log.warn("UserAPI.deleteAccount:" + "Exception on logout!", e); + log.warn("Logout failed during account deletion.", e); } + } + + /** + * Publishes a registration event. + * + * @param user the registered user + * @param request the HTTP servlet request + */ + private void publishRegistrationEvent(User user, HttpServletRequest request) { + String appUrl = request.getContextPath(); + eventPublisher.publishEvent(new OnRegistrationCompleteEvent(user, request.getLocale(), appUrl)); + } + + /** + * Logs an audit event. + * + * @param action the action performed + * @param status the status of the action + * @param message the message describing the action + * @param user the user involved in the action + * @param request the HTTP servlet request + */ + private void logAuditEvent(String action, String status, String message, User user, HttpServletRequest request) { + AuditEvent event = + AuditEvent.builder().source(this).user(user).sessionId(request.getSession().getId()).ipAddress(UserUtils.getClientIP(request)) + .userAgent(request.getHeader("User-Agent")).action(action).actionStatus(status).message(message).build(); + eventPublisher.publishEvent(event); + } - return new ResponseEntity(JSONResponse.builder().success(true).message("Account Deleted").build(), HttpStatus.OK); + /** + * Checks if a string is null or empty. + * + * @param value + * @return true if the string is null or empty, false otherwise + */ + private boolean isNullOrEmpty(String value) { + return value == null || value.isEmpty(); + } + + /** + * Builds an error response. + * + * @param message + * @param code + * @param status + * @return a ResponseEntity containing a JSONResponse with the error response + */ + private ResponseEntity buildErrorResponse(String message, int code, HttpStatus status) { + return ResponseEntity.status(status).body(JSONResponse.builder().success(false).code(code).message(message).build()); + } + + /** + * Builds a success response. + * + * @param message + * @param redirectUrl + * @return a ResponseEntity containing a JSONResponse with the success response + */ + private ResponseEntity buildSuccessResponse(String message, String redirectUrl) { + return ResponseEntity.ok(JSONResponse.builder().success(true).code(0).message(message).redirectUrl(redirectUrl).build()); } } diff --git a/src/main/java/com/digitalsanctuary/spring/user/api/package-info.java b/src/main/java/com/digitalsanctuary/spring/user/api/package-info.java index 3845d26..4ddb65c 100644 --- a/src/main/java/com/digitalsanctuary/spring/user/api/package-info.java +++ b/src/main/java/com/digitalsanctuary/spring/user/api/package-info.java @@ -1 +1,7 @@ -package com.digitalsanctuary.spring.user.api; \ No newline at end of file +/** + * This package contains the API classes for the Spring User Framework. + *

+ * The classes in this package are responsible for providing the public API for user-related operations within the Spring User Framework. + *

+ */ +package com.digitalsanctuary.spring.user.api; diff --git a/src/main/java/com/digitalsanctuary/spring/user/audit/AuditConfig.java b/src/main/java/com/digitalsanctuary/spring/user/audit/AuditConfig.java new file mode 100644 index 0000000..c2441bb --- /dev/null +++ b/src/main/java/com/digitalsanctuary/spring/user/audit/AuditConfig.java @@ -0,0 +1,41 @@ +package com.digitalsanctuary.spring.user.audit; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Component; +import lombok.Data; + +/** + * The AuditConfig class is a Spring Boot configuration class that provides properties for configuring user audit logging. This class is used to + * define properties that control the behavior of the audit logging, such as the log file path and the flush rate. + */ +@Data +@Component +@PropertySource("classpath:config/dsspringuserconfig.properties") +@ConfigurationProperties(prefix = "user.audit") +public class AuditConfig { + + /** + * The enabled flag. If set to false, audit logging will be disabled. + */ + private boolean logEvents; + + /** + * The log file path. This is the path to the log file where audit events will be written. The path can be absolute or relative to the application + */ + private String logFilePath; + + /** + * The flush on write flag, if enabled, causes the BufferedWriter to be flushed on every log entry. This has a performance impact under heavy + * loads, but ensures events are written to the log file without delay. This is beneficial in development environments, or environments where the + * performance penalty is less important that ensuring events are not lost in case of JVM or server crash. + */ + private boolean flushOnWrite; + + /** + * The flush rate. This is the rate at which the audit log buffer is flushed to the log file. The value is in milliseconds and can be set to any + * positive integer. The default value is 1000 (1 second). + */ + private int flushRate; + +} diff --git a/src/main/java/com/digitalsanctuary/spring/user/event/AuditEvent.java b/src/main/java/com/digitalsanctuary/spring/user/audit/AuditEvent.java similarity index 97% rename from src/main/java/com/digitalsanctuary/spring/user/event/AuditEvent.java rename to src/main/java/com/digitalsanctuary/spring/user/audit/AuditEvent.java index 77df0e7..d3ee922 100644 --- a/src/main/java/com/digitalsanctuary/spring/user/event/AuditEvent.java +++ b/src/main/java/com/digitalsanctuary/spring/user/audit/AuditEvent.java @@ -1,4 +1,4 @@ -package com.digitalsanctuary.spring.user.event; +package com.digitalsanctuary.spring.user.audit; import java.util.Date; import org.springframework.context.ApplicationEvent; diff --git a/src/main/java/com/digitalsanctuary/spring/user/audit/AuditEventListener.java b/src/main/java/com/digitalsanctuary/spring/user/audit/AuditEventListener.java new file mode 100644 index 0000000..3cc59ee --- /dev/null +++ b/src/main/java/com/digitalsanctuary/spring/user/audit/AuditEventListener.java @@ -0,0 +1,40 @@ +package com.digitalsanctuary.spring.user.audit; + +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * This class processes AuditEvents. This class writes the AuditEvent data to a text file on the server. You could easily change the logic to write to + * a database, send events to a REST API, or anything else. + * + * @see AuditEvent + */ +@Slf4j +@Async +@Component +@RequiredArgsConstructor +public class AuditEventListener { + + private final AuditConfig auditConfig; + + private final AuditLogWriter auditLogWriter; + + /** + * Handle the AuditEvents. + * + * In this case we are writing the event data out to an audit log on the server, using pipe delimiters. + * + * @param event the event + */ + @EventListener + public void onApplicationEvent(AuditEvent event) { + log.debug("AuditEventListener.onApplicationEvent: called with event: {}", event); + if (auditConfig.isLogEvents() && event != null) { + log.debug("AuditEventListener.onApplicationEvent: logging event..."); + auditLogWriter.writeLog(event); + } + } +} diff --git a/src/main/java/com/digitalsanctuary/spring/user/audit/AuditLogWriter.java b/src/main/java/com/digitalsanctuary/spring/user/audit/AuditLogWriter.java new file mode 100644 index 0000000..c06fb97 --- /dev/null +++ b/src/main/java/com/digitalsanctuary/spring/user/audit/AuditLogWriter.java @@ -0,0 +1,34 @@ +package com.digitalsanctuary.spring.user.audit; + +/** + * Interface for writing audit log + * + * + * + *

+ * Implementations of this interface are responsible for writing audit log messages to a log file or other destination. + *

+ *

+ * This can include writing to a file, writing to a database, sending messages to a REST API, SIEM or any other method of storing or transmitting + * audit log messages. + *

+ */ +public interface AuditLogWriter { + + /** + * Write an audit log message + * + * @param event the audit event to log + */ + void writeLog(AuditEvent event); + + /** + * Setup the audit log writer + */ + void setup(); + + /** + * Cleanup the audit log writer + */ + void cleanup(); +} diff --git a/src/main/java/com/digitalsanctuary/spring/user/audit/FileAuditLogFlushScheduler.java b/src/main/java/com/digitalsanctuary/spring/user/audit/FileAuditLogFlushScheduler.java new file mode 100644 index 0000000..5f83715 --- /dev/null +++ b/src/main/java/com/digitalsanctuary/spring/user/audit/FileAuditLogFlushScheduler.java @@ -0,0 +1,33 @@ +package com.digitalsanctuary.spring.user.audit; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * The FileAuditLogFlushScheduler class is a Spring Boot component that flushes the audit log buffer to the file. This class is used to ensure that + * the audit log buffer is flushed periodically to balance performance with data integrity. + */ +@Slf4j +@Component +@RequiredArgsConstructor +@ConditionalOnProperty(name = "user.audit.flushOnWrite", havingValue = "false") +public class FileAuditLogFlushScheduler { + + /** + * The file audit log writer. This is the writer that is used to write audit log events to the file. + */ + private final FileAuditLogWriter fileAuditLogWriter; + + /** + * Flushes the audit log buffer to the file. This method is called on a schedule to ensure that the buffer is flushed periodically to balance + * performance with data integrity. + */ + @Scheduled(fixedRateString = "#{@auditConfig.flushRate}") + public void flushAuditLog() { + log.info("FileAuditLogFlushScheduler.flushAuditLog: Flushing audit log buffer to file."); + fileAuditLogWriter.flushWriter(); + } +} diff --git a/src/main/java/com/digitalsanctuary/spring/user/audit/FileAuditLogWriter.java b/src/main/java/com/digitalsanctuary/spring/user/audit/FileAuditLogWriter.java new file mode 100644 index 0000000..d82dc86 --- /dev/null +++ b/src/main/java/com/digitalsanctuary/spring/user/audit/FileAuditLogWriter.java @@ -0,0 +1,167 @@ +package com.digitalsanctuary.spring.user.audit; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.text.MessageFormat; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * Implementation of {@link AuditLogWriter} that writes audit logs to a file. This class handles the lifecycle of the log file, including opening, + * writing, and closing the file. It also supports scheduled flushing of the buffer to balance performance with data integrity. + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class FileAuditLogWriter implements AuditLogWriter { + + private final AuditConfig auditConfig; + private BufferedWriter bufferedWriter; + + /** + * Initializes the log file writer. This method is called after the bean is constructed. It validates the configuration and opens the log file for + * writing. + */ + @PostConstruct + @Override + public void setup() { + log.info("FileAuditLogWriter.setup: Entering..."); + if (!validateConfig()) { + return; + } + openLogFile(); + } + + /** + * Cleans up the log file writer. This method is called before the bean is destroyed. It closes the log file to ensure all data is flushed and + * resources are released. + */ + @PreDestroy + @Override + public void cleanup() { + log.info("FileAuditLogWriter.cleanup: Closing log file."); + closeLogFile(); + } + + /** + * Writes an audit event to the log file. The event data is formatted and written as a single line. If the buffered writer is not initialized, an + * error is logged. + * + * @param event the audit event to write + */ + @Override + public void writeLog(AuditEvent event) { + if (bufferedWriter == null) { + log.error("FileAuditLogWriter.writeLog: BufferedWriter is not initialized."); + return; + } + try { + String userId = event.getUser() != null ? event.getUser().getId().toString() : null; + String userEmail = event.getUser() != null ? event.getUser().getEmail() : null; + String output = MessageFormat.format("{0}|{1}|{2}|{3}|{4}|{5}|{6}|{7}|{8}|{9}", event.getDate(), event.getAction(), + event.getActionStatus(), userId, userEmail, event.getIpAddress(), event.getSessionId(), event.getMessage(), event.getUserAgent(), + event.getExtraData()); + bufferedWriter.write(output); + bufferedWriter.newLine(); + if (auditConfig.isFlushOnWrite()) { + bufferedWriter.flush(); + } + } catch (IOException e) { + log.error("FileAuditLogWriter.writeLog: IOException writing to log file: {}", auditConfig.getLogFilePath(), e); + } + } + + + /** + * Flushes the buffered writer to ensure all data is written to the log file. This method is called by the {@link FileAuditLogFlushScheduler} to + * ensure that the buffer is flushed periodically to balance performance with data integrity. + */ + public void flushWriter() { + if (bufferedWriter != null) { + try { + bufferedWriter.flush(); + } catch (IOException e) { + log.error("FileAuditLogWriter.flushWriter: IOException flushing buffer!", e); + } + } + } + + /** + * Validates the audit configuration to ensure it is properly set up. Logs errors if the configuration is invalid. + * + * @return true if the configuration is valid, false otherwise + */ + private boolean validateConfig() { + if (auditConfig == null) { + log.error("FileAuditLogWriter.setup: No AuditConfig has been configured!"); + return false; + } + if (!auditConfig.isLogEvents()) { + log.info("FileAuditLogWriter.setup: Audit logging is disabled."); + return false; + } + if (!StringUtils.hasText(auditConfig.getLogFilePath())) { + log.error("FileAuditLogWriter.setup: No user.audit.logFilePath has been configured!"); + return false; + } + return true; + } + + /** + * Opens the log file for writing. If the file does not exist, it is created. If the file is newly created, a header is written to the file. + */ + private void openLogFile() { + String logFilePath = auditConfig.getLogFilePath(); + log.debug("FileAuditLogWriter.setup: Opening log file: {}", logFilePath); + try { + OpenOption[] fileOptions = {StandardOpenOption.CREATE, StandardOpenOption.APPEND, StandardOpenOption.WRITE}; + boolean newFile = Files.notExists(Path.of(logFilePath)); + bufferedWriter = Files.newBufferedWriter(Path.of(logFilePath), fileOptions); + if (newFile) { + writeHeader(); + } + log.info("FileAuditLogWriter.setup: Log file opened."); + } catch (IOException e) { + log.error("FileAuditLogWriter.setup: IOException trying to open log file: {}", logFilePath, e); + } + } + + /** + * Closes the log file to ensure all data is flushed and resources are released. + */ + private void closeLogFile() { + try { + if (bufferedWriter != null) { + bufferedWriter.close(); + } + } catch (IOException e) { + log.error("FileAuditLogWriter.cleanup: IOException closing log file: {}", auditConfig.getLogFilePath(), e); + } + } + + /** + * Writes a header to the log file. This method is called when the log file is newly created. + */ + private void writeHeader() { + log.debug("FileAuditLogWriter.writeHeader: writing header."); + if (bufferedWriter != null) { + String output = MessageFormat.format("{0}|{1}|{2}|{3}|{4}|{5}|{6}|{7}|{8}|{9}", "Date", "Action", "Action Status", "User ID", "Email", + "IP Address", "SessionId", "Message", "User Agent", "Extra Data"); + try { + bufferedWriter.write(output); + bufferedWriter.newLine(); + bufferedWriter.flush(); + } catch (IOException e) { + log.error("FileAuditLogWriter.writeHeader: IOException writing header: {}", output, e); + } + } + } +} diff --git a/src/main/java/com/digitalsanctuary/spring/user/controller/PageController.java b/src/main/java/com/digitalsanctuary/spring/user/controller/PageController.java deleted file mode 100644 index e9c27b1..0000000 --- a/src/main/java/com/digitalsanctuary/spring/user/controller/PageController.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.digitalsanctuary.spring.user.controller; - -import java.util.Locale; -import java.util.Optional; -import org.springframework.context.MessageSource; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.stereotype.Controller; -import org.springframework.ui.ModelMap; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; -import com.digitalsanctuary.spring.user.service.DSUserDetails; -import jakarta.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * The Main Page Controller for pages outside of the actual User Management Framework. - */ -@Slf4j -@RequiredArgsConstructor -@Controller -public class PageController { - - private final MessageSource messages; - - /** - * Home Page. - * - * @return the string - */ - @GetMapping({"/", "/index.html"}) - public String index(@AuthenticationPrincipal DSUserDetails userDetails, final HttpServletRequest request, final ModelMap model, - @RequestParam("messageKey") final Optional messageKey) { - log.debug("PageController.index: called with messageKey={}", messageKey.orElse("")); - // If the user is logged in, we'll add their details to the model - if (userDetails != null) { - log.debug("PageController.index: userDetails={}", userDetails); - model.addAttribute("user", userDetails.getUser()); - } - - // If there is a messageKey GET param, we'll map that into a locale specific message and add that to the model - Locale locale = request.getLocale(); - messageKey.ifPresent(key -> { - String message = messages.getMessage(key, null, locale); - model.addAttribute("message", message); - }); - return "index"; - } - - /** - * An example Protected page - * - * @return the string - */ - @GetMapping("/protected.html") - public String protectedPage() { - return "protected"; - } - - /** - * An example Unprotected page. - * - * @return the string - */ - @GetMapping("/unprotected.html") - public String unprotectedPage() { - return "unprotected"; - } - -} diff --git a/src/main/java/com/digitalsanctuary/spring/user/controller/UserActionController.java b/src/main/java/com/digitalsanctuary/spring/user/controller/UserActionController.java index 9aad293..3f00041 100644 --- a/src/main/java/com/digitalsanctuary/spring/user/controller/UserActionController.java +++ b/src/main/java/com/digitalsanctuary/spring/user/controller/UserActionController.java @@ -10,7 +10,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.ModelAndView; -import com.digitalsanctuary.spring.user.event.AuditEvent; +import com.digitalsanctuary.spring.user.audit.AuditEvent; import com.digitalsanctuary.spring.user.persistence.model.User; import com.digitalsanctuary.spring.user.service.UserService; import com.digitalsanctuary.spring.user.service.UserService.TokenValidationResult; @@ -58,6 +58,7 @@ public class UserActionController { /** * Validate a forgot password token link from an email, and if valid, show the change password page. * + * @param request the request * @param model the model * @param token the token * @return the model and view @@ -113,7 +114,7 @@ public ModelAndView confirmRegistration(final HttpServletRequest request, final eventPublisher.publishEvent(registrationAuditEvent); } - model.addAttribute("message", messages.getMessage("message.accountVerified", null, locale)); + model.addAttribute("message", messages.getMessage("message.account.verified", null, locale)); log.debug("UserAPI.confirmRegistration: account verified and user logged in!"); String redirectString = "redirect:" + registrationSuccessURI; return new ModelAndView(redirectString, model); diff --git a/src/main/java/com/digitalsanctuary/spring/user/controller/UserPageController.java b/src/main/java/com/digitalsanctuary/spring/user/controller/UserPageController.java index 54fd8e6..601f502 100644 --- a/src/main/java/com/digitalsanctuary/spring/user/controller/UserPageController.java +++ b/src/main/java/com/digitalsanctuary/spring/user/controller/UserPageController.java @@ -31,6 +31,9 @@ public class UserPageController { * Login Page. * * @param userDetails the user details + * @param session the session + * @param model the model + * * @return the string */ @GetMapping("/user/login.html") @@ -48,6 +51,9 @@ public String login(@AuthenticationPrincipal DSUserDetails userDetails, HttpSess /** * Register Page. * + * @param userDetails the user details + * @param session the session + * @param model the model * @return the string */ @GetMapping("/user/register.html") @@ -75,7 +81,9 @@ public String registrationPending() { /** * Registration complete. * - * @param userDetails + * @param userDetails the user details + * @param session the session + * @param model the model * * @return the string */ @@ -125,6 +133,13 @@ public String forgotPasswordChange() { return "user/forgot-password-change"; } + + /** + * @param userDetails the user details + * @param request the request + * @param model the model + * @return String + */ @GetMapping("/user/update-user.html") public String updateUser(@AuthenticationPrincipal DSUserDetails userDetails, final HttpServletRequest request, final ModelMap model) { if (userDetails != null) { @@ -137,11 +152,21 @@ public String updateUser(@AuthenticationPrincipal DSUserDetails userDetails, fin return "user/update-user"; } + /** + * Update password. + * + * @return the string + */ @GetMapping("/user/update-password.html") public String updatePassword() { return "user/update-password"; } + /** + * Delete account. + * + * @return the string + */ @GetMapping("/user/delete-account.html") public String deleteAccount() { return "user/delete-account"; diff --git a/src/main/java/com/digitalsanctuary/spring/user/controller/package-info.java b/src/main/java/com/digitalsanctuary/spring/user/controller/package-info.java index d226a03..5708f08 100644 --- a/src/main/java/com/digitalsanctuary/spring/user/controller/package-info.java +++ b/src/main/java/com/digitalsanctuary/spring/user/controller/package-info.java @@ -1 +1,12 @@ -package com.digitalsanctuary.spring.user.controller; \ No newline at end of file +/** + * This package contains the controller classes for the Spring User Framework. + *

+ * The controllers in this package are responsible for handling HTTP requests and returning appropriate responses. They act as the entry point for the + * user-related operations and interact with the service layer to perform business logic. + *

+ *

+ * The controllers are designed to be RESTful and follow the principles of REST architecture. They provide endpoints for creating, updating, + * retrieving, and deleting user information. + *

+ */ +package com.digitalsanctuary.spring.user.controller; diff --git a/src/main/java/com/digitalsanctuary/spring/user/dto/package-info.java b/src/main/java/com/digitalsanctuary/spring/user/dto/package-info.java index c72be9a..dd0ddcc 100644 --- a/src/main/java/com/digitalsanctuary/spring/user/dto/package-info.java +++ b/src/main/java/com/digitalsanctuary/spring/user/dto/package-info.java @@ -1 +1,7 @@ -package com.digitalsanctuary.spring.user.dto; \ No newline at end of file +/** + * This package contains Data Transfer Object (DTO) classes for the Spring User Framework. + *

+ * DTOs are used to transfer data between different layers of the application. They are simple objects that should not contain any business logic. + *

+ */ +package com.digitalsanctuary.spring.user.dto; diff --git a/src/main/java/com/digitalsanctuary/spring/user/exceptions/OAuth2AuthenticationProcessingException.java b/src/main/java/com/digitalsanctuary/spring/user/exceptions/OAuth2AuthenticationProcessingException.java index 1c60a48..00934b7 100644 --- a/src/main/java/com/digitalsanctuary/spring/user/exceptions/OAuth2AuthenticationProcessingException.java +++ b/src/main/java/com/digitalsanctuary/spring/user/exceptions/OAuth2AuthenticationProcessingException.java @@ -1,17 +1,36 @@ package com.digitalsanctuary.spring.user.exceptions; +/** + * Exception thrown when there is an error processing OAuth2 authentication. + */ public class OAuth2AuthenticationProcessingException extends RuntimeException { private static final long serialVersionUID = 1L; + /** + * Constructs a new OAuth2AuthenticationProcessingException with the specified detail message and cause. + * + * @param message the detail message + * @param cause the cause of the exception + */ public OAuth2AuthenticationProcessingException(String message, Throwable cause) { super(message, cause); } + /** + * Constructs a new OAuth2AuthenticationProcessingException with the specified detail message. + * + * @param message the detail message + */ public OAuth2AuthenticationProcessingException(String message) { super(message); } + /** + * Constructs a new OAuth2AuthenticationProcessingException with the specified cause. + * + * @param cause the cause of the exception + */ public OAuth2AuthenticationProcessingException(Throwable cause) { super(cause); } diff --git a/src/main/java/com/digitalsanctuary/spring/user/exceptions/package-info.java b/src/main/java/com/digitalsanctuary/spring/user/exceptions/package-info.java index 046cc27..8f230b2 100644 --- a/src/main/java/com/digitalsanctuary/spring/user/exceptions/package-info.java +++ b/src/main/java/com/digitalsanctuary/spring/user/exceptions/package-info.java @@ -1 +1,8 @@ -package com.digitalsanctuary.spring.user.exceptions; \ No newline at end of file +/** + * This package contains custom exception classes for the Spring User Framework. + *

+ * The exceptions in this package are used to handle various error conditions that may occur within the user management functionality of the + * framework. + *

+ */ +package com.digitalsanctuary.spring.user.exceptions; diff --git a/src/main/java/com/digitalsanctuary/spring/user/jobs/package-info.java b/src/main/java/com/digitalsanctuary/spring/user/jobs/package-info.java index 910a63d..de1aa5f 100644 --- a/src/main/java/com/digitalsanctuary/spring/user/jobs/package-info.java +++ b/src/main/java/com/digitalsanctuary/spring/user/jobs/package-info.java @@ -1 +1,22 @@ -package com.digitalsanctuary.spring.user.jobs; \ No newline at end of file +/** + * This package contains job-related classes and interfaces for the Spring User Framework. + * + *

+ * The classes in this package are responsible for handling various job operations within the Spring User Framework, such as scheduling, execution, + * and management of jobs. + *

+ * + * + * The main functionalities provided by this package include: + *
    + *
  • Job scheduling and execution
  • + *
  • Job management and monitoring
  • + *
  • Integration with other components of the Spring User Framework
  • + *
+ * + * + *

+ * This package is part of the Digital Sanctuary's Spring User Framework project. + *

+ */ +package com.digitalsanctuary.spring.user.jobs; diff --git a/src/main/java/com/digitalsanctuary/spring/user/listener/AuditEventListener.java b/src/main/java/com/digitalsanctuary/spring/user/listener/AuditEventListener.java deleted file mode 100644 index 6efc5fc..0000000 --- a/src/main/java/com/digitalsanctuary/spring/user/listener/AuditEventListener.java +++ /dev/null @@ -1,165 +0,0 @@ -package com.digitalsanctuary.spring.user.listener; - -import java.io.BufferedWriter; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.OpenOption; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.text.MessageFormat; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.event.EventListener; -import org.springframework.scheduling.annotation.Async; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; -import com.digitalsanctuary.spring.user.event.AuditEvent; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.extern.slf4j.Slf4j; - -/** - * This class processes AuditEvents. This class writes the AuditEvent data to a text file on the server. You could easily change the logic to write to - * a database, send events to a REST API, or anything else. - * - * @see AuditEvent - */ -@Slf4j -@Async -@Component -public class AuditEventListener { - - /** The logEvents flag. Set to true to log audit events. */ - @Value("${user.audit.logEvents:false}") - private boolean logEvents; - - /** The audit log file path. */ - @Value("${user.audit.logFilePath:}") - private String logFilePath; - - /** - * The flush on write flag, if enabled, causes the BufferedWriter to be flushed on every log entry. This has a performance impact under heavy - * loads, but ensures events are written to the log file without delay. This is beneficial in development environments, or environments where the - * performance penalty is less important that ensuring events are not lost in case of JVM or server crash. - */ - @Value("${user.audit.flushOnWrite:false}") - private boolean flushOnWrite; - - /** The buffered writer. This gets instantiated by the setup method. */ - private BufferedWriter bufferedWriter; - - /** - * Setup the service, opening the log file for writing, and if the file is new, write a header line first. - */ - @PostConstruct - private void setup() { - log.info("AuditEventListener.setup: Entering..."); - if (logEvents) { - if (!StringUtils.hasText(logFilePath)) { - log.error("AuditEventListener.setup: user.audit.logEvents is true, but no user.audit.logFilePath has been configured!"); - } else { - log.debug("AuditEventListener.setup: Opening log file: {}", logFilePath); - try { - OpenOption[] fileOptions = {StandardOpenOption.CREATE, StandardOpenOption.APPEND, StandardOpenOption.WRITE}; - boolean newFile = false; - if (Files.notExists(Path.of(logFilePath))) { - newFile = true; - } - bufferedWriter = Files.newBufferedWriter(Path.of(logFilePath), fileOptions); - if (newFile) { - writeHeader(); - } - log.info("AuditEventListener.setup: Log file opened."); - } catch (IOException e) { - log.error("AuditEventListener.setup: IOException trying to open log file: {}", logFilePath, e); - } - } - } - } - - /** - * Write a field header line to the start of a log file. - */ - private void writeHeader() { - log.debug("AuditEventListener.writeHeader: writing header."); - if (bufferedWriter != null) { - String output = MessageFormat.format("{0}|{1}|{2}|{3}|{4}|{5}|{6}|{7}|{8}|{9}", "Date", "Action", "Action Status", "User ID", "Email", - "IP Address", "SessionId", "Message", "User Agent", "Extra Data"); - try { - bufferedWriter.write(output); - bufferedWriter.newLine(); - bufferedWriter.flush(); - } catch (IOException e) { - log.error("AuditEventListener.onApplicationEvent: IOException writing line: {}", output, e); - } - } - } - - /** - * Teardown the service, closing the file writer. - */ - @PreDestroy - public void teardown() { - if (logEvents) { - if (bufferedWriter != null) { - log.debug("AuditEventListener.teardown: Closing log file: {}", logFilePath); - try { - bufferedWriter.close(); - log.debug("AuditEventListener.teardown: Log file closed."); - } catch (IOException e) { - log.error("AuditEventListener.teardown: IOException while trying to close bufferedWriter!", e); - } - } - } - } - - /** - * Flush writer on schedule to balance performance with getting data written to the audit log. - */ - @Scheduled(fixedDelay = 30000, initialDelay = 30000) - public void flushWriterOnSchedule() { - if (bufferedWriter != null && !flushOnWrite) { - try { - bufferedWriter.flush(); - } catch (IOException e) { - log.error("AuditEventListener.flushWriterOnSchedule: IOException flushing buffer!", e); - } - } - } - - /** - * Handle the AuditEvents. - * - * In this case we are writing the event data out to an audit log on the server, using pipe delimiters. - * - * @param event the event - */ - @EventListener - public void onApplicationEvent(AuditEvent event) { - log.debug("AuditEventListener.onApplicationEvent: called with event: {}", event); - if (logEvents && bufferedWriter != null && event != null) { - log.debug("AuditEventListener.onApplicationEvent: logging event..."); - String userId = null; - String userEmail = null; - // If the event has a User object on it, we'll get some data from it - if (event.getUser() != null) { - userId = event.getUser().getId().toString(); - userEmail = event.getUser().getEmail(); - } - String output = MessageFormat.format("{0}|{1}|{2}|{3}|{4}|{5}|{6}|{7}|{8}|{9}", event.getDate(), event.getAction(), - event.getActionStatus(), userId, userEmail, event.getIpAddress(), event.getSessionId(), event.getMessage(), event.getUserAgent(), - event.getExtraData()); - - log.debug("AuditEventListener.onApplicationEvent: output: {}", output); - try { - bufferedWriter.write(output); - bufferedWriter.newLine(); - if (flushOnWrite) { - bufferedWriter.flush(); - } - } catch (IOException e) { - log.error("AuditEventListener.onApplicationEvent: IOException writing line: {}", output, e); - } - } - } -} diff --git a/src/main/java/com/digitalsanctuary/spring/user/listener/AuthenticationEventLIstener.java b/src/main/java/com/digitalsanctuary/spring/user/listener/AuthenticationEventLIstener.java index 14f9119..109a405 100644 --- a/src/main/java/com/digitalsanctuary/spring/user/listener/AuthenticationEventLIstener.java +++ b/src/main/java/com/digitalsanctuary/spring/user/listener/AuthenticationEventLIstener.java @@ -10,7 +10,7 @@ /** * This class is used to listen for authentication events and handle account lockout functionality if needed. - * + * * https://github.com/devondragon/SpringUserFramework/issues/29 */ @Slf4j @@ -20,6 +20,11 @@ public class AuthenticationEventLIstener { final private LoginAttemptService loginAttemptService; + /** + * This method listens for successful authentications and handles account lockout functionality. + * + * @param success the success event + */ @EventListener public void onSuccess(AuthenticationSuccessEvent success) { // Handle successful authentication, e.g. logging or auditing @@ -28,6 +33,11 @@ public void onSuccess(AuthenticationSuccessEvent success) { loginAttemptService.loginSucceeded(username); } + /** + * This method listens for authentication failures and handles account lockout functionality. + * + * @param failure the failure event + */ @EventListener public void onFailure(AbstractAuthenticationFailureEvent failure) { // Handle unsuccessful authentication, e.g. logging or auditing diff --git a/src/main/java/com/digitalsanctuary/spring/user/mail/package-info.java b/src/main/java/com/digitalsanctuary/spring/user/mail/package-info.java index 88a5a26..ce7601b 100644 --- a/src/main/java/com/digitalsanctuary/spring/user/mail/package-info.java +++ b/src/main/java/com/digitalsanctuary/spring/user/mail/package-info.java @@ -1 +1,32 @@ -package com.digitalsanctuary.spring.user.mail; \ No newline at end of file +/** + * This package contains classes and interfaces related to the mailing functionality within the Spring User Framework. It provides the necessary + * components to handle email operations such as sending and receiving emails. + * + *

+ * Key components include: + *

+ *
    + *
  • EmailService: A service interface for email operations.
  • + *
  • EmailServiceImpl: An implementation of the EmailService interface.
  • + *
  • EmailTemplate: A class representing email templates used for sending emails.
  • + *
+ * + *

+ * 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 ab96810..0000000 Binary files a/src/main/resources/static/fonts/nucleo-icons.eot and /dev/null differ diff --git a/src/main/resources/static/fonts/nucleo-icons.svg b/src/main/resources/static/fonts/nucleo-icons.svg deleted file mode 100644 index 6654c1a..0000000 --- a/src/main/resources/static/fonts/nucleo-icons.svg +++ /dev/null @@ -1,312 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 1a55985..0000000 Binary files a/src/main/resources/static/fonts/nucleo-icons.ttf and /dev/null differ diff --git a/src/main/resources/static/fonts/nucleo-icons.woff b/src/main/resources/static/fonts/nucleo-icons.woff deleted file mode 100644 index cb19247..0000000 Binary files a/src/main/resources/static/fonts/nucleo-icons.woff and /dev/null differ 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 e294e08..0000000 Binary files a/src/main/resources/static/fonts/nucleo-icons.woff2 and /dev/null differ diff --git a/src/main/resources/static/fonts/nucleo.eot b/src/main/resources/static/fonts/nucleo.eot deleted file mode 100644 index 8609095..0000000 Binary files a/src/main/resources/static/fonts/nucleo.eot and /dev/null differ diff --git a/src/main/resources/static/fonts/nucleo.ttf b/src/main/resources/static/fonts/nucleo.ttf deleted file mode 100644 index 2a42417..0000000 Binary files a/src/main/resources/static/fonts/nucleo.ttf and /dev/null differ diff --git a/src/main/resources/static/fonts/nucleo.woff b/src/main/resources/static/fonts/nucleo.woff deleted file mode 100644 index 20fecf0..0000000 Binary files a/src/main/resources/static/fonts/nucleo.woff and /dev/null differ diff --git a/src/main/resources/static/fonts/nucleo.woff2 b/src/main/resources/static/fonts/nucleo.woff2 deleted file mode 100644 index eae6879..0000000 Binary files a/src/main/resources/static/fonts/nucleo.woff2 and /dev/null differ diff --git a/src/main/resources/static/js/chart-1.js b/src/main/resources/static/js/chart-1.js deleted file mode 100644 index 107305e..0000000 --- a/src/main/resources/static/js/chart-1.js +++ /dev/null @@ -1,71 +0,0 @@ -// chart 1 - -var ctx = document.getElementById("chart-bars").getContext("2d"); - -new Chart(ctx, { - type: "bar", - data: { - labels: ["Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], - datasets: [ - { - label: "Sales", - tension: 0.4, - borderWidth: 0, - borderRadius: 4, - borderSkipped: false, - backgroundColor: "#fff", - data: [450, 200, 100, 220, 500, 100, 400, 230, 500], - maxBarThickness: 6, - }, - ], - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - display: false, - }, - }, - interaction: { - intersect: false, - mode: "index", - }, - scales: { - y: { - grid: { - drawBorder: false, - display: false, - drawOnChartArea: false, - drawTicks: false, - }, - ticks: { - suggestedMin: 0, - suggestedMax: 600, - beginAtZero: true, - padding: 15, - font: { - size: 14, - family: "Open Sans", - style: "normal", - lineHeight: 2, - }, - color: "#fff", - }, - }, - x: { - grid: { - drawBorder: false, - display: false, - drawOnChartArea: false, - drawTicks: false, - }, - ticks: { - display: false, - }, - }, - }, - }, -}); - -// end chart 1 diff --git a/src/main/resources/static/js/chart-2.js b/src/main/resources/static/js/chart-2.js deleted file mode 100644 index f25d21b..0000000 --- a/src/main/resources/static/js/chart-2.js +++ /dev/null @@ -1,105 +0,0 @@ -// chart 2 - -var ctx2 = document.getElementById("chart-line").getContext("2d"); - -var gradientStroke1 = ctx2.createLinearGradient(0, 230, 0, 50); - -gradientStroke1.addColorStop(1, "rgba(203,12,159,0.2)"); -gradientStroke1.addColorStop(0.2, "rgba(72,72,176,0.0)"); -gradientStroke1.addColorStop(0, "rgba(203,12,159,0)"); //purple colors - -var gradientStroke2 = ctx2.createLinearGradient(0, 230, 0, 50); - -gradientStroke2.addColorStop(1, "rgba(20,23,39,0.2)"); -gradientStroke2.addColorStop(0.2, "rgba(72,72,176,0.0)"); -gradientStroke2.addColorStop(0, "rgba(20,23,39,0)"); //purple colors - -new Chart(ctx2, { - type: "line", - data: { - labels: ["Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], - datasets: [ - { - label: "Mobile apps", - tension: 0.4, - borderWidth: 0, - pointRadius: 0, - borderColor: "#cb0c9f", - borderWidth: 3, - backgroundColor: gradientStroke1, - fill: true, - data: [50, 40, 300, 220, 500, 250, 400, 230, 500], - maxBarThickness: 6, - }, - { - label: "Websites", - tension: 0.4, - borderWidth: 0, - pointRadius: 0, - borderColor: "#3A416F", - borderWidth: 3, - backgroundColor: gradientStroke2, - fill: true, - data: [30, 90, 40, 140, 290, 290, 340, 230, 400], - maxBarThickness: 6, - }, - ], - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - display: false, - }, - }, - interaction: { - intersect: false, - mode: "index", - }, - scales: { - y: { - grid: { - drawBorder: false, - display: true, - drawOnChartArea: true, - drawTicks: false, - borderDash: [5, 5], - }, - ticks: { - display: true, - padding: 10, - color: "#b2b9bf", - font: { - size: 11, - family: "Open Sans", - style: "normal", - lineHeight: 2, - }, - }, - }, - x: { - grid: { - drawBorder: false, - display: false, - drawOnChartArea: false, - drawTicks: false, - borderDash: [5, 5], - }, - ticks: { - display: true, - color: "#b2b9bf", - padding: 20, - font: { - size: 11, - family: "Open Sans", - style: "normal", - lineHeight: 2, - }, - }, - }, - }, - }, -}); - -// end chart 2 diff --git a/src/main/resources/static/js/dropdown.js b/src/main/resources/static/js/dropdown.js deleted file mode 100644 index 6385c55..0000000 --- a/src/main/resources/static/js/dropdown.js +++ /dev/null @@ -1,29 +0,0 @@ -// Navbar notifications dropdown - -var dropdown_triggers = document.querySelectorAll("[dropdown-trigger]"); -dropdown_triggers.forEach((dropdown_trigger) => { - let dropdown_menu = dropdown_trigger.parentElement.querySelector("[dropdown-menu]"); - - dropdown_trigger.addEventListener("click", function () { - dropdown_menu.classList.toggle("opacity-0"); - dropdown_menu.classList.toggle("pointer-events-none"); - dropdown_menu.classList.toggle("before:-top-5"); - if (dropdown_trigger.getAttribute("aria-expanded") == "false") { - dropdown_trigger.setAttribute("aria-expanded", "true"); - dropdown_menu.classList.remove("transform-dropdown"); - dropdown_menu.classList.add("transform-dropdown-show"); - } else { - dropdown_trigger.setAttribute("aria-expanded", "false"); - dropdown_menu.classList.remove("transform-dropdown-show"); - dropdown_menu.classList.add("transform-dropdown"); - } - }); - - window.addEventListener("click", function (e) { - if (!dropdown_menu.contains(e.target) && !dropdown_trigger.contains(e.target)) { - if (dropdown_trigger.getAttribute("aria-expanded") == "true") { - dropdown_trigger.click(); - } - } - }); -}); diff --git a/src/main/resources/static/js/fixed-plugin.js b/src/main/resources/static/js/fixed-plugin.js deleted file mode 100644 index 4f1e93c..0000000 --- a/src/main/resources/static/js/fixed-plugin.js +++ /dev/null @@ -1,287 +0,0 @@ -var pageName = page; -var sidenav_target = to_build + "pages/" + pageName + ".html"; - -var fixedPlugin = document.querySelector("[fixed-plugin]"); -var fixedPluginButton = document.querySelector("[fixed-plugin-button]"); -var fixedPluginButtonNav = document.querySelector("[fixed-plugin-button-nav]"); -var fixedPluginCard = document.querySelector("[fixed-plugin-card]"); -var fixedPluginCloseButton = document.querySelector("[fixed-plugin-close-button]"); - -var navbar = document.querySelector("[navbar-main]"); - -var buttonNavbarFixed = document.querySelector("[navbarFixed]"); - -var sidenav = document.querySelector("aside"); -var sidenav_icons = sidenav.querySelectorAll("li a div"); - - -var transparentBtn = document.querySelector("[transparent-style-btn]"); -var whiteBtn = document.querySelector("[white-style-btn]"); - -var non_active_style = ["bg-none", "bg-transparent", "text-fuchsia-500", "border-fuchsia-500"]; -var active_style = ["bg-gradient-to-tl", "from-purple-700", "to-pink-500", "bg-fuchsia-500", "text-white", "border-transparent"]; - -var transparent_sidenav_classes = ["xl:bg-transparent", "shadow-none"]; -var transparent_sidenav_highlighted = ["shadow-soft-xl"]; -var transparent_sidenav_icons = ["bg-white"]; - -var white_sidenav_classes = ["xl:bg-white", "shadow-soft-xl"]; -var white_sidenav_highlighted = ["shadow-none"]; -var white_sidenav_icons = ["bg-gray-200"]; - -var sidenav_highlight = document.querySelector("a[href=" + CSS.escape(sidenav_target) + "]"); - -// fixed plugin toggle -if (pageName != "rtl") { - fixedPluginButton.addEventListener("click", function () { - fixedPluginCard.classList.toggle("-right-90"); - fixedPluginCard.classList.toggle("right-0"); - }); - - fixedPluginButtonNav.addEventListener("click", function () { - fixedPluginCard.classList.toggle("-right-90"); - fixedPluginCard.classList.toggle("right-0"); - }); - - fixedPluginCloseButton.addEventListener("click", function () { - fixedPluginCard.classList.toggle("-right-90"); - fixedPluginCard.classList.toggle("right-0"); - }); - - window.addEventListener("click", function (e) { - if (!fixedPlugin.contains(e.target) && !fixedPluginButton.contains(e.target) && !fixedPluginButtonNav.contains(e.target)) { - if (fixedPluginCard.classList.contains("right-0")) { - fixedPluginCloseButton.click(); - } - } - }); -} else { - fixedPluginButton.addEventListener("click", function () { - fixedPluginCard.classList.toggle("-left-90"); - fixedPluginCard.classList.toggle("left-0"); - }); - - fixedPluginButtonNav.addEventListener("click", function () { - fixedPluginCard.classList.toggle("-left-90"); - fixedPluginCard.classList.toggle("left-0"); - }); - - fixedPluginCloseButton.addEventListener("click", function () { - fixedPluginCard.classList.toggle("-left-90"); - fixedPluginCard.classList.toggle("left-0"); - }); - - window.addEventListener("click", function (e) { - if (!fixedPlugin.contains(e.target) && !fixedPluginButton.contains(e.target) && !fixedPluginButtonNav.contains(e.target)) { - if (fixedPluginCard.classList.contains("left-0")) { - fixedPluginCloseButton.click(); - } - } - }); -} - -// color sidenav - -function sidebarColor(a) { - var color_from = a.getAttribute("data-color-from"); - var color_to = a.getAttribute("data-color-to"); - var parent = a.parentElement.children; - - var activeColorFrom; - var activeColorTo; - var activeSidenavIconColorClassFrom; - var activeSidenavIconColorClassTo; - var activeSidenavCardColorClassFrom; - var activeSidenavCardColorClassTo; - var activeSidenavCardIconColorClassFrom; - var activeSidenavCardIconColorClassTo; - - var checkedSidenavIconColorFrom = "from-" + color_from; - var checkedSidenavIconColorTo = "to-" + color_to; - - var checkedSidenavCardColorFrom = "after:from-" + (color_from == "purple-700" ? "slate-600" : color_from); - var checkedSidenavCardColorTo = "after:to-" + (color_to == "pink-500" ? "slate-300" : color_to); - - var checkedSidenavCardIconColorClassFrom = "from-" + (color_from == "purple-700" ? "slate-600" : color_from); - var checkedSidenavCardIconColorClassTo = "to-" + (color_to == "pink-500" ? "slate-300" : color_to); - - var sidenavCard = document.querySelector("[sidenav-card]"); - var sidenavCardIcon = document.querySelector("[sidenav-card-icon]"); - var sidenavIcon = sidenav_highlight.firstElementChild; - - for (var i = 0; i < parent.length; i++) { - if (parent[i].hasAttribute("active-color")) { - activeColorFrom = parent[i].getAttribute("data-color-from"); - activeColorTo = parent[i].getAttribute("data-color-to"); - - parent[i].classList.toggle("border-white"); - parent[i].classList.toggle("border-slate-700"); - - activeSidenavIconColorClassFrom = "from-" + activeColorFrom; - activeSidenavIconColorClassTo = "to-" + activeColorTo; - - activeSidenavIconColorClassFrom = "from-" + activeColorFrom; - activeSidenavIconColorClassTo = "to-" + activeColorTo; - - activeSidenavCardIconColorClassFrom = "from-" + (activeColorFrom == "purple-700" ? "slate-600" : activeColorFrom); - activeSidenavCardIconColorClassTo = "to-" + (activeColorTo == "pink-500" ? "slate-300" : activeColorTo); - } - parent[i].removeAttribute("active-color"); - } - - var att = document.createAttribute("active-color"); - - a.setAttributeNode(att); - a.classList.toggle("border-white"); - a.classList.toggle("border-slate-700"); - - sidenavCard.classList.remove(activeSidenavCardColorClassFrom); - sidenavCard.classList.remove(activeSidenavCardColorClassTo); - - sidenavCardIcon.classList.remove(activeSidenavCardIconColorClassFrom); - sidenavCardIcon.classList.remove(activeSidenavCardIconColorClassTo); - - sidenavIcon.classList.remove(activeSidenavIconColorClassFrom); - sidenavIcon.classList.remove(activeSidenavIconColorClassTo); - - sidenavCard.classList.add(checkedSidenavCardColorFrom); - sidenavCard.classList.add(checkedSidenavCardColorTo); - - sidenavCardIcon.classList.add(checkedSidenavCardIconColorClassFrom); - sidenavCardIcon.classList.add(checkedSidenavCardIconColorClassTo); - - sidenavIcon.classList.add(checkedSidenavIconColorFrom); - sidenavIcon.classList.add(checkedSidenavIconColorTo); -} - -// sidenav style - -transparentBtn.addEventListener("click", function () { - const active_style_attr = document.createAttribute("active-style"); - if (!this.hasAttribute(active_style_attr)) { - // change trigger buttons style - - this.setAttributeNode(active_style_attr); - - non_active_style.forEach((style_class) => { - this.classList.remove(style_class); - }); - - active_style.forEach((style_class) => { - this.classList.add(style_class); - }); - - whiteBtn.removeAttribute(active_style_attr); - - active_style.forEach((style_class) => { - whiteBtn.classList.remove(style_class); - }); - - non_active_style.forEach((style_class) => { - whiteBtn.classList.add(style_class); - }); - - // change actual styles - - white_sidenav_classes.forEach((style_class) => { - sidenav.classList.remove(style_class); - }); - transparent_sidenav_classes.forEach((style_class) => { - sidenav.classList.add(style_class); - }); - - white_sidenav_highlighted.forEach((style_class) => { - sidenav_highlight.classList.remove(style_class); - }); - transparent_sidenav_highlighted.forEach((style_class) => { - sidenav_highlight.classList.add(style_class); - }); - for (var i = 0; i < sidenav_icons.length; i++) { - white_sidenav_icons.forEach((style_class) => { - sidenav_icons[i].classList.remove(style_class); - }); - transparent_sidenav_icons.forEach((style_class) => { - sidenav_icons[i].classList.add(style_class); - }); - } - } -}); - -whiteBtn.addEventListener("click", function () { - const active_style_attr = document.createAttribute("active-style"); - if (!this.hasAttribute(active_style_attr)) { - this.setAttributeNode(active_style_attr); - non_active_style.forEach((style_class) => { - this.classList.remove(style_class); - }); - active_style.forEach((style_class) => { - this.classList.add(style_class); - }); - - transparentBtn.removeAttribute(active_style_attr); - active_style.forEach((style_class) => { - transparentBtn.classList.remove(style_class); - }); - non_active_style.forEach((style_class) => { - transparentBtn.classList.add(style_class); - }); - - // change actual styles - - transparent_sidenav_classes.forEach((style_class) => { - sidenav.classList.remove(style_class); - }); - white_sidenav_classes.forEach((style_class) => { - sidenav.classList.add(style_class); - }); - - transparent_sidenav_highlighted.forEach((style_class) => { - sidenav_highlight.classList.remove(style_class); - }); - - white_sidenav_highlighted.forEach((style_class) => { - sidenav_highlight.classList.add(style_class); - }); - - for (var i = 0; i < sidenav_icons.length; i++) { - transparent_sidenav_icons.forEach((style_class) => { - sidenav_icons[i].classList.remove(style_class); - }); - white_sidenav_icons.forEach((style_class) => { - sidenav_icons[i].classList.add(style_class); - }); - } - } -}); - -// navbar fixed plugin - -if (navbar) { - if (navbar.getAttribute("navbar-scroll") == "true") { - buttonNavbarFixed.setAttribute("checked", "true"); - } - buttonNavbarFixed.addEventListener("change", function () { - if (this.checked) { - navbar.setAttribute("navbar-scroll", "true"); - navbar.classList.add("sticky"); - navbar.classList.add("top-[1%]"); - navbar.classList.add("backdrop-saturate-[200%]"); - navbar.classList.add("backdrop-blur-[30px]"); - navbar.classList.add("bg-[hsla(0,0%,100%,0.8)]"); - navbar.classList.add("shadow-blur"); - navbar.classList.add("z-110"); - } else { - navbar.setAttribute("navbar-scroll", "false"); - navbar.classList.remove("sticky"); - navbar.classList.remove("top-[1%]"); - navbar.classList.remove("backdrop-saturate-[200%]"); - navbar.classList.remove("backdrop-blur-[30px]"); - navbar.classList.remove("bg-[hsla(0,0%,100%,0.8)]"); - navbar.classList.remove("shadow-blur"); - navbar.classList.remove("z-110"); - } - }); -} else { - // buttonNavbarFixed.setAttribute("checked", "true"); - buttonNavbarFixed.setAttribute("disabled", "true"); -} diff --git a/src/main/resources/static/js/login.js b/src/main/resources/static/js/login.js deleted file mode 100644 index 7ab4938..0000000 --- a/src/main/resources/static/js/login.js +++ /dev/null @@ -1,17 +0,0 @@ -function validate() { - if (document.f.username.value == "" && document.f.password.value == "") { - alert("${noUser} and ${noPass}"); - document.f.username.focus(); - return false; - } - if (document.f.username.value == "") { - alert("${noUser}"); - document.f.username.focus(); - return false; - } - if (document.f.password.value == "") { - alert("${noPass}"); - document.f.password.focus(); - return false; - } -} \ No newline at end of file diff --git a/src/main/resources/static/js/nav-pills.js b/src/main/resources/static/js/nav-pills.js deleted file mode 100644 index 04584af..0000000 --- a/src/main/resources/static/js/nav-pills.js +++ /dev/null @@ -1,124 +0,0 @@ -// Tabs navigation - -var total = document.querySelectorAll("[nav-pills]"); - -total.forEach(function (item, i) { - var moving_div = document.createElement("div"); - var first_li = item.querySelector("li:first-child [nav-link]"); - var tab = first_li.cloneNode(); - tab.innerHTML = "-"; - tab.classList.remove("bg-inherit"); - tab.classList.add("bg-white", "text-white", "shadow-soft-xxs"); - tab.style.animation = ".2s ease"; - - moving_div.classList.add("z-10", "absolute", "text-slate-700", "rounded-lg", "bg-inherit", "flex-auto", "text-center", "bg-none", "border-0", "block"); - moving_div.setAttribute("moving-tab", ""); - moving_div.setAttribute("nav-link", ""); - moving_div.appendChild(tab); - item.appendChild(moving_div); - - var list_length = item.getElementsByTagName("li").length; - - moving_div.style.boxShadow = "0 1px 5px 1px #ddd"; - moving_div.style.padding = "0px"; - moving_div.style.width = item.querySelector("li:nth-child(1)").offsetWidth + "px"; - moving_div.style.transform = "translate3d(0px, 0px, 0px)"; - moving_div.style.transition = ".5s ease"; - - item.onmouseover = function (event) { - let target = getEventTarget(event); - let li = target.closest("li"); - if (li) { - let nodes = Array.from(li.closest("ul").children); - let index = nodes.indexOf(li) + 1; - item.querySelector("li:nth-child(" + index + ") [nav-link]").onclick = function () { - item.querySelectorAll("li").forEach(function (list_item) { - list_item.firstElementChild.removeAttribute("active"); - }); - li.firstElementChild.setAttribute("active", ""); - moving_div = item.querySelector("[moving-tab]"); - let sum = 0; - if (item.classList.contains("flex-col")) { - for (var j = 1; j <= nodes.indexOf(li); j++) { - sum += item.querySelector("li:nth-child(" + j + ")").offsetHeight; - } - moving_div.style.transform = "translate3d(0px," + sum + "px, 0px)"; - moving_div.style.height = item.querySelector("li:nth-child(" + j + ")").offsetHeight; - } else { - for (var j = 1; j <= nodes.indexOf(li); j++) { - sum += item.querySelector("li:nth-child(" + j + ")").offsetWidth; - } - moving_div.style.transform = "translate3d(" + sum + "px, 0px, 0px)"; - moving_div.style.width = item.querySelector("li:nth-child(" + index + ")").offsetWidth + "px"; - } - }; - } - }; -}); - -// Tabs navigation resize - -window.addEventListener("resize", function (event) { - total.forEach(function (item, i) { - item.querySelector("[moving-tab]").remove(); - var moving_div = document.createElement("div"); - var tab = item.querySelector("[nav-link][active]").cloneNode(); - tab.innerHTML = "-"; - tab.classList.remove("bg-inherit"); - tab.classList.add("bg-white", "text-white", "shadow-soft-xxs"); - tab.style.animation = ".2s ease"; - - moving_div.classList.add("z-10", "absolute", "text-slate-700", "rounded-lg", "bg-inherit", "flex-auto", "text-center", "bg-none", "border-0", "block"); - moving_div.setAttribute("moving-tab", ""); - moving_div.setAttribute("nav-link", ""); - moving_div.appendChild(tab); - - item.appendChild(moving_div); - - moving_div.style.boxShadow = "0 1px 5px 1px #ddd"; - moving_div.style.padding = "0px"; - moving_div.style.transition = ".5s ease"; - - let li = item.querySelector("[nav-link][active]").parentElement; - - if (li) { - let nodes = Array.from(li.closest("ul").children); - let index = nodes.indexOf(li) + 1; - - let sum = 0; - if (item.classList.contains("flex-col")) { - for (var j = 1; j <= nodes.indexOf(li); j++) { - sum += item.querySelector("li:nth-child(" + j + ")").offsetHeight; - } - moving_div.style.transform = "translate3d(0px," + sum + "px, 0px)"; - moving_div.style.width = item.querySelector("li:nth-child(" + index + ")").offsetWidth + "px"; - moving_div.style.height = item.querySelector("li:nth-child(" + j + ")").offsetHeight; - } else { - for (var j = 1; j <= nodes.indexOf(li); j++) { - sum += item.querySelector("li:nth-child(" + j + ")").offsetWidth; - } - moving_div.style.transform = "translate3d(" + sum + "px, 0px, 0px)"; - moving_div.style.width = item.querySelector("li:nth-child(" + index + ")").offsetWidth + "px"; - } - } - }); - - if (window.innerWidth < 991) { - total.forEach(function (item, i) { - if (!item.classList.contains("flex-col")) { - item.classList.add("flex-col", "on-resize"); - } - }); - } else { - total.forEach(function (item, i) { - if (item.classList.contains("on-resize")) { - item.classList.remove("flex-col", "on-resize"); - } - }); - } -}); - -function getEventTarget(e) { - e = e || window.event; - return e.target || e.srcElement; -} diff --git a/src/main/resources/static/js/navbar-collapse.js b/src/main/resources/static/js/navbar-collapse.js deleted file mode 100644 index 86a36fd..0000000 --- a/src/main/resources/static/js/navbar-collapse.js +++ /dev/null @@ -1,37 +0,0 @@ -var expand_trigger = document.querySelector("[navbar-trigger]"); -var bar1 = document.querySelector("[bar1]"); -var bar2 = document.querySelector("[bar2]"); -var bar3 = document.querySelector("[bar3]"); -var navbar_sign_in_up = document.querySelector("[navbar-menu]"); -const collapse_height = navbar_sign_in_up.scrollHeight; - -expand_trigger.addEventListener("click", function () { - elements = navbar_sign_in_up.querySelectorAll("a"); - if (navbar_sign_in_up.classList.contains("lg-max:max-h-0")) { - navbar_sign_in_up.classList.remove("lg-max:max-h-0"); - navbar_sign_in_up.classList.add("lg-max:max-h-54"); - setTimeout(function () { - elements.forEach((element) => { - element.classList.remove("lg-max:opacity-0"); - }); - }, 50); - } else { - setTimeout(function () { - elements.forEach((element) => { - element.classList.add("lg-max:opacity-0"); - }); - }, 100); - navbar_sign_in_up.classList.remove("lg-max:max-h-54"); - navbar_sign_in_up.classList.add("lg-max:max-h-0"); - } - bar1.classList.toggle("rotate-45"); - bar1.classList.toggle("origin-10-10"); - bar1.classList.toggle("mt-1"); - - bar2.classList.toggle("opacity-0"); - - bar3.classList.toggle("-rotate-45"); - bar3.classList.toggle("origin-10-90"); - bar3.classList.toggle("mt-0.75"); - bar3.classList.toggle("mt-1.75"); -}); diff --git a/src/main/resources/static/js/navbar-sticky.js b/src/main/resources/static/js/navbar-sticky.js deleted file mode 100644 index 1a962f2..0000000 --- a/src/main/resources/static/js/navbar-sticky.js +++ /dev/null @@ -1,16 +0,0 @@ -// Navbar stick on scroll ++ styles - -var navbar = document.querySelector("[navbar-main]"); - -window.onscroll = function () { - let blur = navbar.getAttribute("navbar-scroll"); - if (blur == "true") stickyNav(); -}; - -function stickyNav() { - if (window.scrollY >= 5) { - navbar.classList.add("sticky", "top-[1%]", "backdrop-saturate-[200%]", "backdrop-blur-[30px]", "bg-[hsla(0,0%,100%,0.8)]", "shadow-blur", "z-110"); - } else { - navbar.classList.remove("sticky", "top-[1%]", "backdrop-saturate-[200%]", "backdrop-blur-[30px]", "bg-[hsla(0,0%,100%,0.8)]", "shadow-blur", "z-110"); - } -} diff --git a/src/main/resources/static/js/perfect-scrollbar.js b/src/main/resources/static/js/perfect-scrollbar.js deleted file mode 100644 index 92795bd..0000000 --- a/src/main/resources/static/js/perfect-scrollbar.js +++ /dev/null @@ -1,36 +0,0 @@ -(function () { - var isWindows = navigator.platform.indexOf("Win") > -1 ? true : false; - - if (isWindows) { - // if we are on windows OS we activate the perfectScrollbar function - if (document.querySelector("main")) { - var mainpanel = document.querySelector("main"); - var ps = new PerfectScrollbar(mainpanel); - } - - if (document.querySelectorAll(".overflow-auto")[0]) { - var sidebar = document.querySelectorAll(".overflow-auto"); - var i = 0; - var ps; - sidebar.forEach((element) => { - ps[i++] = new PerfectScrollbar(element); - }); - } - if (document.querySelectorAll(".overflow-y-auto")[0]) { - var sidebar = document.querySelectorAll(".overflow-y-auto"); - var i = 0; - var ps; - sidebar.forEach((element) => { - ps[i++] = new PerfectScrollbar(element); - }); - } - if (document.querySelectorAll(".overflow-x-auto")[0]) { - var sidebar = document.querySelectorAll(".overflow-x-auto"); - var i = 0; - var ps; - sidebar.forEach((element) => { - ps[i++] = new PerfectScrollbar(element); - }); - } - } -})(); diff --git a/src/main/resources/static/js/plugins/Chart.extension.js b/src/main/resources/static/js/plugins/Chart.extension.js deleted file mode 100644 index 5e2fc0b..0000000 --- a/src/main/resources/static/js/plugins/Chart.extension.js +++ /dev/null @@ -1,128 +0,0 @@ -// -// Chart extension for making the bars rounded -// Code from: https://codepen.io/jedtrow/full/ygRYgo -// - -Chart.elements.Rectangle.prototype.draw = function () { - var ctx = this._chart.ctx; - var vm = this._view; - var left, right, top, bottom, signX, signY, borderSkipped, radius; - var borderWidth = vm.borderWidth; - // Set Radius Here - // If radius is large enough to cause drawing errors a max radius is imposed - var cornerRadius = 6; - - if (!vm.horizontal) { - // bar - left = vm.x - vm.width / 2; - right = vm.x + vm.width / 2; - top = vm.y; - bottom = vm.base; - signX = 1; - signY = bottom > top ? 1 : -1; - borderSkipped = vm.borderSkipped || "bottom"; - } else { - // horizontal bar - left = vm.base; - right = vm.x; - top = vm.y - vm.height / 2; - bottom = vm.y + vm.height / 2; - signX = right > left ? 1 : -1; - signY = 1; - borderSkipped = vm.borderSkipped || "left"; - } - - // Canvas doesn't allow us to stroke inside the width so we can - // adjust the sizes to fit if we're setting a stroke on the line - if (borderWidth) { - // borderWidth shold be less than bar width and bar height. - var barSize = Math.min(Math.abs(left - right), Math.abs(top - bottom)); - borderWidth = borderWidth > barSize ? barSize : borderWidth; - var halfStroke = borderWidth / 2; - // Adjust borderWidth when bar top position is near vm.base(zero). - var borderLeft = left + (borderSkipped !== "left" ? halfStroke * signX : 0); - var borderRight = right + (borderSkipped !== "right" ? -halfStroke * signX : 0); - var borderTop = top + (borderSkipped !== "top" ? halfStroke * signY : 0); - var borderBottom = bottom + (borderSkipped !== "bottom" ? -halfStroke * signY : 0); - // not become a vertical line? - if (borderLeft !== borderRight) { - top = borderTop; - bottom = borderBottom; - } - // not become a horizontal line? - if (borderTop !== borderBottom) { - left = borderLeft; - right = borderRight; - } - } - - ctx.beginPath(); - ctx.fillStyle = vm.backgroundColor; - ctx.strokeStyle = vm.borderColor; - ctx.lineWidth = borderWidth; - - // Corner points, from bottom-left to bottom-right clockwise - // | 1 2 | - // | 0 3 | - var corners = [ - [left, bottom], - [left, top], - [right, top], - [right, bottom], - ]; - - // Find first (starting) corner with fallback to 'bottom' - var borders = ["bottom", "left", "top", "right"]; - var startCorner = borders.indexOf(borderSkipped, 0); - if (startCorner === -1) { - startCorner = 0; - } - - function cornerAt(index) { - return corners[(startCorner + index) % 4]; - } - - // Draw rectangle from 'startCorner' - var corner = cornerAt(0); - ctx.moveTo(corner[0], corner[1]); - - for (var i = 1; i < 4; i++) { - corner = cornerAt(i); - nextCornerId = i + 1; - if (nextCornerId == 4) { - nextCornerId = 0; - } - - nextCorner = cornerAt(nextCornerId); - - width = corners[2][0] - corners[1][0]; - height = corners[0][1] - corners[1][1]; - x = corners[1][0]; - y = corners[1][1]; - - var radius = cornerRadius; - - // Fix radius being too large - if (radius > height / 2) { - radius = height / 2; - } - if (radius > width / 2) { - radius = width / 2; - } - - ctx.moveTo(x + radius, y); - ctx.lineTo(x + width - radius, y); - ctx.quadraticCurveTo(x + width, y, x + width, y + radius); - ctx.lineTo(x + width, y + height - radius); - ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); - ctx.lineTo(x + radius, y + height); - ctx.quadraticCurveTo(x, y + height, x, y + height - radius); - ctx.lineTo(x, y + radius); - ctx.quadraticCurveTo(x, y, x + radius, y); - } - - ctx.fill(); - if (borderWidth) { - ctx.stroke(); - } -}; diff --git a/src/main/resources/static/js/plugins/chartjs.min.js b/src/main/resources/static/js/plugins/chartjs.min.js deleted file mode 100644 index fb76619..0000000 --- a/src/main/resources/static/js/plugins/chartjs.min.js +++ /dev/null @@ -1,13 +0,0 @@ -/*! - * Chart.js v3.0.2 - * https://www.chartjs.org - * (c) 2021 Chart.js Contributors - * Released under the MIT License - */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";const t="undefined"==typeof window?function(t){return t()}:window.requestAnimationFrame;function e(e,i,n){const o=n||(t=>Array.prototype.slice.call(t));let s=!1,a=[];return function(...n){a=o(n),s||(s=!0,t.call(window,(()=>{s=!1,e.apply(i,a)})))}}function i(t,e){let i;return function(){return e?(clearTimeout(i),i=setTimeout(t,e)):t(),e}}const n=t=>"start"===t?"left":"end"===t?"right":"center",o=(t,e,i)=>"start"===t?e:"end"===t?i:(e+i)/2,s=(t,e,i)=>"right"===t?i:"center"===t?(e+i)/2:e;var a=new class{constructor(){this._request=null,this._charts=new Map,this._running=!1,this._lastDate=void 0}_notify(t,e,i,n){const o=e.listeners[n],s=e.duration;o.forEach((n=>n({chart:t,numSteps:s,currentStep:Math.min(i-e.start,s)})))}_refresh(){const e=this;e._request||(e._running=!0,e._request=t.call(window,(()=>{e._update(),e._request=null,e._running&&e._refresh()})))}_update(t=Date.now()){const e=this;let i=0;e._charts.forEach(((n,o)=>{if(!n.running||!n.items.length)return;const s=n.items;let a,r=s.length-1,l=!1;for(;r>=0;--r)a=s[r],a._active?(a._total>n.duration&&(n.duration=a._total),a.tick(t),l=!0):(s[r]=s[s.length-1],s.pop());l&&(o.draw(),e._notify(o,n,t,"progress")),s.length||(n.running=!1,e._notify(o,n,t,"complete")),i+=s.length})),e._lastDate=t,0===i&&(e._running=!1)}_getAnims(t){const e=this._charts;let i=e.get(t);return i||(i={running:!1,items:[],listeners:{complete:[],progress:[]}},e.set(t,i)),i}listen(t,e,i){this._getAnims(t).listeners[e].push(i)}add(t,e){e&&e.length&&this._getAnims(t).items.push(...e)}has(t){return this._getAnims(t).items.length>0}start(t){const e=this._charts.get(t);e&&(e.running=!0,e.start=Date.now(),e.duration=e.items.reduce(((t,e)=>Math.max(t,e._duration)),0),this._refresh())}running(t){if(!this._running)return!1;const e=this._charts.get(t);return!!(e&&e.running&&e.items.length)}stop(t){const e=this._charts.get(t);if(!e||!e.items.length)return;const i=e.items;let n=i.length-1;for(;n>=0;--n)i[n].cancel();e.items=[],this._notify(t,e,Date.now(),"complete")}remove(t){return this._charts.delete(t)}}; -/*! - * @kurkle/color v0.1.9 - * https://github.com/kurkle/color#readme - * (c) 2020 Jukka Kurkela - * Released under the MIT License - */const r={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},l="0123456789ABCDEF",c=t=>l[15&t],h=t=>l[(240&t)>>4]+l[15&t],d=t=>(240&t)>>4==(15&t);function u(t){var e=function(t){return d(t.r)&&d(t.g)&&d(t.b)&&d(t.a)}(t)?c:h;return t?"#"+e(t.r)+e(t.g)+e(t.b)+(t.a<255?e(t.a):""):t}function f(t){return t+.5|0}const g=(t,e,i)=>Math.max(Math.min(t,i),e);function p(t){return g(f(2.55*t),0,255)}function m(t){return g(f(255*t),0,255)}function x(t){return g(f(t/2.55)/100,0,1)}function b(t){return g(f(100*t),0,100)}const _=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;const y=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function v(t,e,i){const n=e*Math.min(i,1-i),o=(e,o=(e+t/30)%12)=>i-n*Math.max(Math.min(o-3,9-o,1),-1);return[o(0),o(8),o(4)]}function M(t,e,i){const n=(n,o=(n+t/60)%6)=>i-i*e*Math.max(Math.min(o,4-o,1),0);return[n(5),n(3),n(1)]}function w(t,e,i){const n=v(t,1,.5);let o;for(e+i>1&&(o=1/(e+i),e*=o,i*=o),o=0;o<3;o++)n[o]*=1-e-i,n[o]+=e;return n}function k(t){const e=t.r/255,i=t.g/255,n=t.b/255,o=Math.max(e,i,n),s=Math.min(e,i,n),a=(o+s)/2;let r,l,c;return o!==s&&(c=o-s,l=a>.5?c/(2-o-s):c/(o+s),r=o===e?(i-n)/c+(i>16&255,s>>8&255,255&s]}return t}(),T.transparent=[0,0,0,0]);const e=T[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}function R(t,e,i){if(t){let n=k(t);n[e]=Math.max(0,Math.min(n[e]+n[e]*i,0===e?360:1)),n=P(n),t.r=n[0],t.g=n[1],t.b=n[2]}}function E(t,e){return t?Object.assign(e||{},t):t}function I(t){var e={r:0,g:0,b:0,a:255};return Array.isArray(t)?t.length>=3&&(e={r:t[0],g:t[1],b:t[2],a:255},t.length>3&&(e.a=m(t[3]))):(e=E(t,{r:0,g:0,b:0,a:1})).a=m(e.a),e}function F(t){return"r"===t.charAt(0)?function(t){const e=_.exec(t);let i,n,o,s=255;if(e){if(e[7]!==i){const t=+e[7];s=255&(e[8]?p(t):255*t)}return i=+e[1],n=+e[3],o=+e[5],i=255&(e[2]?p(i):i),n=255&(e[4]?p(n):n),o=255&(e[6]?p(o):o),{r:i,g:n,b:o,a:s}}}(t):C(t)}class z{constructor(t){if(t instanceof z)return t;const e=typeof t;let i;var n,o,s;"object"===e?i=I(t):"string"===e&&(s=(n=t).length,"#"===n[0]&&(4===s||5===s?o={r:255&17*r[n[1]],g:255&17*r[n[2]],b:255&17*r[n[3]],a:5===s?17*r[n[4]]:255}:7!==s&&9!==s||(o={r:r[n[1]]<<4|r[n[2]],g:r[n[3]]<<4|r[n[4]],b:r[n[5]]<<4|r[n[6]],a:9===s?r[n[7]]<<4|r[n[8]]:255})),i=o||L(t)||F(t)),this._rgb=i,this._valid=!!i}get valid(){return this._valid}get rgb(){var t=E(this._rgb);return t&&(t.a=x(t.a)),t}set rgb(t){this._rgb=I(t)}rgbString(){return this._valid?(t=this._rgb)&&(t.a<255?`rgba(${t.r}, ${t.g}, ${t.b}, ${x(t.a)})`:`rgb(${t.r}, ${t.g}, ${t.b})`):this._rgb;var t}hexString(){return this._valid?u(this._rgb):this._rgb}hslString(){return this._valid?function(t){if(!t)return;const e=k(t),i=e[0],n=b(e[1]),o=b(e[2]);return t.a<255?`hsla(${i}, ${n}%, ${o}%, ${x(t.a)})`:`hsl(${i}, ${n}%, ${o}%)`}(this._rgb):this._rgb}mix(t,e){const i=this;if(t){const n=i.rgb,o=t.rgb;let s;const a=e===s?.5:e,r=2*a-1,l=n.a-o.a,c=((r*l==-1?r:(r+l)/(1+r*l))+1)/2;s=1-c,n.r=255&c*n.r+s*o.r+.5,n.g=255&c*n.g+s*o.g+.5,n.b=255&c*n.b+s*o.b+.5,n.a=a*n.a+(1-a)*o.a,i.rgb=n}return i}clone(){return new z(this.rgb)}alpha(t){return this._rgb.a=m(t),this}clearer(t){return this._rgb.a*=1-t,this}greyscale(){const t=this._rgb,e=f(.3*t.r+.59*t.g+.11*t.b);return t.r=t.g=t.b=e,this}opaquer(t){return this._rgb.a*=1+t,this}negate(){const t=this._rgb;return t.r=255-t.r,t.g=255-t.g,t.b=255-t.b,this}lighten(t){return R(this._rgb,2,t),this}darken(t){return R(this._rgb,2,-t),this}saturate(t){return R(this._rgb,1,t),this}desaturate(t){return R(this._rgb,1,-t),this}rotate(t){return function(t,e){var i=k(t);i[0]=D(i[0]+e),i=P(i),t.r=i[0],t.g=i[1],t.b=i[2]}(this._rgb,t),this}}function V(t){return new z(t)}const B=t=>t instanceof CanvasGradient||t instanceof CanvasPattern;function W(t){return B(t)?t:V(t)}function H(t){return B(t)?t:V(t).saturate(.5).darken(.1).hexString()}function N(){}const j=function(){let t=0;return function(){return t++}}();function $(t){return null==t}function Y(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.substr(0,7)&&"Array]"===e.substr(-6)}function U(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}const X=t=>("number"==typeof t||t instanceof Number)&&isFinite(+t);function q(t,e){return X(t)?t:e}function K(t,e){return void 0===t?e:t}const G=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:t/e,Z=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function Q(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function J(t,e,i,n){let o,s,a;if(Y(t))if(s=t.length,n)for(o=s-1;o>=0;o--)e.call(i,t[o],o);else for(o=0;oi;)t=t[e.substr(i,n-i)],i=n+1,n=rt(e,i);return t}function ct(t){return t.charAt(0).toUpperCase()+t.slice(1)}const ht=t=>void 0!==t,dt=t=>"function"==typeof t,ut=Object.create(null),ft=Object.create(null);function gt(t,e){if(!e)return t;const i=e.split(".");for(let e=0,n=i.length;et.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(t,e)=>H(e.backgroundColor),this.hoverBorderColor=(t,e)=>H(e.borderColor),this.hoverColor=(t,e)=>H(e.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.describe(t)}set(t,e){return pt(this,t,e)}get(t){return gt(this,t)}describe(t,e){return pt(ft,t,e)}override(t,e){return pt(ut,t,e)}route(t,e,i,n){const o=gt(this,t),s=gt(this,i),a="_"+e;Object.defineProperties(o,{[a]:{value:o[e],writable:!0},[e]:{enumerable:!0,get(){const t=this[a],e=s[n];return U(t)?Object.assign({},e,t):K(t,e)},set(t){this[a]=t}}})}}({_scriptable:t=>!t.startsWith("on"),_indexable:t=>"events"!==t,hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}});const xt=Math.PI,bt=2*xt,_t=bt+xt,yt=Number.POSITIVE_INFINITY,vt=xt/180,Mt=xt/2,wt=xt/4,kt=2*xt/3,St=Math.log10,Pt=Math.sign;function Dt(t){const e=Math.pow(10,Math.floor(St(t))),i=t/e;return(i<=1?1:i<=2?2:i<=5?5:10)*e}function Ct(t){const e=[],i=Math.sqrt(t);let n;for(n=1;nt-e)).pop(),e}function At(t){return!isNaN(parseFloat(t))&&isFinite(t)}function Ot(t,e,i){return Math.abs(t-e)=t}function Lt(t,e,i){let n,o,s;for(n=0,o=t.length;nr&&ln&&(n=s),n}function Yt(t,e,i,n){let o=(n=n||{}).data=n.data||{},s=n.garbageCollect=n.garbageCollect||[];n.font!==e&&(o=n.data={},s=n.garbageCollect=[],n.font=e),t.save(),t.font=e;let a=0;const r=i.length;let l,c,h,d,u;for(l=0;li.length){for(l=0;l0&&t.stroke()}}function Kt(t,e,i){return i=i||.5,t&&t.x>e.left-i&&t.xe.top-i&&t.y0&&""!==s.strokeColor;let l,c;for(t.save(),s.translation&&t.translate(s.translation[0],s.translation[1]),$(s.rotation)||t.rotate(s.rotation),t.font=o.string,s.color&&(t.fillStyle=s.color),s.textAlign&&(t.textAlign=s.textAlign),s.textBaseline&&(t.textBaseline=s.textBaseline),l=0;lt[i]1;)n=s+o>>1,i(n)?s=n:o=n;return{lo:s,hi:o}}const ie=(t,e,i)=>ee(t,i,(n=>t[n][e]ee(t,i,(n=>t[n][e]>=i));function oe(t,e,i){let n=0,o=t.length;for(;nn&&t[o-1]>i;)o--;return n>0||o{const i="_onData"+ct(e),n=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...e){const o=n.apply(this,e);return t._chartjs.listeners.forEach((t=>{"function"==typeof t[i]&&t[i](...e)})),o}})})))}function re(t,e){const i=t._chartjs;if(!i)return;const n=i.listeners,o=n.indexOf(e);-1!==o&&n.splice(o,1),n.length>0||(se.forEach((e=>{delete t[e]})),delete t._chartjs)}function le(t){const e=new Set;let i,n;for(i=0,n=t.length;i{o.push(t)})),o}function ce(t){let e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e}function he(t,e,i){let n;return"string"==typeof t?(n=parseInt(t,10),-1!==t.indexOf("%")&&(n=n/100*e.parentNode[i])):n=t,n}const de=t=>window.getComputedStyle(t,null);function ue(t,e){return de(t).getPropertyValue(e)}const fe=["top","right","bottom","left"];function ge(t,e,i){const n={};i=i?"-"+i:"";for(let o=0;o<4;o++){const s=fe[o];n[s]=parseFloat(t[e+"-"+s+i])||0}return n.width=n.left+n.right,n.height=n.top+n.bottom,n}function pe(t,e){const{canvas:i,currentDevicePixelRatio:n}=e,o=de(i),s="border-box"===o.boxSizing,a=ge(o,"padding"),r=ge(o,"border","width"),{x:l,y:c,box:h}=function(t,e){const i=t.native||t,n=i.touches,o=n&&n.length?n[0]:i,{offsetX:s,offsetY:a}=o;let r,l,c=!1;if(((t,e,i)=>(t>0||e>0)&&(!i||!i.shadowRoot))(s,a,i.target))r=s,l=a;else{const t=e.getBoundingClientRect();r=o.clientX-t.left,l=o.clientY-t.top,c=!0}return{x:r,y:l,box:c}}(t,i),d=a.left+(h&&r.left),u=a.top+(h&&r.top);let{width:f,height:g}=e;return s&&(f-=a.width+r.width,g-=a.height+r.height),{x:Math.round((l-d)/f*i.width/n),y:Math.round((c-u)/g*i.height/n)}}const me=t=>Math.round(10*t)/10;function xe(t,e,i,n){const o=de(t),s=ge(o,"margin"),a=he(o.maxWidth,t,"clientWidth")||yt,r=he(o.maxHeight,t,"clientHeight")||yt,l=function(t,e,i){let n,o;if(void 0===e||void 0===i){const s=ce(t);if(s){const t=s.getBoundingClientRect(),a=de(s),r=ge(a,"border","width"),l=ge(a,"padding");e=t.width-l.width-r.width,i=t.height-l.height-r.height,n=he(a.maxWidth,s,"clientWidth"),o=he(a.maxHeight,s,"clientHeight")}else e=t.clientWidth,i=t.clientHeight}return{width:e,height:i,maxWidth:n||yt,maxHeight:o||yt}}(t,e,i);let{width:c,height:h}=l;if("content-box"===o.boxSizing){const t=ge(o,"border","width"),e=ge(o,"padding");c-=e.width+t.width,h-=e.height+t.height}return c=Math.max(0,c-s.width),h=Math.max(0,n?Math.floor(c/n):h-s.height),c=me(Math.min(c,a,l.maxWidth)),h=me(Math.min(h,r,l.maxHeight)),c&&!h&&(h=me(c/2)),{width:c,height:h}}function be(t,e,i){const n=t.currentDevicePixelRatio=e||1,{canvas:o,width:s,height:a}=t;o.height=a*n,o.width=s*n,t.ctx.setTransform(n,0,0,n,0,0),o.style&&(i||!o.style.height&&!o.style.width)&&(o.style.height=a+"px",o.style.width=s+"px")}const _e=function(){let t=!1;try{const e={get passive(){return t=!0,!1}};window.addEventListener("test",null,e),window.removeEventListener("test",null,e)}catch(t){}return t}();function ye(t,e){const i=ue(t,e),n=i&&i.match(/^(\d+)(\.\d+)?px$/);return n?+n[1]:void 0}function ve(t,e){return"native"in t?{x:t.x,y:t.y}:pe(t,e)}function Me(t,e,i,n){const{controller:o,data:s,_sorted:a}=t,r=o._cachedMeta.iScale;if(r&&e===r.axis&&a&&s.length){const t=r._reversePixels?ne:ie;if(!n)return t(s,e,i);if(o._sharedOptions){const n=s[0],o="function"==typeof n.getRange&&n.getRange(e);if(o){const n=t(s,e,i-o),a=t(s,e,i+o);return{lo:n.lo,hi:a.hi}}}}return{lo:0,hi:s.length-1}}function we(t,e,i,n,o){const s=t.getSortedVisibleDatasetMetas(),a=i[e];for(let t=0,i=s.length;t{t[r](o[a],n)&&s.push({element:t,datasetIndex:e,index:i}),t.inRange(o.x,o.y,n)&&(l=!0)})),i.intersect&&!l?[]:s}var De={modes:{index(t,e,i,n){const o=ve(e,t),s=i.axis||"x",a=i.intersect?ke(t,o,s,n):Se(t,o,s,!1,n),r=[];return a.length?(t.getSortedVisibleDatasetMetas().forEach((t=>{const e=a[0].index,i=t.data[e];i&&!i.skip&&r.push({element:i,datasetIndex:t.index,index:e})})),r):[]},dataset(t,e,i,n){const o=ve(e,t),s=i.axis||"xy";let a=i.intersect?ke(t,o,s,n):Se(t,o,s,!1,n);if(a.length>0){const e=a[0].datasetIndex,i=t.getDatasetMeta(e).data;a=[];for(let t=0;tke(t,ve(e,t),i.axis||"xy",n),nearest:(t,e,i,n)=>Se(t,ve(e,t),i.axis||"xy",i.intersect,n),x:(t,e,i,n)=>(i.axis="x",Pe(t,e,i,n)),y:(t,e,i,n)=>(i.axis="y",Pe(t,e,i,n))}};const Ce=new RegExp(/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/);function Ae(t,e){const i=(""+t).match(Ce);if(!i||"normal"===i[1])return 1.2*e;switch(t=+i[2],i[3]){case"px":return t;case"%":t/=100}return e*t}function Oe(t,e){const i={},n=U(e),o=n?Object.keys(e):e,s=U(t)?n?i=>K(t[i],t[e[i]]):e=>t[e]:()=>t;for(const t of o)i[t]=+s(t)||0;return i}function Te(t){return Oe(t,{top:"y",right:"x",bottom:"y",left:"x"})}function Le(t){return Oe(t,["topLeft","topRight","bottomLeft","bottomRight"])}function Re(t){const e=Te(t);return e.width=e.left+e.right,e.height=e.top+e.bottom,e}function Ee(t,e){t=t||{},e=e||mt.font;let i=K(t.size,e.size);"string"==typeof i&&(i=parseInt(i,10));const n={family:K(t.family,e.family),lineHeight:Ae(K(t.lineHeight,e.lineHeight),i),size:i,style:K(t.style,e.style),weight:K(t.weight,e.weight),string:""};return n.string=jt(n),n}function Ie(t,e,i,n){let o,s,a,r=!0;for(o=0,s=t.length;ot.pos===e))}function Be(t,e){return t.filter((t=>-1===ze.indexOf(t.pos)&&t.box.axis===e))}function We(t,e){return t.sort(((t,i)=>{const n=e?i:t,o=e?t:i;return n.weight===o.weight?n.index-o.index:n.weight-o.weight}))}function He(t,e,i,n){return Math.max(t[i],e[i])+Math.max(t[n],e[n])}function Ne(t,e){t.top=Math.max(t.top,e.top),t.left=Math.max(t.left,e.left),t.bottom=Math.max(t.bottom,e.bottom),t.right=Math.max(t.right,e.right)}function je(t,e,i){const n=i.box,o=t.maxPadding;if(U(i.pos))return{same:!1,other:!1};i.size&&(t[i.pos]-=i.size),i.size=i.horizontal?n.height:n.width,t[i.pos]+=i.size,n.getPadding&&Ne(o,n.getPadding());const s=Math.max(0,e.outerWidth-He(o,t,"left","right")),a=Math.max(0,e.outerHeight-He(o,t,"top","bottom")),r=s!==t.w,l=a!==t.h;return t.w=s,t.h=a,i.horizontal?{same:r,other:l}:{same:l,other:r}}function $e(t,e){const i=e.maxPadding;function n(t){const n={left:0,top:0,right:0,bottom:0};return t.forEach((t=>{n[t]=Math.max(e[t],i[t])})),n}return n(t?["left","right"]:["top","bottom"])}function Ye(t,e,i){const n=[];let o,s,a,r,l,c;for(o=0,s=t.length,l=0;ot.box.fullSize)),!0),n=We(Ve(e,"left"),!0),o=We(Ve(e,"right")),s=We(Ve(e,"top"),!0),a=We(Ve(e,"bottom")),r=Be(e,"x"),l=Be(e,"y");return{fullSize:i,leftAndTop:n.concat(s),rightAndBottom:o.concat(l).concat(a).concat(r),chartArea:Ve(e,"chartArea"),vertical:n.concat(o).concat(l),horizontal:s.concat(a).concat(r)}}(t.boxes),l=r.vertical,c=r.horizontal;J(t.boxes,(t=>{"function"==typeof t.beforeLayout&&t.beforeLayout()}));const h=l.reduce(((t,e)=>e.box.options&&!1===e.box.options.display?t:t+1),0)||1,d=Object.freeze({outerWidth:e,outerHeight:i,padding:o,availableWidth:s,availableHeight:a,vBoxMaxWidth:s/2/h,hBoxMaxHeight:a/2}),u=Object.assign({},o);Ne(u,Re(n));const f=Object.assign({maxPadding:u,w:s,h:a,x:o.left,y:o.top},o);!function(t,e){let i,n,o;for(i=0,n=t.length;i{const i=e.box;Object.assign(i,t.chartArea),i.update(f.w,f.h)}))}};class qe{acquireContext(t,e){}releaseContext(t){return!1}addEventListener(t,e,i){}removeEventListener(t,e,i){}getDevicePixelRatio(){return 1}getMaximumSize(t,e,i,n){return e=Math.max(0,e||t.width),i=i||t.height,{width:e,height:Math.max(0,n?Math.floor(e/n):i)}}isAttached(t){return!0}}class Ke extends qe{acquireContext(t){return t&&t.getContext&&t.getContext("2d")||null}}const Ge={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},Ze=t=>null===t||""===t;const Qe=!!_e&&{passive:!0};function Je(t,e,i){t.canvas.removeEventListener(e,i,Qe)}function ti(t,e,i){const n=t.canvas,o=n&&ce(n)||n,s=new MutationObserver((t=>{const e=ce(o);t.forEach((t=>{for(let n=0;n{t.forEach((t=>{for(let e=0;e{i.currentDevicePixelRatio!==t&&e()})))}function si(t,i,n){const o=t.canvas,s=o&&ce(o);if(!s)return;const a=e(((t,e)=>{const i=s.clientWidth;n(t,e),i{const e=t[0],i=e.contentRect.width,n=e.contentRect.height;0===i&&0===n||a(i,n)}));return r.observe(s),function(t,e){ii.size||window.addEventListener("resize",oi),ii.set(t,e)}(t,a),r}function ai(t,e,i){i&&i.disconnect(),"resize"===e&&function(t){ii.delete(t),ii.size||window.removeEventListener("resize",oi)}(t)}function ri(t,i,n){const o=t.canvas,s=e((e=>{null!==t.ctx&&n(function(t,e){const i=Ge[t.type]||t.type,{x:n,y:o}=pe(t,e);return{type:i,chart:e,native:t,x:void 0!==n?n:null,y:void 0!==o?o:null}}(e,t))}),t,(t=>{const e=t[0];return[e,e.offsetX,e.offsetY]}));return function(t,e,i){t.addEventListener(e,i,Qe)}(o,i,s),s}class li extends qe{acquireContext(t,e){const i=t&&t.getContext&&t.getContext("2d");return i&&i.canvas===t?(function(t,e){const i=t.style,n=t.getAttribute("height"),o=t.getAttribute("width");if(t.$chartjs={initial:{height:n,width:o,style:{display:i.display,height:i.height,width:i.width}}},i.display=i.display||"block",i.boxSizing=i.boxSizing||"border-box",Ze(o)){const e=ye(t,"width");void 0!==e&&(t.width=e)}if(Ze(n))if(""===t.style.height)t.height=t.width/(e||2);else{const e=ye(t,"height");void 0!==e&&(t.height=e)}}(t,e),i):null}releaseContext(t){const e=t.canvas;if(!e.$chartjs)return!1;const i=e.$chartjs.initial;["height","width"].forEach((t=>{const n=i[t];$(n)?e.removeAttribute(t):e.setAttribute(t,n)}));const n=i.style||{};return Object.keys(n).forEach((t=>{e.style[t]=n[t]})),e.width=e.width,delete e.$chartjs,!0}addEventListener(t,e,i){this.removeEventListener(t,e);const n=t.$proxies||(t.$proxies={}),o={attach:ti,detach:ei,resize:si}[e]||ri;n[e]=o(t,e,i)}removeEventListener(t,e){const i=t.$proxies||(t.$proxies={}),n=i[e];if(!n)return;({attach:ai,detach:ai,resize:ai}[e]||Je)(t,e,n),i[e]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(t,e,i,n){return xe(t,e,i,n)}isAttached(t){const e=ce(t);return!(!e||!ce(e))}}var ci=Object.freeze({__proto__:null,BasePlatform:qe,BasicPlatform:Ke,DomPlatform:li});const hi=t=>0===t||1===t,di=(t,e,i)=>-Math.pow(2,10*(t-=1))*Math.sin((t-e)*bt/i),ui=(t,e,i)=>Math.pow(2,-10*t)*Math.sin((t-e)*bt/i)+1,fi={linear:t=>t,easeInQuad:t=>t*t,easeOutQuad:t=>-t*(t-2),easeInOutQuad:t=>(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1),easeInCubic:t=>t*t*t,easeOutCubic:t=>(t-=1)*t*t+1,easeInOutCubic:t=>(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2),easeInQuart:t=>t*t*t*t,easeOutQuart:t=>-((t-=1)*t*t*t-1),easeInOutQuart:t=>(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2),easeInQuint:t=>t*t*t*t*t,easeOutQuint:t=>(t-=1)*t*t*t*t+1,easeInOutQuint:t=>(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2),easeInSine:t=>1-Math.cos(t*Mt),easeOutSine:t=>Math.sin(t*Mt),easeInOutSine:t=>-.5*(Math.cos(xt*t)-1),easeInExpo:t=>0===t?0:Math.pow(2,10*(t-1)),easeOutExpo:t=>1===t?1:1-Math.pow(2,-10*t),easeInOutExpo:t=>hi(t)?t:t<.5?.5*Math.pow(2,10*(2*t-1)):.5*(2-Math.pow(2,-10*(2*t-1))),easeInCirc:t=>t>=1?t:-(Math.sqrt(1-t*t)-1),easeOutCirc:t=>Math.sqrt(1-(t-=1)*t),easeInOutCirc:t=>(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1),easeInElastic:t=>hi(t)?t:di(t,.075,.3),easeOutElastic:t=>hi(t)?t:ui(t,.075,.3),easeInOutElastic(t){const e=.1125;return hi(t)?t:t<.5?.5*di(2*t,e,.45):.5+.5*ui(2*t-1,e,.45)},easeInBack(t){const e=1.70158;return t*t*((e+1)*t-e)},easeOutBack(t){const e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack(t){let e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:t=>1-fi.easeOutBounce(1-t),easeOutBounce(t){const e=7.5625,i=2.75;return t<1/i?e*t*t:t<2/i?e*(t-=1.5/i)*t+.75:t<2.5/i?e*(t-=2.25/i)*t+.9375:e*(t-=2.625/i)*t+.984375},easeInOutBounce:t=>t<.5?.5*fi.easeInBounce(2*t):.5*fi.easeOutBounce(2*t-1)+.5},gi="transparent",pi={boolean:(t,e,i)=>i>.5?e:t,color(t,e,i){const n=W(t||gi),o=n.valid&&W(e||gi);return o&&o.valid?o.mix(n,i).hexString():e},number:(t,e,i)=>t+(e-t)*i};class mi{constructor(t,e,i,n){const o=e[i];n=Ie([t.to,n,o,t.from]);const s=Ie([t.from,o,n]);this._active=!0,this._fn=t.fn||pi[t.type||typeof s],this._easing=fi[t.easing]||fi.linear,this._start=Math.floor(Date.now()+(t.delay||0)),this._duration=this._total=Math.floor(t.duration),this._loop=!!t.loop,this._target=e,this._prop=i,this._from=s,this._to=n,this._promises=void 0}active(){return this._active}update(t,e,i){const n=this;if(n._active){n._notify(!1);const o=n._target[n._prop],s=i-n._start,a=n._duration-s;n._start=i,n._duration=Math.floor(Math.max(a,t.duration)),n._total+=s,n._loop=!!t.loop,n._to=Ie([t.to,e,o,t.from]),n._from=Ie([t.from,o,e])}}cancel(){const t=this;t._active&&(t.tick(Date.now()),t._active=!1,t._notify(!1))}tick(t){const e=this,i=t-e._start,n=e._duration,o=e._prop,s=e._from,a=e._loop,r=e._to;let l;if(e._active=s!==r&&(a||i1?2-l:l,l=e._easing(Math.min(1,Math.max(0,l))),e._target[o]=e._fn(s,r,l))}wait(){const t=this._promises||(this._promises=[]);return new Promise(((e,i)=>{t.push({res:e,rej:i})}))}_notify(t){const e=t?"res":"rej",i=this._promises||[];for(let t=0;t"onProgress"!==t&&"onComplete"!==t&&"fn"!==t}),mt.set("animations",{colors:{type:"color",properties:["color","borderColor","backgroundColor"]},numbers:{type:"number",properties:["x","y","borderWidth","radius","tension"]}}),mt.describe("animations",{_fallback:"animation"}),mt.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:t=>0|t}}}});class bi{constructor(t,e){this._chart=t,this._properties=new Map,this.configure(e)}configure(t){if(!U(t))return;const e=this._properties;Object.getOwnPropertyNames(t).forEach((i=>{const n=t[i];if(!U(n))return;const o={};for(const t of xi)o[t]=n[t];(Y(n.properties)&&n.properties||[i]).forEach((t=>{t!==i&&e.has(t)||e.set(t,o)}))}))}_animateOptions(t,e){const i=e.options,n=function(t,e){if(!e)return;let i=t.options;if(!i)return void(t.options=e);i.$shared&&(t.options=i=Object.assign({},i,{$shared:!1,$animations:{}}));return i}(t,i);if(!n)return[];const o=this._createAnimations(n,i);return i.$shared&&function(t,e){const i=[],n=Object.keys(e);for(let e=0;e{t.options=i}),(()=>{})),o}_createAnimations(t,e){const i=this._properties,n=[],o=t.$animations||(t.$animations={}),s=Object.keys(e),a=Date.now();let r;for(r=s.length-1;r>=0;--r){const l=s[r];if("$"===l.charAt(0))continue;if("options"===l){n.push(...this._animateOptions(t,e));continue}const c=e[l];let h=o[l];const d=i.get(l);if(h){if(d&&h.active()){h.update(d,c,a);continue}h.cancel()}d&&d.duration?(o[l]=h=new mi(d,t,l,c),n.push(h)):t[l]=c}return n}update(t,e){if(0===this._properties.size)return void Object.assign(t,e);const i=this._createAnimations(t,e);return i.length?(a.add(this._chart,i),!0):void 0}}function _i(t,e){const i=t&&t.options||{},n=i.reverse,o=void 0===i.min?e:0,s=void 0===i.max?e:0;return{start:n?s:o,end:n?o:s}}function yi(t,e){const i=[],n=t._getSortedDatasetMetas(e);let o,s;for(o=0,s=n.length;oi[t].axis===e)).shift()}function Pi(t,e){e=e||t._parsed;for(const i of e){const e=i._stacks;if(!e||void 0===e[t.vScale.id]||void 0===e[t.vScale.id][t.index])return;delete e[t.vScale.id][t.index]}}const Di=t=>"reset"===t||"none"===t,Ci=(t,e)=>e?t:Object.assign({},t);class Ai{constructor(t,e){this.chart=t,this._ctx=t.ctx,this.index=e,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.$context=void 0,this.initialize()}initialize(){const t=this,e=t._cachedMeta;t.configure(),t.linkScales(),e._stacked=Mi(e.vScale,e),t.addElements()}updateIndex(t){this.index=t}linkScales(){const t=this,e=t.chart,i=t._cachedMeta,n=t.getDataset(),o=(t,e,i,n)=>"x"===t?e:"r"===t?n:i,s=i.xAxisID=K(n.xAxisID,Si(e,"x")),a=i.yAxisID=K(n.yAxisID,Si(e,"y")),r=i.rAxisID=K(n.rAxisID,Si(e,"r")),l=i.indexAxis,c=i.iAxisID=o(l,s,a,r),h=i.vAxisID=o(l,a,s,r);i.xScale=t.getScaleForId(s),i.yScale=t.getScaleForId(a),i.rScale=t.getScaleForId(r),i.iScale=t.getScaleForId(c),i.vScale=t.getScaleForId(h)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(t){return this.chart.scales[t]}_getOtherScale(t){const e=this._cachedMeta;return t===e.iScale?e.vScale:e.iScale}reset(){this._update("reset")}_destroy(){const t=this._cachedMeta;this._data&&re(this._data,this),t._stacked&&Pi(t)}_dataCheck(){const t=this,e=t.getDataset(),i=e.data||(e.data=[]);U(i)?t._data=function(t){const e=Object.keys(t),i=new Array(e.length);let n,o,s;for(n=0,o=e.length;n0&&n._parsed[t-1];if(!1===i._parsing)n._parsed=o,n._sorted=!0;else{h=Y(o[t])?i.parseArrayData(n,o,t,e):U(o[t])?i.parseObjectData(n,o,t,e):i.parsePrimitiveData(n,o,t,e);const s=()=>null===c[r]||u&&c[r]p||d=0;--u)if(!m()){i.updateRangeFromParsed(c,t,g,l);break}return c}getAllParsedValues(t){const e=this._cachedMeta._parsed,i=[];let n,o,s;for(n=0,o=e.length;n=0&&tn.getContext(i,o)),d);return g.$shared&&(g.$shared=l,s[a]=Object.freeze(Ci(g,l))),g}_resolveAnimations(t,e,i){const n=this,o=n.chart,s=n._cachedDataOpts,a="animation-"+e,r=s[a];if(r)return r;let l;if(!1!==o.options.animation){const o=n.chart.config,s=o.datasetAnimationScopeKeys(n._type,e),a=o.getOptionScopes(n.getDataset(),s);l=o.createResolver(a,n.getContext(t,i,e))}const c=new bi(o,l&&l.animations);return l&&l._cacheable&&(s[a]=Object.freeze(c)),c}getSharedOptions(t){if(t.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},t))}includeOptions(t,e){return!e||Di(t)||this.chart._animationsDisabled}updateElement(t,e,i,n){Di(n)?Object.assign(t,i):this._resolveAnimations(e,n).update(t,i)}updateSharedOptions(t,e,i){t&&!Di(e)&&this._resolveAnimations(void 0,e).update(t,i)}_setStyle(t,e,i,n){t.active=n;const o=this.getStyle(e,n);this._resolveAnimations(e,i,n).update(t,{options:!n&&this.getSharedOptions(o)||o})}removeHoverStyle(t,e,i){this._setStyle(t,i,"active",!1)}setHoverStyle(t,e,i){this._setStyle(t,i,"active",!0)}_removeDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!1)}_setDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!0)}_resyncElements(t){const e=this,i=e._cachedMeta.data.length,n=e._data.length;n>i?e._insertElements(i,n-i,t):n{for(t.length+=e,r=t.length-1;r>=a;r--)t[r]=t[r-e]};for(l(s),r=t;r{o[t]=n[t]&&n[t].active()?n[t]._to:i[t]})),o}}Oi.defaults={},Oi.defaultRoutes=void 0;const Ti=new Map;function Li(t,e,i){return function(t,e){e=e||{};const i=t+JSON.stringify(e);let n=Ti.get(i);return n||(n=new Intl.NumberFormat(t,e),Ti.set(i,n)),n}(e,i).format(t)}const Ri={values:t=>Y(t)?t:""+t,numeric(t,e,i){if(0===t)return"0";const n=this.chart.options.locale;let o,s=t;if(i.length>1){const e=Math.max(Math.abs(i[0].value),Math.abs(i[i.length-1].value));(e<1e-4||e>1e15)&&(o="scientific"),s=function(t,e){let i=e.length>3?e[2].value-e[1].value:e[1].value-e[0].value;Math.abs(i)>1&&t!==Math.floor(t)&&(i=t-Math.floor(t));return i}(t,i)}const a=St(Math.abs(s)),r=Math.max(Math.min(-1*Math.floor(a),20),0),l={notation:o,minimumFractionDigits:r,maximumFractionDigits:r};return Object.assign(l,this.options.ticks.format),Li(t,n,l)},logarithmic(t,e,i){if(0===t)return"0";const n=t/Math.pow(10,Math.floor(St(t)));return 1===n||2===n||5===n?Ri.numeric.call(this,t,e,i):""}};var Ei={formatters:Ri};function Ii(t,e){const i=t.options.ticks,n=i.maxTicksLimit||function(t){const e=t.options.offset,i=t._tickSize(),n=t._length/i+(e?0:1),o=t._maxLength/i;return Math.floor(Math.min(n,o))}(t),o=i.major.enabled?function(t){const e=[];let i,n;for(i=0,n=t.length;in)return function(t,e,i,n){let o,s=0,a=i[0];for(n=Math.ceil(n),o=0;oo)return e}return Math.max(o,1)}(o,e,n);if(s>0){let t,i;const n=s>1?Math.round((r-a)/(s-1)):null;for(Fi(e,l,c,$(n)?0:a-n,a),t=0,i=s-1;te.lineWidth,tickColor:(t,e)=>e.color,offset:!1,borderDash:[],borderDashOffset:0,borderColor:(t,e)=>e.color,borderWidth:(t,e)=>e.lineWidth},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:Ei.formatters.values,minor:{},major:{},align:"center",crossAlign:"near"}}),mt.route("scale.ticks","color","","color"),mt.route("scale.grid","color","","borderColor"),mt.route("scale.title","color","","color"),mt.describe("scale",{_fallback:!1,_scriptable:t=>!t.startsWith("before")&&!t.startsWith("after")&&"callback"!==t&&"parser"!==t,_indexable:t=>"borderDash"!==t&&"tickBorderDash"!==t}),mt.describe("scales",{_fallback:"scale"});const zi=(t,e,i)=>"top"===e||"left"===e?t[e]+i:t[e]-i;function Vi(t,e){const i=[],n=t.length/e,o=t.length;let s=0;for(;sa+r)))return c}function Wi(t){return t.drawTicks?t.tickLength:0}function Hi(t,e){if(!t.display)return 0;const i=Ee(t.font,e),n=Re(t.padding);return(Y(t.text)?t.text.length:1)*i.lineHeight+n.height}function Ni(t,e,i){let o=n(t);return(i&&"right"!==e||!i&&"right"===e)&&(o=(t=>"left"===t?"right":"right"===t?"left":t)(o)),o}class ji extends Oi{constructor(t){super(),this.id=t.id,this.type=t.type,this.options=void 0,this.ctx=t.ctx,this.chart=t.chart,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this._margins={left:0,right:0,top:0,bottom:0},this.maxWidth=void 0,this.maxHeight=void 0,this.paddingTop=void 0,this.paddingBottom=void 0,this.paddingLeft=void 0,this.paddingRight=void 0,this.axis=void 0,this.labelRotation=void 0,this.min=void 0,this.max=void 0,this.ticks=[],this._gridLineItems=null,this._labelItems=null,this._labelSizes=null,this._length=0,this._maxLength=0,this._longestTextCache={},this._startPixel=void 0,this._endPixel=void 0,this._reversePixels=!1,this._userMax=void 0,this._userMin=void 0,this._suggestedMax=void 0,this._suggestedMin=void 0,this._ticksLength=0,this._borderValue=0,this._cache={},this._dataLimitsCached=!1,this.$context=void 0}init(t){const e=this;e.options=t,e.axis=t.axis,e._userMin=e.parse(t.min),e._userMax=e.parse(t.max),e._suggestedMin=e.parse(t.suggestedMin),e._suggestedMax=e.parse(t.suggestedMax)}parse(t,e){return t}getUserBounds(){let{_userMin:t,_userMax:e,_suggestedMin:i,_suggestedMax:n}=this;return t=q(t,Number.POSITIVE_INFINITY),e=q(e,Number.NEGATIVE_INFINITY),i=q(i,Number.POSITIVE_INFINITY),n=q(n,Number.NEGATIVE_INFINITY),{min:q(t,i),max:q(e,n),minDefined:X(t),maxDefined:X(e)}}getMinMax(t){const e=this;let i,{min:n,max:o,minDefined:s,maxDefined:a}=e.getUserBounds();if(s&&a)return{min:n,max:o};const r=e.getMatchingVisibleMetas();for(let l=0,c=r.length;l=s||n<=1||!t.isHorizontal())return void(t.labelRotation=o);const h=t._getLabelSizes(),d=h.widest.width,u=h.highest.height,f=Ht(t.chart.width-d,0,t.maxWidth);a=e.offset?t.maxWidth/n:f/(n-1),d+6>a&&(a=f/(n-(e.offset?.5:1)),r=t.maxHeight-Wi(e.grid)-i.padding-Hi(e.title,t.chart.options.font),l=Math.sqrt(d*d+u*u),c=Et(Math.min(Math.asin(Math.min((h.highest.height+6)/a,1)),Math.asin(Math.min(r/l,1))-Math.asin(u/l))),c=Math.max(o,Math.min(s,c))),t.labelRotation=c}afterCalculateLabelRotation(){Q(this.options.afterCalculateLabelRotation,[this])}beforeFit(){Q(this.options.beforeFit,[this])}fit(){const t=this,e={width:0,height:0},{chart:i,options:{ticks:n,title:o,grid:s}}=t,a=t._isVisible(),r=t.isHorizontal();if(a){const a=Hi(o,i.options.font);if(r?(e.width=t.maxWidth,e.height=Wi(s)+a):(e.height=t.maxHeight,e.width=Wi(s)+a),n.display&&t.ticks.length){const{first:i,last:o,widest:s,highest:a}=t._getLabelSizes(),l=2*n.padding,c=Rt(t.labelRotation),h=Math.cos(c),d=Math.sin(c);if(r){const i=d*s.width+h*a.height;e.height=Math.min(t.maxHeight,e.height+i+l)}else{const i=n.mirror?0:h*s.width+d*a.height;e.width=Math.min(t.maxWidth,e.width+i+l)}t._calculatePadding(i,o,d,h)}}t._handleMargins(),r?(t.width=t._length=i.width-t._margins.left-t._margins.right,t.height=e.height):(t.width=e.width,t.height=t._length=i.height-t._margins.top-t._margins.bottom)}_calculatePadding(t,e,i,n){const o=this,{ticks:{align:s,padding:a},position:r}=o.options,l=0!==o.labelRotation,c="top"!==r&&"x"===o.axis;if(o.isHorizontal()){const r=o.getPixelForTick(0)-o.left,h=o.right-o.getPixelForTick(o.ticks.length-1);let d=0,u=0;l?c?(d=n*t.width,u=i*e.height):(d=i*t.height,u=n*e.width):"start"===s?u=e.width:"end"===s?d=t.width:(d=t.width/2,u=e.width/2),o.paddingLeft=Math.max((d-r+a)*o.width/(o.width-r),0),o.paddingRight=Math.max((u-h+a)*o.width/(o.width-h),0)}else{let i=e.height/2,n=t.height/2;"start"===s?(i=0,n=t.height):"end"===s&&(i=e.height,n=0),o.paddingTop=i+a,o.paddingBottom=n+a}}_handleMargins(){const t=this;t._margins&&(t._margins.left=Math.max(t.paddingLeft,t._margins.left),t._margins.top=Math.max(t.paddingTop,t._margins.top),t._margins.right=Math.max(t.paddingRight,t._margins.right),t._margins.bottom=Math.max(t.paddingBottom,t._margins.bottom))}afterFit(){Q(this.options.afterFit,[this])}isHorizontal(){const{axis:t,position:e}=this.options;return"top"===e||"bottom"===e||"x"===t}isFullSize(){return this.options.fullSize}_convertTicksToLabels(t){const e=this;e.beforeTickToLabelConversion(),e.generateTickLabels(t),e.afterTickToLabelConversion()}_getLabelSizes(){const t=this;let e=t._labelSizes;if(!e){const i=t.options.ticks.sampleSize;let n=t.ticks;i{const i=t.gc,n=i.length/2;let o;if(n>e){for(o=0;o({width:o[t]||0,height:s[t]||0});return{first:v(0),last:v(e-1),widest:v(_),highest:v(y)}}getLabelForValue(t){return t}getPixelForValue(t,e){return NaN}getValueForPixel(t){}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getPixelForDecimal(t){const e=this;e._reversePixels&&(t=1-t);const i=e._startPixel+t*e._length;return Nt(e._alignToPixels?Ut(e.chart,i,0):i)}getDecimalForPixel(t){const e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:t,max:e}=this;return t<0&&e<0?e:t>0&&e>0?t:0}getContext(t){const e=this,i=e.ticks||[];if(t>=0&&tr*o?r/n:l/o:l*o0}_computeGridLineItems(t){const e=this,i=e.axis,n=e.chart,o=e.options,{grid:s,position:a}=o,r=s.offset,l=e.isHorizontal(),c=e.ticks.length+(r?1:0),h=Wi(s),d=[],u=s.setContext(e.getContext(0)),f=u.drawBorder?u.borderWidth:0,g=f/2,p=function(t){return Ut(n,t,f)};let m,x,b,_,y,v,M,w,k,S,P,D;if("top"===a)m=p(e.bottom),v=e.bottom-h,w=m-g,S=p(t.top)+g,D=t.bottom;else if("bottom"===a)m=p(e.top),S=t.top,D=p(t.bottom)-g,v=m+g,w=e.top+h;else if("left"===a)m=p(e.right),y=e.right-h,M=m-g,k=p(t.left)+g,P=t.right;else if("right"===a)m=p(e.left),k=t.left,P=p(t.right)-g,y=m+g,M=e.left+h;else if("x"===i){if("center"===a)m=p((t.top+t.bottom)/2+.5);else if(U(a)){const t=Object.keys(a)[0],i=a[t];m=p(e.chart.scales[t].getPixelForValue(i))}S=t.top,D=t.bottom,v=m+g,w=v+h}else if("y"===i){if("center"===a)m=p((t.left+t.right)/2);else if(U(a)){const t=Object.keys(a)[0],i=a[t];m=p(e.chart.scales[t].getPixelForValue(i))}y=m-g,M=y-h,k=t.left,P=t.right}for(x=0;xe.value===t));if(n>=0){return i.setContext(e.getContext(n)).lineWidth}return 0}drawGrid(t){const e=this,i=e.options.grid,n=e.ctx,o=e.chart,s=i.setContext(e.getContext(0)),a=i.drawBorder?s.borderWidth:0,r=e._gridLineItems||(e._gridLineItems=e._computeGridLineItems(t));let l,c;const h=(t,e,i)=>{i.width&&i.color&&(n.save(),n.lineWidth=i.width,n.strokeStyle=i.color,n.setLineDash(i.borderDash||[]),n.lineDashOffset=i.borderDashOffset,n.beginPath(),n.moveTo(t.x,t.y),n.lineTo(e.x,e.y),n.stroke(),n.restore())};if(i.display)for(l=0,c=r.length;l$i([o,...t],e,i,n)};return new Proxy(o,{deleteProperty:(e,i)=>(delete e[i],delete e._keys,delete t[0][i],!0),get:(i,n)=>Ki(i,n,(()=>function(t,e,i,n){let o;for(const s of e)if(o=en(Xi(s,t),i),ht(o))return qi(t,o)?Ji(i,n,t,o):o}(n,e,t,i))),getOwnPropertyDescriptor:(t,e)=>Reflect.getOwnPropertyDescriptor(t._scopes[0],e),getPrototypeOf:()=>Reflect.getPrototypeOf(t[0]),has:(t,e)=>nn(t).includes(e),ownKeys:t=>nn(t),set:(e,i,n)=>(t[0][i]=n,delete e[i],delete e._keys,!0)})}function Yi(t,e,i,n){const o={_cacheable:!1,_proxy:t,_context:e,_subProxy:i,_stack:new Set,_descriptors:Ui(t,n),setContext:e=>Yi(t,e,i,n),override:o=>Yi(t.override(o),e,i,n)};return new Proxy(o,{deleteProperty:(e,i)=>(delete e[i],delete t[i],!0),get:(t,e,i)=>Ki(t,e,(()=>function(t,e,i){const{_proxy:n,_context:o,_subProxy:s,_descriptors:a}=t;let r=n[e];dt(r)&&a.isScriptable(e)&&(r=function(t,e,i,n){const{_proxy:o,_context:s,_subProxy:a,_stack:r}=i;if(r.has(t))throw new Error("Recursion detected: "+[...r].join("->")+"->"+t);r.add(t),e=e(s,a||n),r.delete(t),U(e)&&(e=Ji(o._scopes,o,t,e));return e}(e,r,t,i));Y(r)&&r.length&&(r=function(t,e,i,n){const{_proxy:o,_context:s,_subProxy:a,_descriptors:r}=i;if(ht(s.index)&&n(t))e=e[s.index%e.length];else if(U(e[0])){const i=e,n=o._scopes.filter((t=>t!==i));e=[];for(const l of i){const i=Ji(n,o,t,l);e.push(Yi(i,s,a&&a[t],r))}}return e}(e,r,t,a.isIndexable));qi(e,r)&&(r=Yi(r,o,s&&s[e],a));return r}(t,e,i))),getOwnPropertyDescriptor:(e,i)=>e._descriptors.allKeys?Reflect.has(t,i)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(t,i),getPrototypeOf:()=>Reflect.getPrototypeOf(t),has:(e,i)=>Reflect.has(t,i),ownKeys:()=>Reflect.ownKeys(t),set:(e,i,n)=>(t[i]=n,delete e[i],!0)})}function Ui(t,e={scriptable:!0,indexable:!0}){const{_scriptable:i=e.scriptable,_indexable:n=e.indexable,_allKeys:o=e.allKeys}=t;return{allKeys:o,scriptable:i,indexable:n,isScriptable:dt(i)?i:()=>i,isIndexable:dt(n)?n:()=>n}}const Xi=(t,e)=>t?t+ct(e):e,qi=(t,e)=>U(e)&&"adapters"!==t;function Ki(t,e,i){let n=t[e];return ht(n)||(n=i(),ht(n)&&(t[e]=n)),n}function Gi(t,e,i){return dt(t)?t(e,i):t}const Zi=(t,e)=>!0===t?e:"string"==typeof t?lt(e,t):void 0;function Qi(t,e,i,n){for(const o of e){const e=Zi(i,o);if(e){t.add(e);const o=Gi(e._fallback,i,e);if(ht(o)&&o!==i&&o!==n)return o}else if(!1===e&&ht(n)&&i!==n)return null}return!1}function Ji(t,e,i,n){const o=e._rootScopes,s=Gi(e._fallback,i,n),a=[...t,...o],r=new Set,l=t[0];U(l)&&!(i in l)&&r.add(l[i]={}),r.add(n);let c=tn(r,a,i,s||i);return null!==c&&((!ht(s)||s===i||(c=tn(r,a,s,c),null!==c))&&$i([...r],[""],o,s))}function tn(t,e,i,n){for(;i;)i=Qi(t,e,i,n);return i}function en(t,e){for(const i of e){if(!i)continue;const e=i[t];if(ht(e))return e}}function nn(t){let e=t._keys;return e||(e=t._keys=function(t){const e=new Set;for(const i of t)for(const t of Object.keys(i).filter((t=>!t.startsWith("_"))))e.add(t);return[...e]}(t._scopes)),e}const on=Number.EPSILON||1e-14,sn=(t,e)=>e!t.skip))),"monotone"===e.cubicInterpolationMode)rn(t);else{let i=n?t[t.length-1]:t[0];for(o=0,s=t.length;o0?e.y:t.y}}function un(t,e,i,n){const o={x:t.cp2x,y:t.cp2y},s={x:e.cp1x,y:e.cp1y},a=hn(t,o,i),r=hn(o,s,i),l=hn(s,e,i),c=hn(a,r,i),h=hn(r,l,i);return hn(c,h,i)}function fn(t,e,i){return t?function(t,e){return{x:i=>t+t+e-i,setWidth(t){e=t},textAlign:t=>"center"===t?t:"right"===t?"left":"right",xPlus:(t,e)=>t-e,leftForLtr:(t,e)=>t-e}}(e,i):{x:t=>t,setWidth(t){},textAlign:t=>t,xPlus:(t,e)=>t+e,leftForLtr:(t,e)=>t}}function gn(t,e){let i,n;"ltr"!==e&&"rtl"!==e||(i=t.canvas.style,n=[i.getPropertyValue("direction"),i.getPropertyPriority("direction")],i.setProperty("direction",e,"important"),t.prevTextDirection=n)}function pn(t,e){void 0!==e&&(delete t.prevTextDirection,t.canvas.style.setProperty("direction",e[0],e[1]))}function mn(t){return"angle"===t?{between:Wt,compare:Vt,normalize:Bt}:{between:(t,e,i)=>t>=e&&t<=i,compare:(t,e)=>t-e,normalize:t=>t}}function xn(t,e,i,n){return{start:t%n,end:e%n,loop:i&&(e-t+1)%n==0}}function bn(t,e,i){if(!i)return[t];const{property:n,start:o,end:s}=i,a=e.length,{compare:r,between:l,normalize:c}=mn(n),{start:h,end:d,loop:u}=function(t,e,i){const{property:n,start:o,end:s}=i,{between:a,normalize:r}=mn(n),l=e.length;let c,h,{start:d,end:u,loop:f}=t;if(f){for(d+=l,u+=l,c=0,h=l;cx||l(o,m,g)&&0!==r(o,m),y=()=>!x||0===r(s,g)||l(s,m,g);for(let t=h,i=h;t<=d;++t)p=e[t%a],p.skip||(g=c(p[n]),x=l(g,o,s),null===b&&_()&&(b=0===r(g,o)?t:i),null!==b&&y()&&(f.push(xn(b,t,u,a)),b=null),i=t,m=g);return null!==b&&f.push(xn(b,d,u,a)),f}function _n(t,e){const i=[],n=t.segments;for(let o=0;oo&&t[s%e].skip;)s--;return s%=e,{start:o,end:s}}(e,n,o,i);if(!0===i)return[{start:s,end:a,loop:o}];return function(t,e,i,n){const o=t.length,s=[];let a,r=e,l=t[e];for(a=e+1;a<=i;++a){const i=t[a%o];i.skip||i.stop?l.skip||(n=!1,s.push({start:e%o,end:(a-1)%o,loop:n}),e=r=i.stop?a:null):(r=a,l.skip&&(e=a)),l=i}return null!==r&&s.push({start:e%o,end:r%o,loop:n}),s}(e,s,a{const n=i.split("."),o=n.pop(),s=[t].concat(n).join("."),a=e[i].split("."),r=a.pop(),l=a.join(".");mt.route(s,o,l,r)}))}(e,t.defaultRoutes);t.descriptors&&mt.describe(e,t.descriptors)}(t,a,n),e.override&&mt.override(t.id,t.overrides)),a}get(t){return this.items[t]}unregister(t){const e=this.items,i=t.id,n=this.scope;i in e&&delete e[i],n&&i in mt[n]&&(delete mt[n][i],this.override&&delete ut[i])}}var wn=new class{constructor(){this.controllers=new Mn(Ai,"datasets",!0),this.elements=new Mn(Oi,"elements"),this.plugins=new Mn(Object,"plugins"),this.scales=new Mn(ji,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...t){this._each("register",t)}remove(...t){this._each("unregister",t)}addControllers(...t){this._each("register",t,this.controllers)}addElements(...t){this._each("register",t,this.elements)}addPlugins(...t){this._each("register",t,this.plugins)}addScales(...t){this._each("register",t,this.scales)}getController(t){return this._get(t,this.controllers,"controller")}getElement(t){return this._get(t,this.elements,"element")}getPlugin(t){return this._get(t,this.plugins,"plugin")}getScale(t){return this._get(t,this.scales,"scale")}removeControllers(...t){this._each("unregister",t,this.controllers)}removeElements(...t){this._each("unregister",t,this.elements)}removePlugins(...t){this._each("unregister",t,this.plugins)}removeScales(...t){this._each("unregister",t,this.scales)}_each(t,e,i){const n=this;[...e].forEach((e=>{const o=i||n._getRegistryForType(e);i||o.isForType(e)||o===n.plugins&&e.id?n._exec(t,o,e):J(e,(e=>{const o=i||n._getRegistryForType(e);n._exec(t,o,e)}))}))}_exec(t,e,i){const n=ct(t);Q(i["before"+n],[],i),e[t](i),Q(i["after"+n],[],i)}_getRegistryForType(t){for(let e=0;et.filter((t=>!e.some((e=>t.plugin.id===e.plugin.id))));this._notify(n(e,i),t,"stop"),this._notify(n(i,e),t,"start")}}function Sn(t,e){return e||!1!==t?!0===t?{}:t:null}function Pn(t,e,i,n){const o=t.pluginScopeKeys(e),s=t.getOptionScopes(i,o);return t.createResolver(s,n,[""],{scriptable:!1,indexable:!1,allKeys:!0})}function Dn(t,e){const i=mt.datasets[t]||{};return((e.datasets||{})[t]||{}).indexAxis||e.indexAxis||i.indexAxis||"x"}function Cn(t,e){return"x"===t||"y"===t?t:e.axis||("top"===(i=e.position)||"bottom"===i?"x":"left"===i||"right"===i?"y":void 0)||t.charAt(0).toLowerCase();var i}function An(t){const e=t.options||(t.options={});e.plugins=K(e.plugins,{}),e.scales=function(t,e){const i=ut[t.type]||{scales:{}},n=e.scales||{},o=Dn(t.type,e),s=Object.create(null),a=Object.create(null);return Object.keys(n).forEach((t=>{const e=n[t],r=Cn(t,e),l=function(t,e){return t===e?"_index_":"_value_"}(r,o),c=i.scales||{};s[r]=s[r]||t,a[t]=st(Object.create(null),[{axis:r},e,c[r],c[l]])})),t.data.datasets.forEach((i=>{const o=i.type||t.type,r=i.indexAxis||Dn(o,e),l=(ut[o]||{}).scales||{};Object.keys(l).forEach((t=>{const e=function(t,e){let i=t;return"_index_"===t?i=e:"_value_"===t&&(i="x"===e?"y":"x"),i}(t,r),o=i[e+"AxisID"]||s[e]||e;a[o]=a[o]||Object.create(null),st(a[o],[{axis:e},n[o],l[t]])}))})),Object.keys(a).forEach((t=>{const e=a[t];st(e,[mt.scales[e.type],mt.scale])})),a}(t,e)}const On=new Map,Tn=new Set;function Ln(t,e){let i=On.get(t);return i||(i=e(),On.set(t,i),Tn.add(i)),i}const Rn=(t,e,i)=>{const n=lt(e,i);void 0!==n&&t.add(n)};class En{constructor(t){this._config=function(t){const e=(t=t||{}).data=t.data||{datasets:[],labels:[]};return e.datasets=e.datasets||[],e.labels=e.labels||[],An(t),t}(t),this._scopeCache=new Map,this._resolverCache=new Map}get type(){return this._config.type}set type(t){this._config.type=t}get data(){return this._config.data}set data(t){this._config.data=t}get options(){return this._config.options}set options(t){this._config.options=t}get plugins(){return this._config.plugins}update(){const t=this._config;this.clearCache(),An(t)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(t){return Ln(t,(()=>[["datasets."+t,""]]))}datasetAnimationScopeKeys(t,e){return Ln(`${t}.transition.${e}`,(()=>[[`datasets.${t}.transitions.${e}`,"transitions."+e],["datasets."+t,""]]))}datasetElementScopeKeys(t,e){return Ln(`${t}-${e}`,(()=>[[`datasets.${t}.elements.${e}`,"datasets."+t,"elements."+e,""]]))}pluginScopeKeys(t){const e=t.id;return Ln(`${this.type}-plugin-${e}`,(()=>[["plugins."+e,...t.additionalOptionScopes||[]]]))}_cachedScopes(t,e){const i=this._scopeCache;let n=i.get(t);return n&&!e||(n=new Map,i.set(t,n)),n}getOptionScopes(t,e,i){const{options:n,type:o}=this,s=this._cachedScopes(t,i),a=s.get(e);if(a)return a;const r=new Set;e.forEach((e=>{t&&(r.add(t),e.forEach((e=>Rn(r,t,e)))),e.forEach((t=>Rn(r,n,t))),e.forEach((t=>Rn(r,ut[o]||{},t))),e.forEach((t=>Rn(r,mt,t))),e.forEach((t=>Rn(r,ft,t)))}));const l=[...r];return Tn.has(e)&&s.set(e,l),l}chartOptionScopes(){const{options:t,type:e}=this;return[t,ut[e]||{},mt.datasets[e]||{},{type:e},mt,ft]}resolveNamedOptions(t,e,i,n=[""]){const o={$shared:!0},{resolver:s,subPrefixes:a}=In(this._resolverCache,t,n);let r=s;if(function(t,e){const{isScriptable:i,isIndexable:n}=Ui(t);for(const o of e)if(i(o)&&dt(t[o])||n(o)&&Y(t[o]))return!0;return!1}(s,e)){o.$shared=!1;r=Yi(s,i=dt(i)?i():i,this.createResolver(t,i,a))}for(const t of e)o[t]=r[t];return o}createResolver(t,e,i=[""],n){const{resolver:o}=In(this._resolverCache,t,i);return U(e)?Yi(o,e,void 0,n):o}}function In(t,e,i){let n=t.get(e);n||(n=new Map,t.set(e,n));const o=i.join();let s=n.get(o);if(!s){s={resolver:$i(e,i),subPrefixes:i.filter((t=>!t.toLowerCase().includes("hover")))},n.set(o,s)}return s}const Fn=["top","bottom","left","right","chartArea"];function zn(t,e){return"top"===t||"bottom"===t||-1===Fn.indexOf(t)&&"x"===e}function Vn(t,e){return function(i,n){return i[t]===n[t]?i[e]-n[e]:i[t]-n[t]}}function Bn(t){const e=t.chart,i=e.options.animation;e.notifyPlugins("afterRender"),Q(i&&i.onComplete,[t],e)}function Wn(t){const e=t.chart,i=e.options.animation;Q(i&&i.onProgress,[t],e)}function Hn(){return"undefined"!=typeof window&&"undefined"!=typeof document}function Nn(t){return Hn()&&"string"==typeof t?t=document.getElementById(t):t&&t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas),t}const jn={},$n=t=>{const e=Nn(t);return Object.values(jn).filter((t=>t.canvas===e)).pop()};class Yn{constructor(t,e){const n=this;this.config=e=new En(e);const o=Nn(t),s=$n(o);if(s)throw new Error("Canvas is already in use. Chart with ID '"+s.id+"' must be destroyed before the canvas can be reused.");const r=e.createResolver(e.chartOptionScopes(),n.getContext());this.platform=n._initializePlatform(o,e);const l=n.platform.acquireContext(o,r.aspectRatio),c=l&&l.canvas,h=c&&c.height,d=c&&c.width;this.id=j(),this.ctx=l,this.canvas=c,this.width=d,this.height=h,this._options=r,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._sortedMetasets=[],this.scales={},this.scale=void 0,this._plugins=new kn,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=i((()=>this.update("resize")),r.resizeDelay||0),jn[n.id]=n,l&&c?(a.listen(n,"complete",Bn),a.listen(n,"progress",Wn),n._initialize(),n.attached&&n.update()):console.error("Failed to create chart: can't acquire context from the given item")}get aspectRatio(){const{options:{aspectRatio:t,maintainAspectRatio:e},width:i,height:n,_aspectRatio:o}=this;return $(t)?e&&o?o:n?i/n:null:t}get data(){return this.config.data}set data(t){this.config.data=t}get options(){return this._options}set options(t){this.config.options=t}_initialize(){const t=this;return t.notifyPlugins("beforeInit"),t.options.responsive?t.resize():be(t,t.options.devicePixelRatio),t.bindEvents(),t.notifyPlugins("afterInit"),t}_initializePlatform(t,e){return e.platform?new e.platform:!Hn()||"undefined"!=typeof OffscreenCanvas&&t instanceof OffscreenCanvas?new Ke:new li}clear(){return Xt(this.canvas,this.ctx),this}stop(){return a.stop(this),this}resize(t,e){a.running(this)?this._resizeBeforeDraw={width:t,height:e}:this._resize(t,e)}_resize(t,e){const i=this,n=i.options,o=i.canvas,s=n.maintainAspectRatio&&i.aspectRatio,a=i.platform.getMaximumSize(o,t,e,s),r=i.currentDevicePixelRatio,l=n.devicePixelRatio||i.platform.getDevicePixelRatio();i.width===a.width&&i.height===a.height&&r===l||(i.width=a.width,i.height=a.height,i._aspectRatio=i.aspectRatio,be(i,l,!0),i.notifyPlugins("resize",{size:a}),Q(n.onResize,[i,a],i),i.attached&&i._doResize()&&i.render())}ensureScalesHaveIDs(){J(this.options.scales||{},((t,e)=>{t.id=e}))}buildOrUpdateScales(){const t=this,e=t.options,i=e.scales,n=t.scales,o=Object.keys(n).reduce(((t,e)=>(t[e]=!1,t)),{});let s=[];i&&(s=s.concat(Object.keys(i).map((t=>{const e=i[t],n=Cn(t,e),o="r"===n,s="x"===n;return{options:e,dposition:o?"chartArea":s?"bottom":"left",dtype:o?"radialLinear":s?"category":"linear"}})))),J(s,(i=>{const s=i.options,a=s.id,r=Cn(a,s),l=K(s.type,i.dtype);void 0!==s.position&&zn(s.position,r)===zn(i.dposition)||(s.position=i.dposition),o[a]=!0;let c=null;if(a in n&&n[a].type===l)c=n[a];else{c=new(wn.getScale(l))({id:a,type:l,ctx:t.ctx,chart:t}),n[c.id]=c}c.init(s,e)})),J(o,((t,e)=>{t||delete n[e]})),J(n,(e=>{Xe.configure(t,e,e.options),Xe.addBox(t,e)}))}_updateMetasetIndex(t,e){const i=this._metasets,n=t.index;n!==e&&(i[n]=i[e],i[e]=t,t.index=e)}_updateMetasets(){const t=this,e=t._metasets,i=t.data.datasets.length,n=e.length;if(n>i){for(let e=i;ei.length&&delete t._stacks,e.forEach(((e,n)=>{0===i.filter((t=>t===e._dataset)).length&&t._destroyDatasetMeta(n)}))}buildOrUpdateControllers(){const t=this,e=[],i=t.data.datasets;let n,o;for(t._removeUnreferencedMetasets(),n=0,o=i.length;n{t.getDatasetMeta(i).controller.reset()}),t)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(t){const e=this,i=e.config;i.update(),e._options=i.createResolver(i.chartOptionScopes(),e.getContext()),J(e.scales,(t=>{Xe.removeBox(e,t)}));const n=e._animationsDisabled=!e.options.animation;if(e.ensureScalesHaveIDs(),e.buildOrUpdateScales(),e._plugins.invalidate(),!1===e.notifyPlugins("beforeUpdate",{mode:t,cancelable:!0}))return;const o=e.buildOrUpdateControllers();e.notifyPlugins("beforeElementsUpdate");let s=0;for(let t=0,i=e.data.datasets.length;t{t.reset()})),e._updateDatasets(t),e.notifyPlugins("afterUpdate",{mode:t}),e._layers.sort(Vn("z","_idx")),e._lastEvent&&e._eventHandler(e._lastEvent,!0),e.render()}_updateLayout(t){const e=this;if(!1===e.notifyPlugins("beforeLayout",{cancelable:!0}))return;Xe.update(e,e.width,e.height,t);const i=e.chartArea,n=i.width<=0||i.height<=0;e._layers=[],J(e.boxes,(t=>{n&&"chartArea"===t.position||(t.configure&&t.configure(),e._layers.push(...t._layers()))}),e),e._layers.forEach(((t,e)=>{t._idx=e})),e.notifyPlugins("afterLayout")}_updateDatasets(t){const e=this,i="function"==typeof t;if(!1!==e.notifyPlugins("beforeDatasetsUpdate",{mode:t,cancelable:!0})){for(let n=0,o=e.data.datasets.length;n=0;--i)t._drawDataset(e[i]);t.notifyPlugins("afterDatasetsDraw")}_drawDataset(t){const e=this,i=e.ctx,n=t._clip,o=e.chartArea,s={meta:t,index:t.index,cancelable:!0};!1!==e.notifyPlugins("beforeDatasetDraw",s)&&(Gt(i,{left:!1===n.left?0:o.left-n.left,right:!1===n.right?e.width:o.right+n.right,top:!1===n.top?0:o.top-n.top,bottom:!1===n.bottom?e.height:o.bottom+n.bottom}),t.controller.draw(),Zt(i),s.cancelable=!1,e.notifyPlugins("afterDatasetDraw",s))}getElementsAtEventForMode(t,e,i,n){const o=De.modes[e];return"function"==typeof o?o(this,t,i,n):[]}getDatasetMeta(t){const e=this.data.datasets[t],i=this._metasets;let n=i.filter((t=>t&&t._dataset===e)).pop();return n||(n=i[t]={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e&&e.order||0,index:t,_dataset:e,_parsed:[],_sorted:!1}),n}getContext(){return this.$context||(this.$context={chart:this,type:"chart"})}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(t){const e=this.data.datasets[t];if(!e)return!1;const i=this.getDatasetMeta(t);return"boolean"==typeof i.hidden?!i.hidden:!e.hidden}setDatasetVisibility(t,e){this.getDatasetMeta(t).hidden=!e}toggleDataVisibility(t){this._hiddenIndices[t]=!this._hiddenIndices[t]}getDataVisibility(t){return!this._hiddenIndices[t]}_updateDatasetVisibility(t,e){const i=this,n=e?"show":"hide",o=i.getDatasetMeta(t),s=o.controller._resolveAnimations(void 0,n);i.setDatasetVisibility(t,e),s.update(o,{visible:e}),i.update((e=>e.datasetIndex===t?n:void 0))}hide(t){this._updateDatasetVisibility(t,!1)}show(t){this._updateDatasetVisibility(t,!0)}_destroyDatasetMeta(t){const e=this,i=e._metasets&&e._metasets[t];i&&i.controller&&(i.controller._destroy(),delete e._metasets[t])}destroy(){const t=this,{canvas:e,ctx:i}=t;let n,o;for(t.stop(),a.remove(t),n=0,o=t.data.datasets.length;n{i.addEventListener(t,n,o),e[n]=o},o=(n,o)=>{e[n]&&(i.removeEventListener(t,n,o),delete e[n])};let s=function(e,i,n){e.offsetX=i,e.offsetY=n,t._eventHandler(e)};if(J(t.options.events,(t=>n(t,s))),t.options.responsive){let e;s=(e,i)=>{t.canvas&&t.resize(e,i)};const a=()=>{o("attach",a),t.attached=!0,t.resize(),n("resize",s),n("detach",e)};e=()=>{t.attached=!1,o("resize",s),n("attach",a)},i.isAttached(t.canvas)?a():e()}else t.attached=!0}unbindEvents(){const t=this,e=t._listeners;e&&(delete t._listeners,J(e,((e,i)=>{t.platform.removeEventListener(t,i,e)})))}updateHoverStyle(t,e,i){const n=i?"set":"remove";let o,s,a,r;for("dataset"===e&&(o=this.getDatasetMeta(t[0].datasetIndex),o.controller["_"+n+"DatasetHoverStyle"]()),a=0,r=t.length;a{const n=e.getDatasetMeta(t);if(!n)throw new Error("No dataset found at index "+t);return{datasetIndex:t,element:n.data[i],index:i}}));!tt(n,i)&&(e._active=n,e._updateHoverStyles(n,i))}notifyPlugins(t,e){return this._plugins.notify(this,t,e)}_updateHoverStyles(t,e,i){const n=this,o=n.options.hover,s=(t,e)=>t.filter((t=>!e.some((e=>t.datasetIndex===e.datasetIndex&&t.index===e.index)))),a=s(e,t),r=i?t:s(t,e);a.length&&n.updateHoverStyle(a,o.mode,!1),r.length&&o.mode&&n.updateHoverStyle(r,o.mode,!0)}_eventHandler(t,e){const i=this,n={event:t,replay:e,cancelable:!0};if(!1===i.notifyPlugins("beforeEvent",n))return;const o=i._handleEvent(t,e);return n.cancelable=!1,i.notifyPlugins("afterEvent",n),(o||n.changed)&&i.render(),i}_handleEvent(t,e){const i=this,{_active:n=[],options:o}=i,s=o.hover,a=e;let r=[],l=!1,c=null;return"mouseout"!==t.type&&(r=i.getElementsAtEventForMode(t,s.mode,s,a),c="click"===t.type?i._lastEvent:t),i._lastEvent=null,Q(o.onHover,[t,r,i],i),"mouseup"!==t.type&&"click"!==t.type&&"contextmenu"!==t.type||Kt(t,i.chartArea,i._minPadding)&&Q(o.onClick,[t,r,i],i),l=!tt(r,n),(l||e)&&(i._active=r,i._updateHoverStyles(r,n,e)),i._lastEvent=c,l}}const Un=()=>J(Yn.instances,(t=>t._plugins.invalidate())),Xn=!0;function qn(){throw new Error("This method is not implemented: either no adapter can be found or an incomplete integration was provided.")}Object.defineProperties(Yn,{defaults:{enumerable:Xn,value:mt},instances:{enumerable:Xn,value:jn},overrides:{enumerable:Xn,value:ut},registry:{enumerable:Xn,value:wn},version:{enumerable:Xn,value:"3.0.2"},getChart:{enumerable:Xn,value:$n},register:{enumerable:Xn,value:(...t)=>{wn.add(...t),Un()}},unregister:{enumerable:Xn,value:(...t)=>{wn.remove(...t),Un()}}});class Kn{constructor(t){this.options=t||{}}formats(){return qn()}parse(t,e){return qn()}format(t,e){return qn()}add(t,e,i){return qn()}diff(t,e,i){return qn()}startOf(t,e,i){return qn()}endOf(t,e){return qn()}}Kn.override=function(t){Object.assign(Kn.prototype,t)};var Gn={_date:Kn};function Zn(t){const e=function(t){if(!t._cache.$bar){const e=t.getMatchingVisibleMetas("bar");let i=[];for(let n=0,o=e.length;nt-e)))}return t._cache.$bar}(t);let i,n,o,s,a=t._length;const r=()=>{a=Math.min(a,i&&Math.abs(o-s)||a),s=o};for(i=0,n=e.length;iMath.abs(r)&&(l=r,c=a),e[i.axis]=c,e._custom={barStart:l,barEnd:c,start:o,end:s,min:a,max:r}}(t,e,i,n):e[i.axis]=i.parse(t,n),e}function Jn(t,e,i,n){const o=t.iScale,s=t.vScale,a=o.getLabels(),r=o===s,l=[];let c,h,d,u;for(c=i,h=i+n;c0?(p+=t,h-=t):h<0&&(p-=t,h+=t)}return{size:h,base:p,head:c,center:c+h/2}}_calculateBarIndexPixels(t,e){const i=this,n=e.scale,o=i.options,s=K(o.maxBarThickness,1/0);let a,r;if(e.grouped){const n=o.skipNull?i._getStackCount(t):e.stackCount,l="flex"===o.barThickness?function(t,e,i,n){const o=e.pixels,s=o[t];let a=t>0?o[t-1]:null,r=t=0;--n)i=Math.max(i,t[n].size()/2,e[n]._custom);return i>0&&i}getLabelAndValue(t){const e=this._cachedMeta,{xScale:i,yScale:n}=e,o=this.getParsed(t),s=i.getLabelForValue(o.x),a=n.getLabelForValue(o.y),r=o._custom;return{label:e.label,value:"("+s+", "+a+(r?", "+r:"")+")"}}update(t){const e=this._cachedMeta.data;this.updateElements(e,0,e.length,t)}updateElements(t,e,i,n){const o=this,s="reset"===n,{xScale:a,yScale:r}=o._cachedMeta,l=o.resolveDataElementOptions(e,n),c=o.getSharedOptions(l),h=o.includeOptions(n,c);for(let l=e;l""}}}};class no extends Ai{constructor(t,e){super(t,e),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(t,e){const i=this.getDataset().data,n=this._cachedMeta;let o,s;for(o=t,s=t+e;oWt(t,r,l)?1:Math.max(e,e*i,n,n*i),g=(t,e,n)=>Wt(t,r,l)?-1:Math.min(e,e*i,n,n*i),p=f(0,c,d),m=f(Mt,h,u),x=g(xt,c,d),b=g(xt+Mt,h,u);n=(p-x)/2,o=(m-b)/2,s=-(p+x)/2,a=-(m+b)/2}return{ratioX:n,ratioY:o,offsetX:s,offsetY:a}}(d,h,l),m=(n.width-a)/u,x=(n.height-a)/f,b=Math.max(Math.min(m,x)/2,0),_=Z(e.options.radius,b),y=(_-Math.max(_*l,0))/e._getVisibleDatasetWeightTotal();e.offsetX=g*_,e.offsetY=p*_,o.total=e.calculateTotal(),e.outerRadius=_-y*e._getRingWeightOffset(e.index),e.innerRadius=Math.max(e.outerRadius-y*c,0),e.updateElements(s,0,s.length,t)}_circumference(t,e){const i=this,n=i.options,o=i._cachedMeta,s=i._getCircumference();return e&&n.animation.animateRotate||!this.chart.getDataVisibility(t)||null===o._parsed[t]?0:i.calculateCircumference(o._parsed[t]*s/bt)}updateElements(t,e,i,n){const o=this,s="reset"===n,a=o.chart,r=a.chartArea,l=a.options.animation,c=(r.left+r.right)/2,h=(r.top+r.bottom)/2,d=s&&l.animateScale,u=d?0:o.innerRadius,f=d?0:o.outerRadius,g=o.resolveDataElementOptions(e,n),p=o.getSharedOptions(g),m=o.includeOptions(n,p);let x,b=o._getRotation();for(x=0;x0&&!isNaN(t)?bt*(Math.abs(t)/e):0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,n=i.data.labels||[],o=Li(e._parsed[t],i.options.locale);return{label:n[t]||"",value:o}}getMaxBorderWidth(t){const e=this;let i=0;const n=e.chart;let o,s,a,r,l;if(!t)for(o=0,s=n.data.datasets.length;o{const n=t.getDatasetMeta(0).controller.getStyle(i);return{text:e,fillStyle:n.backgroundColor,strokeStyle:n.borderColor,lineWidth:n.borderWidth,hidden:!t.getDataVisibility(i),index:i}})):[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}},tooltip:{callbacks:{title:()=>"",label(t){let e=t.label;const i=": "+t.formattedValue;return Y(e)?(e=e.slice(),e[0]+=i):e+=i,e}}}}};class oo extends Ai{initialize(){this.enableOptionSharing=!0,super.initialize()}update(t){const e=this,i=e._cachedMeta,{dataset:n,data:o=[],_dataset:s}=i,a=e.chart._animationsDisabled;let{start:r,count:l}=function(t,e,i){const n=e.length;let o=0,s=n;if(t._sorted){const{iScale:a,_parsed:r}=t,l=a.axis,{min:c,max:h,minDefined:d,maxDefined:u}=a.getUserBounds();d&&(o=Ht(Math.min(ie(r,a.axis,c).lo,i?n:ie(e,l,a.getPixelForValue(c)).lo),0,n-1)),s=u?Ht(Math.max(ie(r,a.axis,h).hi+1,i?0:ie(e,l,a.getPixelForValue(h)).hi+1),o,n)-o:n-o}return{start:o,count:s}}(i,o,a);if(e._drawStart=r,e._drawCount=l,function(t){const{xScale:e,yScale:i,_scaleRanges:n}=t,o={xmin:e.min,xmax:e.max,ymin:i.min,ymax:i.max};if(!n)return t._scaleRanges=o,!0;const s=n.xmin!==e.min||n.xmax!==e.max||n.ymin!==i.min||n.ymax!==i.max;return Object.assign(n,o),s}(i)&&(r=0,l=o.length),n._decimated=!!s._decimated,n.points=o,"resize"!==t){const i=e.resolveDatasetElementOptions(t);e.options.showLine||(i.borderWidth=0),e.updateElement(n,void 0,{animated:!a,options:i},t)}e.updateElements(o,r,l,t)}updateElements(t,e,i,n){const o=this,s="reset"===n,{xScale:a,yScale:r,_stacked:l}=o._cachedMeta,c=o.resolveDataElementOptions(e,n),h=o.getSharedOptions(c),d=o.includeOptions(n,h),u=o.options.spanGaps,f=At(u)?u:Number.POSITIVE_INFINITY,g=o.chart._animationsDisabled||s||"none"===n;let p=e>0&&o.getParsed(e-1);for(let c=e;c0&&i.x-p.x>f,d&&(u.options=h||o.resolveDataElementOptions(c,n)),g||o.updateElement(e,c,u,n),p=i}o.updateSharedOptions(h,n,c)}getMaxOverflow(){const t=this,e=t._cachedMeta,i=e.dataset,n=i.options&&i.options.borderWidth||0,o=e.data||[];if(!o.length)return n;const s=o[0].size(t.resolveDataElementOptions(0)),a=o[o.length-1].size(t.resolveDataElementOptions(o.length-1));return Math.max(n,s,a)/2}draw(){this._cachedMeta.dataset.updateControlPoints(this.chart.chartArea),super.draw()}}oo.id="line",oo.defaults={datasetElementType:"line",dataElementType:"point",showLine:!0,spanGaps:!1},oo.overrides={scales:{_index_:{type:"category"},_value_:{type:"linear"}}};class so extends Ai{constructor(t,e){super(t,e),this.innerRadius=void 0,this.outerRadius=void 0}update(t){const e=this._cachedMeta.data;this._updateRadius(),this.updateElements(e,0,e.length,t)}_updateRadius(){const t=this,e=t.chart,i=e.chartArea,n=e.options,o=Math.min(i.right-i.left,i.bottom-i.top),s=Math.max(o/2,0),a=(s-Math.max(n.cutoutPercentage?s/100*n.cutoutPercentage:1,0))/e.getVisibleDatasetCount();t.outerRadius=s-a*t.index,t.innerRadius=t.outerRadius-a}updateElements(t,e,i,n){const o=this,s="reset"===n,a=o.chart,r=o.getDataset(),l=a.options.animation,c=o._cachedMeta.rScale,h=c.xCenter,d=c.yCenter,u=c.getIndexAngle(0)-.5*xt;let f,g=u;const p=360/o.countVisibleElements();for(f=0;f{!isNaN(t.data[n])&&this.chart.getDataVisibility(n)&&i++})),i}_computeAngle(t,e,i){return this.chart.getDataVisibility(t)?Rt(this.resolveDataElementOptions(t,e).angle||i):0}}so.id="polarArea",so.defaults={dataElementType:"arc",animation:{animateRotate:!0,animateScale:!0},animations:{numbers:{type:"number",properties:["x","y","startAngle","endAngle","innerRadius","outerRadius"]}},indexAxis:"r",startAngle:0},so.overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data;return e.labels.length&&e.datasets.length?e.labels.map(((e,i)=>{const n=t.getDatasetMeta(0).controller.getStyle(i);return{text:e,fillStyle:n.backgroundColor,strokeStyle:n.borderColor,lineWidth:n.borderWidth,hidden:!t.getDataVisibility(i),index:i}})):[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}},tooltip:{callbacks:{title:()=>"",label:t=>t.chart.data.labels[t.dataIndex]+": "+t.formattedValue}}},scales:{r:{type:"radialLinear",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}};class ao extends no{}ao.id="pie",ao.defaults={cutout:0,rotation:0,circumference:360,radius:"100%"};class ro extends Ai{getLabelAndValue(t){const e=this._cachedMeta.vScale,i=this.getParsed(t);return{label:e.getLabels()[t],value:""+e.getLabelForValue(i[e.axis])}}update(t){const e=this,i=e._cachedMeta,n=i.dataset,o=i.data||[],s=i.iScale.getLabels();if(n.points=o,"resize"!==t){const i=e.resolveDatasetElementOptions(t);e.options.showLine||(i.borderWidth=0);const a={_loop:!0,_fullLoop:s.length===o.length,options:i};e.updateElement(n,void 0,a,t)}e.updateElements(o,0,o.length,t)}updateElements(t,e,i,n){const o=this,s=o.getDataset(),a=o._cachedMeta.rScale,r="reset"===n;for(let l=e;l"",label:t=>"("+t.label+", "+t.formattedValue+")"}}},scales:{x:{type:"linear"},y:{type:"linear"}}};var co=Object.freeze({__proto__:null,BarController:eo,BubbleController:io,DoughnutController:no,LineController:oo,PolarAreaController:so,PieController:ao,RadarController:ro,ScatterController:lo});function ho(t,e){const{startAngle:i,endAngle:n,pixelMargin:o,x:s,y:a,outerRadius:r,innerRadius:l}=e;let c=o/r;t.beginPath(),t.arc(s,a,r,i-c,n+c),l>o?(c=o/l,t.arc(s,a,l,n+c,i-c,!0)):t.arc(s,a,o,n+Mt,i-Mt),t.closePath(),t.clip()}function uo(t,e){const{x:i,y:n,startAngle:o,endAngle:s,pixelMargin:a}=e,r=Math.max(e.outerRadius-a,0),l=e.innerRadius+a;t.beginPath(),t.arc(i,n,r,o,s),t.arc(i,n,l,s,o,!0),t.closePath()}function fo(t,e){const{x:i,y:n,startAngle:o,endAngle:s,pixelMargin:a,options:r}=e,l=e.outerRadius,c=e.innerRadius+a,h="inner"===r.borderAlign;r.borderWidth&&(h?(t.lineWidth=2*r.borderWidth,t.lineJoin="round"):(t.lineWidth=r.borderWidth,t.lineJoin="bevel"),e.fullCircles&&function(t,e,i){const{x:n,y:o,startAngle:s,endAngle:a,pixelMargin:r}=e,l=Math.max(e.outerRadius-r,0),c=e.innerRadius+r;let h;for(i&&(e.endAngle=e.startAngle+bt,ho(t,e),e.endAngle=a,e.endAngle===e.startAngle&&(e.endAngle+=bt,e.fullCircles--)),t.beginPath(),t.arc(n,o,c,s+bt,s,!0),h=0;h=bt||Wt(o,a,r))&&(s>=l&&s<=c)}getCenterPoint(t){const{x:e,y:i,startAngle:n,endAngle:o,innerRadius:s,outerRadius:a}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius"],t),r=(n+o)/2,l=(s+a)/2;return{x:e+Math.cos(r)*l,y:i+Math.sin(r)*l}}tooltipPosition(t){return this.getCenterPoint(t)}draw(t){const e=this,i=e.options,n=i.offset||0;if(e.pixelMargin="inner"===i.borderAlign?.33:0,e.fullCircles=Math.floor(e.circumference/bt),!(0===e.circumference||e.innerRadius<0||e.outerRadius<0)){if(t.save(),n&&e.circumference(a+(c?r-t:t))%s,_=()=>{f!==g&&(t.lineTo(m,g),t.lineTo(m,f),t.lineTo(m,p))};for(l&&(d=o[b(0)],t.moveTo(d.x,d.y)),h=0;h<=r;++h){if(d=o[b(h)],d.skip)continue;const e=d.x,i=d.y,n=0|e;n===u?(ig&&(g=i),m=(x*m+e)/++x):(_(),t.lineTo(e,i),u=n,x=0,f=g=i),p=i}_()}function _o(t){const e=t.options,i=e.borderDash&&e.borderDash.length;return!(t._decimated||t._loop||e.tension||e.stepped||i)?bo:xo}go.id="arc",go.defaults={borderAlign:"center",borderColor:"#fff",borderWidth:2,offset:0,angle:void 0},go.defaultRoutes={backgroundColor:"backgroundColor"};const yo="function"==typeof Path2D?function(t,e,i,n){let o=e._path;o||(o=e._path=new Path2D,e.path(o,i,n)&&o.closePath()),t.stroke(o)}:function(t,e,i,n){t.beginPath(),e.path(t,i,n)&&t.closePath(),t.stroke()};class vo extends Oi{constructor(t){super(),this.animated=!0,this.options=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,t&&Object.assign(this,t)}updateControlPoints(t){const e=this,i=e.options;if(i.tension&&!i.stepped&&!e._pointsUpdated){const n=i.spanGaps?e._loop:e._fullLoop;cn(e._points,i,t,n),e._pointsUpdated=!0}}set points(t){const e=this;e._points=t,delete e._segments,delete e._path,e._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=yn(this))}first(){const t=this.segments,e=this.points;return t.length&&e[t[0].start]}last(){const t=this.segments,e=this.points,i=t.length;return i&&e[t[i-1].end]}interpolate(t,e){const i=this,n=i.options,o=t[e],s=i.points,a=_n(i,{property:e,start:o,end:o});if(!a.length)return;const r=[],l=function(t){return t.stepped?dn:t.tension?un:hn}(n);let c,h;for(c=0,h=a.length;c"borderDash"!==t&&"fill"!==t};class wo extends Oi{constructor(t){super(),this.options=void 0,this.skip=void 0,this.stop=void 0,t&&Object.assign(this,t)}inRange(t,e,i){const n=this.options,{x:o,y:s}=this.getProps(["x","y"],i);return Math.pow(t-o,2)+Math.pow(e-s,2)t.x):Po(e,"bottom","top",t.base=a.left&&e<=a.right)&&(s||i>=a.top&&i<=a.bottom)}function To(t,e){const{x:i,y:n,w:o,h:s,radius:a}=e;t.arc(i+a.topLeft,n+a.topLeft,a.topLeft,-Mt,xt,!0),t.lineTo(i,n+s-a.bottomLeft),t.arc(i+a.bottomLeft,n+s-a.bottomLeft,a.bottomLeft,xt,Mt,!0),t.lineTo(i+o-a.bottomRight,n+s),t.arc(i+o-a.bottomRight,n+s-a.bottomRight,a.bottomRight,Mt,0,!0),t.lineTo(i+o,n+a.topRight),t.arc(i+o-a.topRight,n+a.topRight,a.topRight,0,-Mt,!0),t.lineTo(i+a.topLeft,n)}function Lo(t,e){t.rect(e.x,e.y,e.w,e.h)}wo.id="point",wo.defaults={borderWidth:1,hitRadius:1,hoverBorderWidth:1,hoverRadius:4,pointStyle:"circle",radius:3,rotation:0},wo.defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};class Ro extends Oi{constructor(t){super(),this.options=void 0,this.horizontal=void 0,this.base=void 0,this.width=void 0,this.height=void 0,t&&Object.assign(this,t)}draw(t){const e=this.options,{inner:i,outer:n}=Ao(this),o=(s=n.radius).topLeft||s.topRight||s.bottomLeft||s.bottomRight?To:Lo;var s;t.save(),n.w===i.w&&n.h===i.h||(t.beginPath(),o(t,n),t.clip(),o(t,i),t.fillStyle=e.borderColor,t.fill("evenodd")),t.beginPath(),o(t,i),t.fillStyle=e.backgroundColor,t.fill(),t.restore()}inRange(t,e,i){return Oo(this,t,e,i)}inXRange(t,e){return Oo(this,t,null,e)}inYRange(t,e){return Oo(this,null,t,e)}getCenterPoint(t){const{x:e,y:i,base:n,horizontal:o}=this.getProps(["x","y","base","horizontal"],t);return{x:o?(e+n)/2:e,y:o?i:(i+n)/2}}getRange(t){return"x"===t?this.width/2:this.height/2}}Ro.id="bar",Ro.defaults={borderSkipped:"start",borderWidth:0,borderRadius:0,pointStyle:void 0},Ro.defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};var Eo=Object.freeze({__proto__:null,ArcElement:go,LineElement:vo,PointElement:wo,BarElement:Ro});function Io(t){t.data.datasets.forEach((t=>{if(t._decimated){const e=t._data;delete t._decimated,delete t._data,Object.defineProperty(t,"data",{value:e})}}))}var Fo={id:"decimation",defaults:{algorithm:"min-max",enabled:!1},beforeElementsUpdate:(t,e,i)=>{if(!i.enabled)return void Io(t);const n=t.width;t.data.datasets.forEach(((e,o)=>{const{_data:s,indexAxis:a}=e,r=t.getDatasetMeta(o),l=s||e.data;if("y"===Ie([a,t.options.indexAxis]))return;if("line"!==r.type)return;const c=t.scales[r.xAxisID];if("linear"!==c.type&&"time"!==c.type)return;if(t.options.parsing)return;if(l.length<=4*n)return;let h;switch($(s)&&(e._data=l,delete e.data,Object.defineProperty(e,"data",{configurable:!0,enumerable:!0,get:function(){return this._decimated},set:function(t){this._data=t}})),i.algorithm){case"lttb":h=function(t,e,i){const n=i.samples||e,o=[],s=(t.length-2)/(n-2);let a,r,l,c,h,d=0,u=0;for(o[d++]=t[u],a=0;al&&(l=c,r=t[e],h=e);o[d++]=r,u=h}return o[d++]=t[t.length-1],o}(l,n,i);break;case"min-max":h=function(t,e){let i,n,o,s,a,r,l,c,h,d,u=0,f=0;const g=[],p=t[0].x,m=t[t.length-1].x-p;for(i=0;id&&(d=s,l=i),u=(f*u+n.x)/++f;else{const e=i-1;if(!$(r)&&!$(l)){const i=Math.min(r,l),n=Math.max(r,l);i!==c&&i!==e&&g.push({...t[i],x:u}),n!==c&&n!==e&&g.push({...t[n],x:u})}i>0&&e!==c&&g.push(t[e]),g.push(n),a=x,f=0,h=d=s,r=l=c=i}}return g}(l,n);break;default:throw new Error(`Unsupported decimation algorithm '${i.algorithm}'`)}e._decimated=h}))},destroy(t){Io(t)}};function zo(t,e,i){const n=function(t){const e=t.options,i=e.fill;let n=K(i&&i.target,i);return void 0===n&&(n=!!e.backgroundColor),!1!==n&&null!==n&&(!0===n?"origin":n)}(t);if(U(n))return!isNaN(n.value)&&n;let o=parseFloat(n);return X(o)&&Math.floor(o)===o?("-"!==n[0]&&"+"!==n[0]||(o=e+o),!(o===e||o<0||o>=i)&&o):["origin","start","end","stack"].indexOf(n)>=0&&n}class Vo{constructor(t){this.x=t.x,this.y=t.y,this.radius=t.radius}pathSegment(t,e,i){const{x:n,y:o,radius:s}=this;return e=e||{start:0,end:bt},t.arc(n,o,s,e.end,e.start,!0),!i.bounds}interpolate(t){const{x:e,y:i,radius:n}=this,o=t.angle;return{x:e+Math.cos(o)*n,y:i+Math.sin(o)*n,angle:o}}}function Bo(t){return(t.scale||{}).getPointPositionForValue?function(t){const{scale:e,fill:i}=t,n=e.options,o=e.getLabels().length,s=[],a=n.reverse?e.max:e.min,r=n.reverse?e.min:e.max;let l,c,h;if(h="start"===i?a:"end"===i?r:U(i)?i.value:e.getBaseValue(),n.grid.circular)return c=e.getPointPositionForValue(0,a),new Vo({x:c.x,y:c.y,radius:e.getDistanceFromCenterForValue(h)});for(l=0;l"line"===t.type&&!t.hidden;function No(t,e,i){const n=[];for(let o=0;o=n&&o<=c){r=o===n,l=o===c;break}}return{first:r,last:l,point:n}}function $o(t,e){let i=[],n=!1;return Y(t)?(n=!0,i=t):i=function(t,e){const{x:i=null,y:n=null}=t||{},o=e.points,s=[];return e.segments.forEach((t=>{const e=o[t.start],a=o[t.end];null!==n?(s.push({x:e.x,y:n}),s.push({x:a.x,y:n})):null!==i&&(s.push({x:i,y:e.y}),s.push({x:i,y:a.y}))})),s}(t,e),i.length?new vo({points:i,options:{tension:0},_loop:n,_fullLoop:n}):null}function Yo(t,e,i){let n=t[e].fill;const o=[e];let s;if(!i)return n;for(;!1!==n&&-1===o.indexOf(n);){if(!X(n))return n;if(s=t[n],!s)return!1;if(s.visible)return n;o.push(n),n=s.fill}return!1}function Uo(t,e,i){t.beginPath(),e.path(t),t.lineTo(e.last().x,i),t.lineTo(e.first().x,i),t.closePath(),t.clip()}function Xo(t,e,i,n){if(n)return;let o=e[t],s=i[t];return"angle"===t&&(o=Bt(o),s=Bt(s)),{property:t,start:o,end:s}}function qo(t,e,i,n){return t&&e?n(t[i],e[i]):t?t[i]:e?e[i]:0}function Ko(t,e,i){const{top:n,bottom:o}=e.chart.chartArea,{property:s,start:a,end:r}=i||{};"x"===s&&(t.beginPath(),t.rect(a,n,r-a,o-n),t.clip())}function Go(t,e,i,n){const o=e.interpolate(i,n);o&&t.lineTo(o.x,o.y)}function Zo(t,e){const{line:i,target:n,property:o,color:s,scale:a}=e,r=function(t,e,i){const n=t.segments,o=t.points,s=e.points,a=[];for(let t=0;t=0;--n)o=e[n].$filler,o&&o.line.updateControlPoints(i)},beforeDatasetDraw(t,e){const i=t.chartArea,n=t.ctx,o=e.meta.$filler;if(!o||!1===o.fill)return;const s=function(t){const{chart:e,fill:i,line:n}=t;if(X(i))return function(t,e){const i=t.getDatasetMeta(e);return i&&t.isDatasetVisible(e)?i.dataset:null}(e,i);if("stack"===i)return Wo(t);const o=Bo(t);return o instanceof Vo?o:$o(o,n)}(o),{line:a,scale:r}=o,l=a.options,c=l.fill,h=l.backgroundColor,{above:d=h,below:u=h}=c||{};s&&a.points.length&&(Gt(n,i),function(t,e){const{line:i,target:n,above:o,below:s,area:a,scale:r}=e,l=i._loop?"angle":"x";t.save(),"x"===l&&s!==o&&(Uo(t,n,a.top),Zo(t,{line:i,target:n,color:o,scale:r,property:l}),t.restore(),t.save(),Uo(t,n,a.bottom)),Zo(t,{line:i,target:n,color:s,scale:r,property:l}),t.restore()}(n,{line:a,target:s,above:d,below:u,area:i,scale:r}),Zt(n))},defaults:{propagate:!0}};const Jo=(t,e)=>{let{boxHeight:i=e,boxWidth:n=e}=t;return t.usePointStyle&&(i=Math.min(i,e),n=Math.min(n,e)),{boxWidth:n,boxHeight:i,itemHeight:Math.max(e,i)}};class ts extends Oi{constructor(t){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e,i){const n=this;n.maxWidth=t,n.maxHeight=e,n._margins=i,n.setDimensions(),n.buildLabels(),n.fit()}setDimensions(){const t=this;t.isHorizontal()?(t.width=t.maxWidth,t.left=0,t.right=t.width):(t.height=t.maxHeight,t.top=0,t.bottom=t.height)}buildLabels(){const t=this,e=t.options.labels||{};let i=Q(e.generateLabels,[t.chart],t)||[];e.filter&&(i=i.filter((i=>e.filter(i,t.chart.data)))),e.sort&&(i=i.sort(((i,n)=>e.sort(i,n,t.chart.data)))),t.options.reverse&&i.reverse(),t.legendItems=i}fit(){const t=this,{options:e,ctx:i}=t;if(!e.display)return void(t.width=t.height=0);const n=e.labels,o=Ee(n.font),s=o.size,a=t._computeTitleHeight(),{boxWidth:r,itemHeight:l}=Jo(n,s);let c,h;i.font=o.string,t.isHorizontal()?(c=t.maxWidth,h=t._fitRows(a,s,r,l)+10):(h=t.maxHeight,c=t._fitCols(a,s,r,l)+10),t.width=Math.min(c,e.maxWidth||t.maxWidth),t.height=Math.min(h,e.maxHeight||t.maxHeight)}_fitRows(t,e,i,n){const o=this,{ctx:s,maxWidth:a,options:{labels:{padding:r}}}=o,l=o.legendHitBoxes=[],c=o.lineWidths=[0],h=n+r;let d=t;s.textAlign="left",s.textBaseline="middle";let u=-1,f=-h;return o.legendItems.forEach(((t,o)=>{const g=i+e/2+s.measureText(t.text).width;(0===o||c[c.length-1]+g+2*r>a)&&(d+=h,c[c.length-(o>0?0:1)]=0,f+=h,u++),l[o]={left:0,top:f,row:u,width:g,height:n},c[c.length-1]+=g+r})),d}_fitCols(t,e,i,n){const o=this,{ctx:s,maxHeight:a,options:{labels:{padding:r}}}=o,l=o.legendHitBoxes=[],c=o.columnSizes=[],h=a-t;let d=r,u=0,f=0,g=0,p=0,m=0;return o.legendItems.forEach(((t,o)=>{const a=i+e/2+s.measureText(t.text).width;o>0&&f+e+2*r>h&&(d+=u+r,c.push({width:u,height:f}),g+=u+r,m++,p=0,u=f=0),u=Math.max(u,a),f+=e+r,l[o]={left:g,top:p,col:m,width:a,height:n},p+=n+r})),d+=u,c.push({width:u,height:f}),d}adjustHitBoxes(){const t=this;if(!t.options.display)return;const e=t._computeTitleHeight(),{legendHitBoxes:i,options:{align:n,labels:{padding:s}}}=t;if(this.isHorizontal()){let a=0,r=o(n,t.left+s,t.right-t.lineWidths[a]);for(const l of i)a!==l.row&&(a=l.row,r=o(n,t.left+s,t.right-t.lineWidths[a])),l.top+=t.top+e+s,l.left=r,r+=l.width+s}else{let a=0,r=o(n,t.top+e+s,t.bottom-t.columnSizes[a].height);for(const l of i)l.col!==a&&(a=l.col,r=o(n,t.top+e+s,t.bottom-t.columnSizes[a].height)),l.top=r,l.left+=t.left+s,r+=l.height+s}}isHorizontal(){return"top"===this.options.position||"bottom"===this.options.position}draw(){const t=this;if(t.options.display){const e=t.ctx;Gt(e,t),t._draw(),Zt(e)}}_draw(){const t=this,{options:e,columnSizes:i,lineWidths:n,ctx:a}=t,{align:r,labels:l}=e,c=mt.color,h=fn(e.rtl,t.left,t.width),d=Ee(l.font),{color:u,padding:f}=l,g=d.size,p=g/2;let m;t.drawTitle(),a.textAlign=h.textAlign("left"),a.textBaseline="middle",a.lineWidth=.5,a.strokeStyle=u,a.fillStyle=u,a.font=d.string;const{boxWidth:x,boxHeight:b,itemHeight:_}=Jo(l,g),y=t.isHorizontal(),v=this._computeTitleHeight();m=y?{x:o(r,t.left+f,t.right-n[0]),y:t.top+f+v,line:0}:{x:t.left+f,y:o(r,t.top+v+f,t.bottom-i[0].height),line:0},gn(t.ctx,e.textDirection);const M=_+f;t.legendItems.forEach(((e,u)=>{const w=a.measureText(e.text).width,k=h.textAlign(e.textAlign||(e.textAlign=l.textAlign)),S=x+g/2+w;let P=m.x,D=m.y;h.setWidth(t.width),y?u>0&&P+S+f>t.right&&(D=m.y+=M,m.line++,P=m.x=o(r,t.left+f,t.right-n[m.line])):u>0&&D+M>t.bottom&&(P=m.x=P+i[m.line].width+f,m.line++,D=m.y=o(r,t.top+v+f,t.bottom-i[m.line].height));!function(t,e,i){if(isNaN(x)||x<=0||isNaN(b)||b<0)return;a.save();const n=K(i.lineWidth,1);if(a.fillStyle=K(i.fillStyle,c),a.lineCap=K(i.lineCap,"butt"),a.lineDashOffset=K(i.lineDashOffset,0),a.lineJoin=K(i.lineJoin,"miter"),a.lineWidth=n,a.strokeStyle=K(i.strokeStyle,c),a.setLineDash(K(i.lineDash,[])),l.usePointStyle){const o={radius:x*Math.SQRT2/2,pointStyle:i.pointStyle,rotation:i.rotation,borderWidth:n},s=h.xPlus(t,x/2);qt(a,o,s,e+p)}else{const i=e+Math.max((g-b)/2,0);a.fillRect(h.leftForLtr(t,x),i,x,b),0!==n&&a.strokeRect(h.leftForLtr(t,x),i,x,b)}a.restore()}(h.x(P),D,e),P=s(k,P+x+p,t.right),function(t,e,i){te(a,i.text,t,e+_/2,d,{strikethrough:i.hidden,textAlign:i.textAlign})}(h.x(P),D,e),y?m.x+=S+f:m.y+=M})),pn(t.ctx,e.textDirection)}drawTitle(){const t=this,e=t.options,i=e.title,s=Ee(i.font),a=Re(i.padding);if(!i.display)return;const r=fn(e.rtl,t.left,t.width),l=t.ctx,c=i.position,h=s.size/2,d=a.top+h;let u,f=t.left,g=t.width;if(this.isHorizontal())g=Math.max(...t.lineWidths),u=t.top+d,f=o(e.align,f,t.right-g);else{const i=t.columnSizes.reduce(((t,e)=>Math.max(t,e.height)),0);u=d+o(e.align,t.top,t.bottom-i-e.labels.padding-t._computeTitleHeight())}const p=o(c,f,f+g);l.textAlign=r.textAlign(n(c)),l.textBaseline="middle",l.strokeStyle=i.color,l.fillStyle=i.color,l.font=s.string,te(l,i.text,p,u,s)}_computeTitleHeight(){const t=this.options.title,e=Ee(t.font),i=Re(t.padding);return t.display?e.lineHeight+i.height:0}_getLegendItemAt(t,e){const i=this;let n,o,s;if(t>=i.left&&t<=i.right&&e>=i.top&&e<=i.bottom)for(s=i.legendHitBoxes,n=0;n=o.left&&t<=o.left+o.width&&e>=o.top&&e<=o.top+o.height)return i.legendItems[n];return null}handleEvent(t){const e=this,i=e.options;if(!function(t,e){if("mousemove"===t&&(e.onHover||e.onLeave))return!0;if(e.onClick&&("click"===t||"mouseup"===t))return!0;return!1}(t.type,i))return;const n=e._getLegendItemAt(t.x,t.y);if("mousemove"===t.type){const a=e._hoveredItem,r=(s=n,null!==(o=a)&&null!==s&&o.datasetIndex===s.datasetIndex&&o.index===s.index);a&&!r&&Q(i.onLeave,[t,a,e],e),e._hoveredItem=n,n&&!r&&Q(i.onHover,[t,n,e],e)}else n&&Q(i.onClick,[t,n,e],e);var o,s}}var es={id:"legend",_element:ts,start(t,e,i){const n=t.legend=new ts({ctx:t.ctx,options:i,chart:t});Xe.configure(t,n,i),Xe.addBox(t,n)},stop(t){Xe.removeBox(t,t.legend),delete t.legend},beforeUpdate(t,e,i){const n=t.legend;Xe.configure(t,n,i),n.options=i},afterUpdate(t){const e=t.legend;e.buildLabels(),e.adjustHitBoxes()},afterEvent(t,e){e.replay||t.legend.handleEvent(e.event)},defaults:{display:!0,position:"top",align:"center",fullSize:!0,reverse:!1,weight:1e3,onClick(t,e,i){const n=e.datasetIndex,o=i.chart;o.isDatasetVisible(n)?(o.hide(n),e.hidden=!0):(o.show(n),e.hidden=!1)},onHover:null,onLeave:null,labels:{color:t=>t.chart.options.color,boxWidth:40,padding:10,generateLabels(t){const e=t.data.datasets,{labels:{usePointStyle:i,pointStyle:n,textAlign:o}}=t.legend.options;return t._getSortedDatasetMetas().map((t=>{const s=t.controller.getStyle(i?0:void 0),a=Re(s.borderWidth);return{text:e[t.index].label,fillStyle:s.backgroundColor,hidden:!t.visible,lineCap:s.borderCapStyle,lineDash:s.borderDash,lineDashOffset:s.borderDashOffset,lineJoin:s.borderJoinStyle,lineWidth:(a.width+a.height)/4,strokeStyle:s.borderColor,pointStyle:n||s.pointStyle,rotation:s.rotation,textAlign:o||s.textAlign,datasetIndex:t.index}}),this)}},title:{color:t=>t.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:t=>!t.startsWith("on"),labels:{_scriptable:t=>!["generateLabels","filter","sort"].includes(t)}}};class is extends Oi{constructor(t){super(),this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e){const i=this,n=i.options;if(i.left=0,i.top=0,!n.display)return void(i.width=i.height=i.right=i.bottom=0);i.width=i.right=t,i.height=i.bottom=e;const o=Y(n.text)?n.text.length:1;i._padding=Re(n.padding);const s=o*Ee(n.font).lineHeight+i._padding.height;i.isHorizontal()?i.height=s:i.width=s}isHorizontal(){const t=this.options.position;return"top"===t||"bottom"===t}_drawArgs(t){const{top:e,left:i,bottom:n,right:s,options:a}=this,r=a.align;let l,c,h,d=0;return this.isHorizontal()?(c=o(r,i,s),h=e+t,l=s-i):("left"===a.position?(c=i+t,h=o(r,n,e),d=-.5*xt):(c=s-t,h=o(r,e,n),d=.5*xt),l=n-e),{titleX:c,titleY:h,maxWidth:l,rotation:d}}draw(){const t=this,e=t.ctx,i=t.options;if(!i.display)return;const o=Ee(i.font),s=o.lineHeight/2+t._padding.top,{titleX:a,titleY:r,maxWidth:l,rotation:c}=t._drawArgs(s);te(e,i.text,0,0,o,{color:i.color,maxWidth:l,rotation:c,textAlign:n(i.align),textBaseline:"middle",translation:[a,r]})}}var ns={id:"title",_element:is,start(t,e,i){!function(t,e){const i=new is({ctx:t.ctx,options:e,chart:t});Xe.configure(t,i,e),Xe.addBox(t,i),t.titleBlock=i}(t,i)},stop(t){const e=t.titleBlock;Xe.removeBox(t,e),delete t.titleBlock},beforeUpdate(t,e,i){const n=t.titleBlock;Xe.configure(t,n,i),n.options=i},defaults:{align:"center",display:!1,font:{style:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const os={average(t){if(!t.length)return!1;let e,i,n=0,o=0,s=0;for(e=0,i=t.length;e-1?t.split("\n"):t}function rs(t,e){const{element:i,datasetIndex:n,index:o}=e,s=t.getDatasetMeta(n).controller,{label:a,value:r}=s.getLabelAndValue(o);return{chart:t,label:a,parsed:s.getParsed(o),raw:t.data.datasets[n].data[o],formattedValue:r,dataset:s.getDataset(),dataIndex:o,datasetIndex:n,element:i}}function ls(t,e){const i=t._chart.ctx,{body:n,footer:o,title:s}=t,{boxWidth:a,boxHeight:r}=e,l=Ee(e.bodyFont),c=Ee(e.titleFont),h=Ee(e.footerFont),d=s.length,u=o.length,f=n.length,g=Re(e.padding);let p=g.height,m=0,x=n.reduce(((t,e)=>t+e.before.length+e.lines.length+e.after.length),0);if(x+=t.beforeBody.length+t.afterBody.length,d&&(p+=d*c.lineHeight+(d-1)*e.titleSpacing+e.titleMarginBottom),x){p+=f*(e.displayColors?Math.max(r,l.lineHeight):l.lineHeight)+(x-f)*l.lineHeight+(x-1)*e.bodySpacing}u&&(p+=e.footerMarginTop+u*h.lineHeight+(u-1)*e.footerSpacing);let b=0;const _=function(t){m=Math.max(m,i.measureText(t).width+b)};return i.save(),i.font=c.string,J(t.title,_),i.font=l.string,J(t.beforeBody.concat(t.afterBody),_),b=e.displayColors?a+2:0,J(n,(t=>{J(t.before,_),J(t.lines,_),J(t.after,_)})),b=0,i.font=h.string,J(t.footer,_),i.restore(),m+=g.width,{width:m,height:p}}function cs(t,e,i,n){const{x:o,width:s}=i,{width:a,chartArea:{left:r,right:l}}=t;let c="center";return"center"===n?c=o<=(r+l)/2?"left":"right":o<=s/2?c="left":o>=a-s/2&&(c="right"),function(t,e,i,n){const{x:o,width:s}=n,a=i.caretSize+i.caretPadding;return"left"===t&&o+s+a>e.width||"right"===t&&o-s-a<0||void 0}(c,t,e,i)&&(c="center"),c}function hs(t,e,i){const n=e.yAlign||function(t,e){const{y:i,height:n}=e;return it.height-n/2?"bottom":"center"}(t,i);return{xAlign:e.xAlign||cs(t,e,i,n),yAlign:n}}function ds(t,e,i,n){const{caretSize:o,caretPadding:s,cornerRadius:a}=t,{xAlign:r,yAlign:l}=i,c=o+s,h=a+s;let d=function(t,e){let{x:i,width:n}=t;return"right"===e?i-=n:"center"===e&&(i-=n/2),i}(e,r);const u=function(t,e,i){let{y:n,height:o}=t;return"top"===e?n+=i:n-="bottom"===e?o+i:o/2,n}(e,l,c);return"center"===l?"left"===r?d+=c:"right"===r&&(d-=c):"left"===r?d-=h:"right"===r&&(d+=h),{x:Ht(d,0,n.width-e.width),y:Ht(u,0,n.height-e.height)}}function us(t,e,i){const n=Re(i.padding);return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-n.right:t.x+n.left}function fs(t){return ss([],as(t))}function gs(t,e){const i=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return i?t.override(i):t}class ps extends Oi{constructor(t){super(),this.opacity=0,this._active=[],this._chart=t._chart,this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.options=t.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(t){this.options=t,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){const t=this,e=t._cachedAnimations;if(e)return e;const i=t._chart,n=t.options.setContext(t.getContext()),o=n.enabled&&i.options.animation&&n.animations,s=new bi(t._chart,o);return o._cacheable&&(t._cachedAnimations=Object.freeze(s)),s}getContext(){const t=this;return t.$context||(t.$context=(e=t._chart.getContext(),i=t,n=t._tooltipItems,Object.assign(Object.create(e),{tooltip:i,tooltipItems:n,type:"tooltip"})));var e,i,n}getTitle(t,e){const i=this,{callbacks:n}=e,o=n.beforeTitle.apply(i,[t]),s=n.title.apply(i,[t]),a=n.afterTitle.apply(i,[t]);let r=[];return r=ss(r,as(o)),r=ss(r,as(s)),r=ss(r,as(a)),r}getBeforeBody(t,e){return fs(e.callbacks.beforeBody.apply(this,[t]))}getBody(t,e){const i=this,{callbacks:n}=e,o=[];return J(t,(t=>{const e={before:[],lines:[],after:[]},s=gs(n,t);ss(e.before,as(s.beforeLabel.call(i,t))),ss(e.lines,s.label.call(i,t)),ss(e.after,as(s.afterLabel.call(i,t))),o.push(e)})),o}getAfterBody(t,e){return fs(e.callbacks.afterBody.apply(this,[t]))}getFooter(t,e){const i=this,{callbacks:n}=e,o=n.beforeFooter.apply(i,[t]),s=n.footer.apply(i,[t]),a=n.afterFooter.apply(i,[t]);let r=[];return r=ss(r,as(o)),r=ss(r,as(s)),r=ss(r,as(a)),r}_createItems(t){const e=this,i=e._active,n=e._chart.data,o=[],s=[],a=[];let r,l,c=[];for(r=0,l=i.length;rt.filter(e,i,o,n)))),t.itemSort&&(c=c.sort(((e,i)=>t.itemSort(e,i,n)))),J(c,(i=>{const n=gs(t.callbacks,i);o.push(n.labelColor.call(e,i)),s.push(n.labelPointStyle.call(e,i)),a.push(n.labelTextColor.call(e,i))})),e.labelColors=o,e.labelPointStyles=s,e.labelTextColors=a,e.dataPoints=c,c}update(t,e){const i=this,n=i.options.setContext(i.getContext()),o=i._active;let s,a=[];if(o.length){const t=os[n.position].call(i,o,i._eventPosition);a=i._createItems(n),i.title=i.getTitle(a,n),i.beforeBody=i.getBeforeBody(a,n),i.body=i.getBody(a,n),i.afterBody=i.getAfterBody(a,n),i.footer=i.getFooter(a,n);const e=i._size=ls(i,n),r=Object.assign({},t,e),l=hs(i._chart,n,r),c=ds(n,r,l,i._chart);i.xAlign=l.xAlign,i.yAlign=l.yAlign,s={opacity:1,x:c.x,y:c.y,width:e.width,height:e.height,caretX:t.x,caretY:t.y}}else 0!==i.opacity&&(s={opacity:0});i._tooltipItems=a,i.$context=void 0,s&&i._resolveAnimations().update(i,s),t&&n.external&&n.external.call(i,{chart:i._chart,tooltip:i,replay:e})}drawCaret(t,e,i,n){const o=this.getCaretPosition(t,i,n);e.lineTo(o.x1,o.y1),e.lineTo(o.x2,o.y2),e.lineTo(o.x3,o.y3)}getCaretPosition(t,e,i){const{xAlign:n,yAlign:o}=this,{cornerRadius:s,caretSize:a}=i,{x:r,y:l}=t,{width:c,height:h}=e;let d,u,f,g,p,m;return"center"===o?(p=l+h/2,"left"===n?(d=r,u=d-a,g=p+a,m=p-a):(d=r+c,u=d+a,g=p-a,m=p+a),f=d):(u="left"===n?r+s+a:"right"===n?r+c-s-a:this.caretX,"top"===o?(g=l,p=g-a,d=u-a,f=u+a):(g=l+h,p=g+a,d=u+a,f=u-a),m=g),{x1:d,x2:u,x3:f,y1:g,y2:p,y3:m}}drawTitle(t,e,i){const n=this,o=n.title,s=o.length;let a,r,l;if(s){const c=fn(i.rtl,n.x,n.width);for(t.x=us(n,i.titleAlign,i),e.textAlign=c.textAlign(i.titleAlign),e.textBaseline="middle",a=Ee(i.titleFont),r=i.titleSpacing,e.fillStyle=i.titleColor,e.font=a.string,l=0;l0&&e.stroke()}_updateAnimationTarget(t){const e=this,i=e._chart,n=e.$animations,o=n&&n.x,s=n&&n.y;if(o||s){const n=os[t.position].call(e,e._active,e._eventPosition);if(!n)return;const a=e._size=ls(e,t),r=Object.assign({},n,e._size),l=hs(i,t,r),c=ds(t,r,l,i);o._to===c.x&&s._to===c.y||(e.xAlign=l.xAlign,e.yAlign=l.yAlign,e.width=a.width,e.height=a.height,e.caretX=n.x,e.caretY=n.y,e._resolveAnimations().update(e,c))}}draw(t){const e=this,i=e.options.setContext(e.getContext());let n=e.opacity;if(!n)return;e._updateAnimationTarget(i);const o={width:e.width,height:e.height},s={x:e.x,y:e.y};n=Math.abs(n)<.001?0:n;const a=Re(i.padding),r=e.title.length||e.beforeBody.length||e.body.length||e.afterBody.length||e.footer.length;i.enabled&&r&&(t.save(),t.globalAlpha=n,e.drawBackground(s,t,o,i),gn(t,i.textDirection),s.y+=a.top,e.drawTitle(s,t,i),e.drawBody(s,t,i),e.drawFooter(s,t,i),pn(t,i.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,e){const i=this,n=i._active,o=t.map((({datasetIndex:t,index:e})=>{const n=i._chart.getDatasetMeta(t);if(!n)throw new Error("Cannot find a dataset at index "+t);return{datasetIndex:t,element:n.data[e],index:e}})),s=!tt(n,o),a=i._positionChanged(o,e);(s||a)&&(i._active=o,i._eventPosition=e,i.update(!0))}handleEvent(t,e){const i=this,n=i.options,o=i._active||[];let s=!1,a=[];"mouseout"!==t.type&&(a=i._chart.getElementsAtEventForMode(t,n.mode,n,e),n.reverse&&a.reverse());const r=i._positionChanged(a,t);return s=e||!tt(a,o)||r,s&&(i._active=a,(n.enabled||n.external)&&(i._eventPosition={x:t.x,y:t.y},i.update(!0,e))),s}_positionChanged(t,e){const i=this,n=os[i.options.position].call(i,t,e);return i.caretX!==n.x||i.caretY!==n.y}}ps.positioners=os;var ms={id:"tooltip",_element:ps,positioners:os,afterInit(t,e,i){i&&(t.tooltip=new ps({_chart:t,options:i}))},beforeUpdate(t,e,i){t.tooltip&&t.tooltip.initialize(i)},reset(t,e,i){t.tooltip&&t.tooltip.initialize(i)},afterDraw(t){const e=t.tooltip,i={tooltip:e};!1!==t.notifyPlugins("beforeTooltipDraw",i)&&(e&&e.draw(t.ctx),t.notifyPlugins("afterTooltipDraw",i))},afterEvent(t,e){if(t.tooltip){const i=e.replay;t.tooltip.handleEvent(e.event,i)&&(e.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{style:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{style:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(t,e)=>e.bodyFont.size,boxWidth:(t,e)=>e.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:{beforeTitle:N,title(t){if(t.length>0){const e=t[0],i=e.chart.data.labels,n=i?i.length:0;if(this&&this.options&&"dataset"===this.options.mode)return e.dataset.label||"";if(e.label)return e.label;if(n>0&&e.dataIndex"filter"!==t&&"itemSort"!==t&&"external"!==t,_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]},xs=Object.freeze({__proto__:null,Decimation:Fo,Filler:Qo,Legend:es,Title:ns,Tooltip:ms});function bs(t,e,i){const n=t.indexOf(e);if(-1===n)return((t,e,i)=>"string"==typeof e?t.push(e)-1:isNaN(e)?null:i)(t,e,i);return n!==t.lastIndexOf(e)?i:n}class _s extends ji{constructor(t){super(t),this._startValue=void 0,this._valueRange=0}parse(t,e){if($(t))return null;const i=this.getLabels();return((t,e)=>null===t?null:Ht(Math.round(t),0,e))(e=isFinite(e)&&i[e]===t?e:bs(i,t,K(e,t)),i.length-1)}determineDataLimits(){const t=this,{minDefined:e,maxDefined:i}=t.getUserBounds();let{min:n,max:o}=t.getMinMax(!0);"ticks"===t.options.bounds&&(e||(n=0),i||(o=t.getLabels().length-1)),t.min=n,t.max=o}buildTicks(){const t=this,e=t.min,i=t.max,n=t.options.offset,o=[];let s=t.getLabels();s=0===e&&i===s.length-1?s:s.slice(e,i+1),t._valueRange=Math.max(s.length-(n?0:1),1),t._startValue=t.min-(n?.5:0);for(let t=e;t<=i;t++)o.push({value:t});return o}getLabelForValue(t){const e=this.getLabels();return t>=0&&te.length-1?null:this.getPixelForValue(e[t].value)}getValueForPixel(t){const e=this;return Math.round(e._startValue+e.getDecimalForPixel(t)*e._valueRange)}getBasePixel(){return this.bottom}}_s.id="category",_s.defaults={ticks:{callback:_s.prototype.getLabelForValue}};class ys extends ji{constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._endValue=void 0,this._valueRange=0}parse(t,e){return $(t)||("number"==typeof t||t instanceof Number)&&!isFinite(+t)?null:+t}handleTickRangeOptions(){const t=this,{beginAtZero:e,stacked:i}=t.options,{minDefined:n,maxDefined:o}=t.getUserBounds();let{min:s,max:a}=t;const r=t=>s=n?s:t,l=t=>a=o?a:t;if(e||i){const t=Pt(s),e=Pt(a);t<0&&e<0?l(0):t>0&&e>0&&r(0)}s===a&&(l(a+1),e||r(s-1)),t.min=s,t.max=a}getTickLimit(){const t=this,e=t.options.ticks;let i,{maxTicksLimit:n,stepSize:o}=e;return o?i=Math.ceil(t.max/o)-Math.floor(t.min/o)+1:(i=t.computeTickLimit(),n=n||11),n&&(i=Math.min(n,i)),i}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const t=this,e=t.options,i=e.ticks;let n=t.getTickLimit();n=Math.max(2,n);const o=function(t,e){const i=[],{step:n,min:o,max:s,precision:a,count:r,maxTicks:l}=t,c=n||1,h=l-1,{min:d,max:u}=e,f=!$(o),g=!$(s),p=!$(r);let m,x,b,_,y=Dt((u-d)/h/c)*c;if(y<1e-14&&!f&&!g)return[{value:d},{value:u}];_=Math.ceil(u/y)-Math.floor(d/y),_>h&&(y=Dt(_*y/h/c)*c),$(a)||(m=Math.pow(10,a),y=Math.ceil(y*m)/m),x=Math.floor(d/y)*y,b=Math.ceil(u/y)*y,f&&g&&n&&Tt((s-o)/n,y/1e3)?(_=Math.min((s-o)/y,l),y=(s-o)/_,x=o,b=s):p?(x=f?o:x,b=g?s:b,_=r-1,y=(b-x)/_):(_=(b-x)/y,_=Ot(_,Math.round(_),y/1e3)?Math.round(_):Math.ceil(_)),m=Math.pow(10,$(a)?It(y):a),x=Math.round(x*m)/m,b=Math.round(b*m)/m;let v=0;for(f&&(i.push({value:o}),x<=o&&v++,Ot(Math.round((x+v*y)*m)/m,o,y/10)&&v++);v<_;++v)i.push({value:Math.round((x+v*y)*m)/m});return g?Ot(i[i.length-1].value,s,y/10)?i[i.length-1].value=s:i.push({value:s}):i.push({value:b}),i}({maxTicks:n,min:e.min,max:e.max,precision:i.precision,step:i.stepSize,count:i.count},Fe(t,e.grace));return"ticks"===e.bounds&&Lt(o,t,"value"),e.reverse?(o.reverse(),t.start=t.max,t.end=t.min):(t.start=t.min,t.end=t.max),o}configure(){const t=this,e=t.ticks;let i=t.min,n=t.max;if(super.configure(),t.options.offset&&e.length){const t=(n-i)/Math.max(e.length-1,1)/2;i-=t,n+=t}t._startValue=i,t._endValue=n,t._valueRange=n-i}getLabelForValue(t){return Li(t,this.chart.options.locale)}}class vs extends ys{determineDataLimits(){const t=this,{min:e,max:i}=t.getMinMax(!0);t.min=X(e)?e:0,t.max=X(i)?i:1,t.handleTickRangeOptions()}computeTickLimit(){const t=this;if(t.isHorizontal())return Math.ceil(t.width/40);const e=t._resolveTickFontOptions(0);return Math.ceil(t.height/e.lineHeight)}getPixelForValue(t){return null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getValueForPixel(t){return this._startValue+this.getDecimalForPixel(t)*this._valueRange}}function Ms(t){return 1===t/Math.pow(10,Math.floor(St(t)))}vs.id="linear",vs.defaults={ticks:{callback:Ei.formatters.numeric}};class ws extends ji{constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(t,e){const i=ys.prototype.parse.apply(this,[t,e]);if(0!==i)return X(i)&&i>0?i:null;this._zero=!0}determineDataLimits(){const t=this,{min:e,max:i}=t.getMinMax(!0);t.min=X(e)?Math.max(0,e):null,t.max=X(i)?Math.max(0,i):null,t.options.beginAtZero&&(t._zero=!0),t.handleTickRangeOptions()}handleTickRangeOptions(){const t=this,{minDefined:e,maxDefined:i}=t.getUserBounds();let n=t.min,o=t.max;const s=t=>n=e?n:t,a=t=>o=i?o:t,r=(t,e)=>Math.pow(10,Math.floor(St(t))+e);n===o&&(n<=0?(s(1),a(10)):(s(r(n,-1)),a(r(o,1)))),n<=0&&s(r(o,-1)),o<=0&&a(r(n,1)),t._zero&&t.min!==t._suggestedMin&&n===r(t.min,0)&&s(r(n,-1)),t.min=n,t.max=o}buildTicks(){const t=this,e=t.options,i=function(t,e){const i=Math.floor(St(e.max)),n=Math.ceil(e.max/Math.pow(10,i)),o=[];let s=q(t.min,Math.pow(10,Math.floor(St(e.min)))),a=Math.floor(St(s)),r=Math.floor(s/Math.pow(10,a)),l=a<0?Math.pow(10,Math.abs(a)):1;do{o.push({value:s,major:Ms(s)}),++r,10===r&&(r=1,++a,l=a>=0?1:l),s=Math.round(r*Math.pow(10,a)*l)/l}while(ao?{start:e-i,end:e}:{start:e,end:e+i}}function Ps(t){return 0===t||180===t?"center":t<180?"left":"right"}function Ds(t,e,i){90===t||270===t?i.y-=e.h/2:(t>270||t<90)&&(i.y-=e.h)}function Cs(t,e,i,n){const{ctx:o}=t;if(i)o.arc(t.xCenter,t.yCenter,e,0,bt);else{let i=t.getPointPosition(0,e);o.moveTo(i.x,i.y);for(let s=1;s{const n=Q(e.options.pointLabels.callback,[t,i],e);return n||0===n?n:""}))}fit(){const t=this,e=t.options;e.display&&e.pointLabels.display?function(t){const e={l:0,r:t.width,t:0,b:t.height-t.paddingTop},i={};let n,o,s;const a=[],r=[],l=t.getLabels().length;for(n=0;ne.r&&(e.r=p.end,i.r=f),m.starte.b&&(e.b=m.end,i.b=f)}var c,h,d;t._setReductions(t.drawingArea,e,i),t._pointLabelItems=[];const u=t.options,f=ks(u),g=t.getDistanceFromCenterForValue(u.ticks.reverse?t.min:t.max);for(n=0;n=0;o--){const e=n.setContext(t.getContext(o)),s=Ee(e.font),{x:a,y:r,textAlign:l,left:c,top:h,right:d,bottom:u}=t._pointLabelItems[o],{backdropColor:f}=e;if(!$(f)){const t=Re(e.backdropPadding);i.fillStyle=f,i.fillRect(c-t.left,h-t.top,d-c+t.width,u-h+t.height)}te(i,t._pointLabels[o],a,r+s.lineHeight/2,s,{color:e.color,textAlign:l,textBaseline:"middle"})}}(t,s),o.display&&t.ticks.forEach(((e,i)=>{if(0!==i){r=t.getDistanceFromCenterForValue(e.value);const n=o.setContext(t.getContext(i-1));!function(t,e,i,n){const o=t.ctx,s=e.circular,{color:a,lineWidth:r}=e;!s&&!n||!a||!r||i<0||(o.save(),o.strokeStyle=a,o.lineWidth=r,o.setLineDash(e.borderDash),o.lineDashOffset=e.borderDashOffset,o.beginPath(),Cs(t,i,s,n),o.closePath(),o.stroke(),o.restore())}(t,n,r,s)}})),n.display){for(e.save(),a=t.getLabels().length-1;a>=0;a--){const o=n.setContext(t.getContext(a)),{color:s,lineWidth:c}=o;c&&s&&(e.lineWidth=c,e.strokeStyle=s,e.setLineDash(o.borderDash),e.lineDashOffset=o.borderDashOffset,r=t.getDistanceFromCenterForValue(i.ticks.reverse?t.min:t.max),l=t.getPointPosition(a,r),e.beginPath(),e.moveTo(t.xCenter,t.yCenter),e.lineTo(l.x,l.y),e.stroke())}e.restore()}}drawLabels(){const t=this,e=t.ctx,i=t.options,n=i.ticks;if(!n.display)return;const o=t.getIndexAngle(0);let s,a;e.save(),e.translate(t.xCenter,t.yCenter),e.rotate(o),e.textAlign="center",e.textBaseline="middle",t.ticks.forEach(((o,r)=>{if(0===r&&!i.reverse)return;const l=n.setContext(t.getContext(r)),c=Ee(l.font);if(s=t.getDistanceFromCenterForValue(t.ticks[r].value),l.showLabelBackdrop){a=e.measureText(o.label).width,e.fillStyle=l.backdropColor;const t=Re(l.backdropPadding);e.fillRect(-a/2-t.left,-s-c.size/2-t.top,a+t.width,c.size+t.height)}te(e,o.label,0,-s,c,{color:l.color})})),e.restore()}drawTitle(){}}Os.id="radialLinear",Os.defaults={display:!0,animate:!0,position:"chartArea",angleLines:{display:!0,lineWidth:1,borderDash:[],borderDashOffset:0},grid:{circular:!1},startAngle:0,ticks:{showLabelBackdrop:!0,backdropColor:"rgba(255,255,255,0.75)",backdropPadding:2,callback:Ei.formatters.numeric},pointLabels:{backdropColor:void 0,backdropPadding:2,display:!0,font:{size:10},callback:t=>t,padding:5}},Os.defaultRoutes={"angleLines.color":"borderColor","pointLabels.color":"color","ticks.color":"color"},Os.descriptors={angleLines:{_fallback:"grid"}};const Ts={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},Ls=Object.keys(Ts);function Rs(t,e){return t-e}function Es(t,e){if($(e))return null;const i=t._adapter,n=t.options.time,{parser:o,round:s,isoWeekday:a}=n;let r=e;return"function"==typeof o&&(r=o(r)),X(r)||(r="string"==typeof o?i.parse(r,o):i.parse(r)),null===r?null:(s&&(r="week"!==s||!At(a)&&!0!==a?i.startOf(r,s):i.startOf(r,"isoWeek",a)),+r)}function Is(t,e,i,n){const o=Ls.length;for(let s=Ls.indexOf(t);s=e?i[n]:i[o]]=!0}}else t[e]=!0}function zs(t,e,i){const n=[],o={},s=e.length;let a,r;for(a=0;a=0&&(e[l].major=!0);return e}(t,n,o,i):n}class Vs extends ji{constructor(t){super(t),this._cache={data:[],labels:[],all:[]},this._unit="day",this._majorUnit=void 0,this._offsets={},this._normalized=!1}init(t,e){const i=t.time||(t.time={}),n=this._adapter=new Gn._date(t.adapters.date);st(i.displayFormats,n.formats()),super.init(t),this._normalized=e.normalized}parse(t,e){return void 0===t?null:Es(this,t)}beforeLayout(){super.beforeLayout(),this._cache={data:[],labels:[],all:[]}}determineDataLimits(){const t=this,e=t.options,i=t._adapter,n=e.time.unit||"day";let{min:o,max:s,minDefined:a,maxDefined:r}=t.getUserBounds();function l(t){a||isNaN(t.min)||(o=Math.min(o,t.min)),r||isNaN(t.max)||(s=Math.max(s,t.max))}a&&r||(l(t._getLabelBounds()),"ticks"===e.bounds&&"labels"===e.ticks.source||l(t.getMinMax(!1))),o=X(o)&&!isNaN(o)?o:+i.startOf(Date.now(),n),s=X(s)&&!isNaN(s)?s:+i.endOf(Date.now(),n)+1,t.min=Math.min(o,s-1),t.max=Math.max(o+1,s)}_getLabelBounds(){const t=this.getLabelTimestamps();let e=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;return t.length&&(e=t[0],i=t[t.length-1]),{min:e,max:i}}buildTicks(){const t=this,e=t.options,i=e.time,n=e.ticks,o="labels"===n.source?t.getLabelTimestamps():t._generate();"ticks"===e.bounds&&o.length&&(t.min=t._userMin||o[0],t.max=t._userMax||o[o.length-1]);const s=t.min,a=oe(o,s,t.max);return t._unit=i.unit||(n.autoSkip?Is(i.minUnit,t.min,t.max,t._getLabelCapacity(s)):function(t,e,i,n,o){for(let s=Ls.length-1;s>=Ls.indexOf(i);s--){const i=Ls[s];if(Ts[i].common&&t._adapter.diff(o,n,i)>=e-1)return i}return Ls[i?Ls.indexOf(i):0]}(t,a.length,i.minUnit,t.min,t.max)),t._majorUnit=n.major.enabled&&"year"!==t._unit?function(t){for(let e=Ls.indexOf(t)+1,i=Ls.length;e1e5*r)throw new Error(i+" and "+n+" are too far apart with stepSize of "+r+" "+a);const g="data"===o.ticks.source&&t.getDataTimestamps();for(d=f,u=0;dt-e)).map((t=>+t))}getLabelForValue(t){const e=this._adapter,i=this.options.time;return i.tooltipFormat?e.format(t,i.tooltipFormat):e.format(t,i.displayFormats.datetime)}_tickFormatFunction(t,e,i,n){const o=this,s=o.options,a=s.time.displayFormats,r=o._unit,l=o._majorUnit,c=r&&a[r],h=l&&a[l],d=i[e],u=l&&h&&d&&d.major,f=o._adapter.format(t,n||(u?h:c)),g=s.ticks.callback;return g?g(f,e,i):f}generateTickLabels(t){let e,i,n;for(e=0,i=t.length;e0?r:1}getDataTimestamps(){const t=this;let e,i,n=t._cache.data||[];if(n.length)return n;const o=t.getMatchingVisibleMetas();if(t._normalized&&o.length)return t._cache.data=o[0].controller.getAllParsedValues(t);for(e=0,i=o.length;ee&&a0&&!$(e)?e/i._maxIndex:i.getDecimalForValue(t);return i.getPixelForDecimal((n.start+o)*n.factor)}getDecimalForValue(t){return Bs(this._table,t)/this._maxIndex}getValueForPixel(t){const e=this,i=e._offsets,n=e.getDecimalForPixel(t)/i.factor-i.end;return Bs(e._table,n*this._maxIndex,!0)}}Ws.id="timeseries",Ws.defaults=Vs.defaults;var Hs=Object.freeze({__proto__:null,CategoryScale:_s,LinearScale:vs,LogarithmicScale:ws,RadialLinearScale:Os,TimeScale:Vs,TimeSeriesScale:Ws});return Yn.register(co,Hs,Eo,xs),Yn.helpers={...vn},Yn._adapters=Gn,Yn.Animation=mi,Yn.Animations=bi,Yn.animator=a,Yn.controllers=wn.controllers.items,Yn.DatasetController=Ai,Yn.Element=Oi,Yn.elements=Eo,Yn.Interaction=De,Yn.layouts=Xe,Yn.platforms=ci,Yn.Scale=ji,Yn.Ticks=Ei,Object.assign(Yn,co,Hs,Eo,xs,ci),Yn.Chart=Yn,"undefined"!=typeof window&&(window.Chart=Yn),Yn})); diff --git a/src/main/resources/static/js/plugins/perfect-scrollbar.min.js b/src/main/resources/static/js/plugins/perfect-scrollbar.min.js deleted file mode 100644 index 464c1f7..0000000 --- a/src/main/resources/static/js/plugins/perfect-scrollbar.min.js +++ /dev/null @@ -1,19 +0,0 @@ -/*! - * perfect-scrollbar v1.5.1 - * Copyright 2020 Hyunje Jun, MDBootstrap and Contributors - * Licensed under MIT - */(function(a,b){"object"==typeof exports&&"undefined"!=typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):(a=a||self,a.PerfectScrollbar=b())})(this,function(){'use strict';var u=Math.abs,v=Math.floor;function a(a){return getComputedStyle(a)}function b(a,b){for(var c in b){var d=b[c];"number"==typeof d&&(d+="px"),a.style[c]=d}return a}function c(a){var b=document.createElement("div");return b.className=a,b}function d(a,b){if(!w)throw new Error("No element matching method supported");return w.call(a,b)}function e(a){a.remove?a.remove():a.parentNode&&a.parentNode.removeChild(a)}function f(a,b){return Array.prototype.filter.call(a.children,function(a){return d(a,b)})}function g(a,b){var c=a.element.classList,d=z.state.scrolling(b);c.contains(d)?clearTimeout(A[b]):c.add(d)}function h(a,b){A[b]=setTimeout(function(){return a.isAlive&&a.element.classList.remove(z.state.scrolling(b))},a.settings.scrollingThreshold)}function j(a,b){g(a,b),h(a,b)}function k(a){if("function"==typeof window.CustomEvent)return new CustomEvent(a);var b=document.createEvent("CustomEvent");return b.initCustomEvent(a,!1,!1,void 0),b}function l(a,b,c,d,e){void 0===d&&(d=!0),void 0===e&&(e=!1);var f;if("top"===b)f=["contentHeight","containerHeight","scrollTop","y","up","down"];else if("left"===b)f=["contentWidth","containerWidth","scrollLeft","x","left","right"];else throw new Error("A proper axis should be provided");m(a,c,f,d,e)}function m(a,b,c,d,e){var f=c[0],g=c[1],h=c[2],i=c[3],l=c[4],m=c[5];void 0===d&&(d=!0),void 0===e&&(e=!1);var n=a.element;// reset reach -a.reach[i]=null,1>n[h]&&(a.reach[i]="start"),n[h]>a[f]-a[g]-1&&(a.reach[i]="end"),b&&(n.dispatchEvent(k("ps-scroll-"+i)),0>b?n.dispatchEvent(k("ps-scroll-"+l)):0=a.railXWidth-a.scrollbarXWidth&&(a.scrollbarXLeft=a.railXWidth-a.scrollbarXWidth),a.scrollbarYTop>=a.railYHeight-a.scrollbarYHeight&&(a.scrollbarYTop=a.railYHeight-a.scrollbarYHeight),s(c,a),a.scrollbarXActive?c.classList.add(z.state.active("x")):(c.classList.remove(z.state.active("x")),a.scrollbarXWidth=0,a.scrollbarXLeft=0,c.scrollLeft=!0===a.isRtl?a.contentWidth:0),a.scrollbarYActive?c.classList.add(z.state.active("y")):(c.classList.remove(z.state.active("y")),a.scrollbarYHeight=0,a.scrollbarYTop=0,c.scrollTop=0)}function r(a,b){var c=Math.min,d=Math.max;return a.settings.minScrollbarLength&&(b=d(b,a.settings.minScrollbarLength)),a.settings.maxScrollbarLength&&(b=c(b,a.settings.maxScrollbarLength)),b}function s(a,c){var d={width:c.railXWidth},e=v(a.scrollTop);d.left=c.isRtl?c.negativeScrollAdjustment+a.scrollLeft+c.containerWidth-c.contentWidth:a.scrollLeft,c.isScrollbarXUsingBottom?d.bottom=c.scrollbarXBottom-e:d.top=c.scrollbarXTop+e,b(c.scrollbarXRail,d);var f={top:e,height:c.railYHeight};c.isScrollbarYUsingRight?c.isRtl?f.right=c.contentWidth-(c.negativeScrollAdjustment+a.scrollLeft)-c.scrollbarYRight-c.scrollbarYOuterWidth-9:f.right=c.scrollbarYRight-a.scrollLeft:c.isRtl?f.left=c.negativeScrollAdjustment+a.scrollLeft+2*c.containerWidth-c.contentWidth-c.scrollbarYLeft-c.scrollbarYOuterWidth:f.left=c.scrollbarYLeft+a.scrollLeft,b(c.scrollbarYRail,f),b(c.scrollbarX,{left:c.scrollbarXLeft,width:c.scrollbarXWidth-c.railBorderXWidth}),b(c.scrollbarY,{top:c.scrollbarYTop,height:c.scrollbarYHeight-c.railBorderYWidth})}function t(a,b){function c(b){b.touches&&b.touches[0]&&(b[k]=b.touches[0].pageY),s[o]=t+v*(b[k]-u),g(a,p),q(a),b.stopPropagation(),b.preventDefault()}function d(){h(a,p),a[r].classList.remove(z.state.clicking),a.event.unbind(a.ownerDocument,"mousemove",c)}function f(b,e){t=s[o],e&&b.touches&&(b[k]=b.touches[0].pageY),u=b[k],v=(a[j]-a[i])/(a[l]-a[n]),e?a.event.bind(a.ownerDocument,"touchmove",c):(a.event.bind(a.ownerDocument,"mousemove",c),a.event.once(a.ownerDocument,"mouseup",d),b.preventDefault()),a[r].classList.add(z.state.clicking),b.stopPropagation()}var i=b[0],j=b[1],k=b[2],l=b[3],m=b[4],n=b[5],o=b[6],p=b[7],r=b[8],s=a.element,t=null,u=null,v=null;a.event.bind(a[m],"mousedown",function(a){f(a)}),a.event.bind(a[m],"touchstart",function(a){f(a,!0)})}var w="undefined"!=typeof Element&&(Element.prototype.matches||Element.prototype.webkitMatchesSelector||Element.prototype.mozMatchesSelector||Element.prototype.msMatchesSelector),z={main:"ps",rtl:"ps__rtl",element:{thumb:function(a){return"ps__thumb-"+a},rail:function(a){return"ps__rail-"+a},consuming:"ps__child--consume"},state:{focus:"ps--focus",clicking:"ps--clicking",active:function(a){return"ps--active-"+a},scrolling:function(a){return"ps--scrolling-"+a}}},A={x:null,y:null},B=function(a){this.element=a,this.handlers={}},C={isEmpty:{configurable:!0}};B.prototype.bind=function(a,b){"undefined"==typeof this.handlers[a]&&(this.handlers[a]=[]),this.handlers[a].push(b),this.element.addEventListener(a,b,!1)},B.prototype.unbind=function(a,b){var c=this;this.handlers[a]=this.handlers[a].filter(function(d){return!!(b&&d!==b)||(c.element.removeEventListener(a,d,!1),!1)})},B.prototype.unbindAll=function(){for(var a in this.handlers)this.unbind(a)},C.isEmpty.get=function(){var a=this;return Object.keys(this.handlers).every(function(b){return 0===a.handlers[b].length})},Object.defineProperties(B.prototype,C);var D=function(){this.eventElements=[]};D.prototype.eventElement=function(a){var b=this.eventElements.filter(function(b){return b.element===a})[0];return b||(b=new B(a),this.eventElements.push(b)),b},D.prototype.bind=function(a,b,c){this.eventElement(a).bind(b,c)},D.prototype.unbind=function(a,b,c){var d=this.eventElement(a);d.unbind(b,c),d.isEmpty&&this.eventElements.splice(this.eventElements.indexOf(d),1)},D.prototype.unbindAll=function(){this.eventElements.forEach(function(a){return a.unbindAll()}),this.eventElements=[]},D.prototype.once=function(a,b,c){var d=this.eventElement(a),e=function(a){d.unbind(b,e),c(a)};d.bind(b,e)};var E={isWebKit:"undefined"!=typeof document&&"WebkitAppearance"in document.documentElement.style,supportsTouch:"undefined"!=typeof window&&("ontouchstart"in window||"maxTouchPoints"in window.navigator&&0a.scrollbarYTop?1:-1;a.element.scrollTop+=d*a.containerHeight,q(a),b.stopPropagation()}),a.event.bind(a.scrollbarX,"mousedown",function(a){return a.stopPropagation()}),a.event.bind(a.scrollbarXRail,"mousedown",function(b){var c=b.pageX-window.pageXOffset-a.scrollbarXRail.getBoundingClientRect().left,d=c>a.scrollbarXLeft?1:-1;a.element.scrollLeft+=d*a.containerWidth,q(a),b.stopPropagation()})},"drag-thumb":function(a){t(a,["containerWidth","contentWidth","pageX","railXWidth","scrollbarX","scrollbarXWidth","scrollLeft","x","scrollbarXRail"]),t(a,["containerHeight","contentHeight","pageY","railYHeight","scrollbarY","scrollbarYHeight","scrollTop","y","scrollbarYRail"])},keyboard:function(a){function b(b,d){var e=v(c.scrollTop);if(0===b){if(!a.scrollbarYActive)return!1;if(0===e&&0=a.contentHeight-a.containerHeight&&0>d)return!a.settings.wheelPropagation}var f=c.scrollLeft;if(0===d){if(!a.scrollbarXActive)return!1;if(0===f&&0>b||f>=a.contentWidth-a.containerWidth&&0u(a)?f||g:i||j,!d||!b.settings.wheelPropagation}function d(a){var b=a.deltaX,c=-1*a.deltaY;return("undefined"==typeof b||"undefined"==typeof c)&&(b=-1*a.wheelDeltaX/6,c=a.wheelDeltaY/6),a.deltaMode&&1===a.deltaMode&&(b*=10,c*=10),b!==b&&c!==c/* NaN checks */&&(b=0,c=a.wheelDelta),a.shiftKey?[-c,-b]:[b,c]}function f(b,c,d){// FIXME: this is a workaround for -
- - -
- - - - - - - - - - - - diff --git a/src/main/resources/templates/user/forgot-password-change.html b/src/main/resources/templates/user/forgot-password-change.html deleted file mode 100644 index 4e6a0f8..0000000 --- a/src/main/resources/templates/user/forgot-password-change.html +++ /dev/null @@ -1,87 +0,0 @@ - - - - - Forgot Password - - - - -
-
-
-
-
-

reset

-
- - -
-
- -
-
- - -
-

- - -
- - -
-
- -
-
-
-
-
-
- - - -
- - - diff --git a/src/main/resources/templates/user/forgot-password-pending-verification.html b/src/main/resources/templates/user/forgot-password-pending-verification.html deleted file mode 100644 index 4aa0a38..0000000 --- a/src/main/resources/templates/user/forgot-password-pending-verification.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - Forgot Password Pending Verification - - - - -
-
-
-

-
-
- -
-
-
-
-
- - - diff --git a/src/main/resources/templates/user/forgot-password.html b/src/main/resources/templates/user/forgot-password.html deleted file mode 100644 index c8ba803..0000000 --- a/src/main/resources/templates/user/forgot-password.html +++ /dev/null @@ -1,80 +0,0 @@ - - - - - Forgot Password - - - - -
-
-
-
-
-

form

-
- - - -
- -
- - - - -
-
- -
-
- login -
-
-
-
- - - -
- - - diff --git a/src/main/resources/templates/user/login.html b/src/main/resources/templates/user/login.html deleted file mode 100644 index 6c863d8..0000000 --- a/src/main/resources/templates/user/login.html +++ /dev/null @@ -1,142 +0,0 @@ - - - - - Login - - - -
-
-
-
-
-
- -
-
Log in with
-
- -
- -
-
- -
-
- -
-
- -
-

Need to create an account? - Register -
- Log in not working? Forgot Password -

-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/src/main/resources/templates/user/register.html b/src/main/resources/templates/user/register.html deleted file mode 100644 index 443be0e..0000000 --- a/src/main/resources/templates/user/register.html +++ /dev/null @@ -1,143 +0,0 @@ - - - - - Register - - - - -
- - -
- -
-
-
-
-
-
Register with
-
- -
- -
-
- - - -
-
- -
-
- -
-
- -
-
- -
-
- - -
-
- -
-

Already have an account? login

-
-
-
-
-
-
-
-
- - diff --git a/src/main/resources/templates/user/registration-complete.html b/src/main/resources/templates/user/registration-complete.html deleted file mode 100644 index eadb3eb..0000000 --- a/src/main/resources/templates/user/registration-complete.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - Registration Complete! - - - - -
-
-
-

-
-
- -
-
-
-
- -
- - - diff --git a/src/main/resources/templates/user/registration-pending-verification.html b/src/main/resources/templates/user/registration-pending-verification.html deleted file mode 100644 index 39c4107..0000000 --- a/src/main/resources/templates/user/registration-pending-verification.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - Registration Pending Verification - - - - -
-
-
-

-
-
-

-
-
-
-
- -
- - - diff --git a/src/main/resources/templates/user/request-new-verification-email.html b/src/main/resources/templates/user/request-new-verification-email.html deleted file mode 100644 index 2d848e5..0000000 --- a/src/main/resources/templates/user/request-new-verification-email.html +++ /dev/null @@ -1,98 +0,0 @@ - - - - - Send New Verification Email - - - - -
-
-
-
-
-

form

-
-

- - -

- - - -
- -
- - - - -
-
- -
-
- login -
-
-
-
- - - -
- - - diff --git a/src/main/resources/templates/user/update-password.html b/src/main/resources/templates/user/update-password.html deleted file mode 100644 index 4018406..0000000 --- a/src/main/resources/templates/user/update-password.html +++ /dev/null @@ -1,97 +0,0 @@ - - - - - Update Password - - - - -
- -
-
-
-
-

reset

-
- - -
-
- -
-
- - -

- - - -

- - - - -
-
- -
-
-
-
-
-
- - - -
- - - diff --git a/src/main/resources/templates/user/update-user.html b/src/main/resources/templates/user/update-user.html deleted file mode 100644 index 7583266..0000000 --- a/src/main/resources/templates/user/update-user.html +++ /dev/null @@ -1,91 +0,0 @@ - - - - - Update Profile - - - - -
-
-
-
-
-

form

-
-
- -
-
-
- - - - -
-
- - - - -
-
- -
-
-
-

- - -
-
- - -
- - - diff --git a/src/test/java/com/digitalsanctuary/spring/user/UserApplicationTests.java b/src/test/java/com/digitalsanctuary/spring/user/UserApplicationTests.java index 616877d..f9a3a71 100644 --- a/src/test/java/com/digitalsanctuary/spring/user/UserApplicationTests.java +++ b/src/test/java/com/digitalsanctuary/spring/user/UserApplicationTests.java @@ -1,8 +1,10 @@ package com.digitalsanctuary.spring.user; +import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest +@SpringBootApplication class UserApplicationTests { } diff --git a/src/test/java/com/digitalsanctuary/spring/user/api/UserApiTest.java b/src/test/java/com/digitalsanctuary/spring/user/api/UserApiTest.java index 41c764e..29cb3a4 100644 --- a/src/test/java/com/digitalsanctuary/spring/user/api/UserApiTest.java +++ b/src/test/java/com/digitalsanctuary/spring/user/api/UserApiTest.java @@ -1,18 +1,10 @@ package com.digitalsanctuary.spring.user.api; -import com.digitalsanctuary.spring.user.api.data.ApiTestData; -import com.digitalsanctuary.spring.user.api.data.DataStatus; -import com.digitalsanctuary.spring.user.api.data.Response; -import com.digitalsanctuary.spring.user.api.helper.AssertionsHelper; -import com.digitalsanctuary.spring.user.api.provider.ApiTestDeleteAccountArgumentsProvider; -import com.digitalsanctuary.spring.user.api.provider.ApiTestUpdatePasswordArgumentsProvider; -import com.digitalsanctuary.spring.user.api.provider.ApiTestUpdateUserArgumentsProvider; -import com.digitalsanctuary.spring.user.api.provider.holder.ApiTestArgumentsHolder; -import com.digitalsanctuary.spring.user.api.provider.ApiTestRegistrationArgumentsProvider; -import com.digitalsanctuary.spring.user.dto.UserDto; -import com.digitalsanctuary.spring.user.persistence.model.User; -import com.digitalsanctuary.spring.user.service.UserService; -import org.junit.jupiter.api.*; +import static com.digitalsanctuary.spring.user.api.helper.ApiTestHelper.buildUrlEncodedFormEntity; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; import org.springframework.beans.factory.annotation.Autowired; @@ -20,11 +12,16 @@ import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import com.digitalsanctuary.spring.user.api.data.ApiTestData; +import com.digitalsanctuary.spring.user.api.data.DataStatus; +import com.digitalsanctuary.spring.user.api.data.Response; +import com.digitalsanctuary.spring.user.api.helper.AssertionsHelper; +import com.digitalsanctuary.spring.user.api.provider.ApiTestRegistrationArgumentsProvider; +import com.digitalsanctuary.spring.user.api.provider.holder.ApiTestArgumentsHolder; +import com.digitalsanctuary.spring.user.dto.UserDto; import com.digitalsanctuary.spring.user.jdbc.Jdbc; - -import static com.digitalsanctuary.spring.user.api.helper.ApiTestHelper.buildUrlEncodedFormEntity; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.digitalsanctuary.spring.user.persistence.model.User; +import com.digitalsanctuary.spring.user.service.UserService; public class UserApiTest extends BaseApiTest { private static final String URL = "/user"; @@ -42,16 +39,14 @@ public static void afterAll() { /** * * @param argumentsHolder - * @throws Exception - * testing with three params: new user data, exist user data and invalid user data + * @throws Exception testing with three params: new user data, exist user data and invalid user data */ @ParameterizedTest @ArgumentsSource(ApiTestRegistrationArgumentsProvider.class) @Order(1) // correctly run separately public void registerUserAccount(ApiTestArgumentsHolder argumentsHolder) throws Exception { - ResultActions action = perform(MockMvcRequestBuilders.post(URL + "/registration") - .contentType(MediaType.APPLICATION_FORM_URLENCODED) + ResultActions action = perform(MockMvcRequestBuilders.post(URL + "/registration").contentType(MediaType.APPLICATION_FORM_URLENCODED) .content(buildUrlEncodedFormEntity(argumentsHolder.getUserDto()))); if (argumentsHolder.getStatus() == DataStatus.NEW) { @@ -72,71 +67,15 @@ public void registerUserAccount(ApiTestArgumentsHolder argumentsHolder) throws E @Test @Order(2) public void resetPassword() throws Exception { - ResultActions action = perform(MockMvcRequestBuilders.post(URL + "/resetPassword") - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .content(buildUrlEncodedFormEntity(baseTestUser))) - .andExpect(status().isOk()); + ResultActions action = perform(MockMvcRequestBuilders.post(URL + "/resetPassword").contentType(MediaType.APPLICATION_FORM_URLENCODED) + .content(buildUrlEncodedFormEntity(baseTestUser))).andExpect(status().isOk()); MockHttpServletResponse actual = action.andReturn().getResponse(); Response excepted = ApiTestData.resetPassword(); AssertionsHelper.compareResponses(actual, excepted); } - @ParameterizedTest - @ArgumentsSource(ApiTestUpdateUserArgumentsProvider.class) - @Order(3) - public void updateUser(ApiTestArgumentsHolder argumentsHolder) throws Exception { - if(argumentsHolder.getStatus() == DataStatus.LOGGED) { - login(argumentsHolder.getUserDto()); - } - - ResultActions action = perform(MockMvcRequestBuilders.post(URL + "/updateUser") - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .content(buildUrlEncodedFormEntity(argumentsHolder.getUserDto()))) - .andExpect(status().isOk()); - MockHttpServletResponse actual = action.andReturn().getResponse(); - Response expected = argumentsHolder.getResponse(); - AssertionsHelper.compareResponses(actual, expected); - } - - @ParameterizedTest - @ArgumentsSource(ApiTestUpdatePasswordArgumentsProvider.class) - @Order(4) - public void updatePassword(ApiTestArgumentsHolder argumentsHolder) throws Exception { - login(baseTestUser); - ResultActions action = perform(MockMvcRequestBuilders.post(URL + "/updatePassword") - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .content(buildUrlEncodedFormEntity(argumentsHolder.getPasswordDto()))); - if (argumentsHolder.getStatus() == DataStatus.VALID) { - action.andExpect(status().isOk()); - } else { - action.andExpect(status().is4xxClientError()); - } - - MockHttpServletResponse actual = action.andReturn().getResponse(); - Response expected = argumentsHolder.getResponse(); - AssertionsHelper.compareResponses(actual, expected); - } - - @ParameterizedTest - @ArgumentsSource(ApiTestDeleteAccountArgumentsProvider.class) - @Order(5) - public void deleteAccount(ApiTestArgumentsHolder argumentsHolder) throws Exception { - if (argumentsHolder.getStatus() == DataStatus.LOGGED) { - login(baseTestUser); - } else { - Jdbc.deleteTestUser(baseTestUser); - } - - ResultActions action = perform(delete(URL + "/deleteAccount")); - - MockHttpServletResponse actual = action.andReturn().getResponse(); - Response expected = argumentsHolder.getResponse(); - AssertionsHelper.compareResponses(actual, expected); - - - } protected void login(UserDto userDto) { User user; diff --git a/src/test/java/com/digitalsanctuary/spring/user/config/TestConfig.java b/src/test/java/com/digitalsanctuary/spring/user/config/TestConfig.java index 3880088..afa0150 100644 --- a/src/test/java/com/digitalsanctuary/spring/user/config/TestConfig.java +++ b/src/test/java/com/digitalsanctuary/spring/user/config/TestConfig.java @@ -4,10 +4,7 @@ import org.springframework.context.annotation.ComponentScan; @TestConfiguration -@ComponentScan(basePackages = { - "com.digitalsanctuary.spring.user.service.**", - "com.digitalsanctuary.spring.user.mail.**", - "com.digitalsanctuary.spring.user.persistence.model.**" -}) +@ComponentScan(basePackages = {"com.digitalsanctuary.spring.user.service.**", "com.digitalsanctuary.spring.user.roles.**", + "com.digitalsanctuary.spring.user.mail.**", "com.digitalsanctuary.spring.user.persistence.model.**"}) public class TestConfig { } diff --git a/src/test/java/com/digitalsanctuary/spring/user/jdbc/Jdbc.java b/src/test/java/com/digitalsanctuary/spring/user/jdbc/Jdbc.java index 1b8c6f8..5773bc8 100644 --- a/src/test/java/com/digitalsanctuary/spring/user/jdbc/Jdbc.java +++ b/src/test/java/com/digitalsanctuary/spring/user/jdbc/Jdbc.java @@ -6,25 +6,27 @@ import java.sql.ResultSet; import java.sql.SQLException; -import static com.digitalsanctuary.spring.user.ui.data.UiTestData.TEST_USER_ENCODED_PASSWORD; /** * Using for delete/save user test data */ public class Jdbc { - private static final String DELETE_VERIFICATION_TOKEN_QUERY = "DELETE FROM verification_token WHERE user_id = (SELECT id FROM user_account WHERE email = ?)"; + private static final String DELETE_VERIFICATION_TOKEN_QUERY = + "DELETE FROM verification_token WHERE user_id = (SELECT id FROM user_account WHERE email = ?)"; private static final String DELETE_TEST_USER_ROLE = "DELETE FROM users_roles WHERE user_id = (SELECT id FROM user_account WHERE email = ?)"; - private static final String DELETE_TEST_PASSWORD_RESET_TOKEN = "DELETE FROM password_reset_token WHERE user_id = (SELECT id FROM user_account WHERE email = ?)"; + private static final String DELETE_TEST_PASSWORD_RESET_TOKEN = + "DELETE FROM password_reset_token WHERE user_id = (SELECT id FROM user_account WHERE email = ?)"; private static final String DELETE_TEST_USER_QUERY = "DELETE FROM user_account WHERE email = ?"; private static final String GET_LAST_USER_ID_QUERY = "SELECT max(id) FROM user_account"; - private static final String GET_PASSWORD_RESET_TOKEN_BY_USER_EMAIL_QUERY = "SELECT token FROM password_reset_token WHERE user_id = (SELECT id FROM user_account WHERE email = ?)"; + private static final String GET_PASSWORD_RESET_TOKEN_BY_USER_EMAIL_QUERY = + "SELECT token FROM password_reset_token WHERE user_id = (SELECT id FROM user_account WHERE email = ?)"; - private static final String SAVE_TEST_USER_QUERY = "INSERT INTO user_account (id, first_name, last_name, email, " + - "password, enabled, failed_login_attempts, locked) VALUES (?,?,?,?,?,?,?,?)"; + private static final String SAVE_TEST_USER_QUERY = "INSERT INTO user_account (id, first_name, last_name, email, " + + "password, enabled, failed_login_attempts, locked) VALUES (?,?,?,?,?,?,?,?)"; public static void deleteTestUser(UserDto userDto) { - try(Connection connection = ConnectionManager.open()) { - String[] params = new String[]{userDto.getEmail()}; + try (Connection connection = ConnectionManager.open()) { + String[] params = new String[] {userDto.getEmail()}; execute(connection, DELETE_VERIFICATION_TOKEN_QUERY, params); execute(connection, DELETE_TEST_USER_ROLE, params); execute(connection, DELETE_TEST_PASSWORD_RESET_TOKEN, params); @@ -36,14 +38,14 @@ public static void deleteTestUser(UserDto userDto) { public static void saveTestUser(UserDto userDto) { - try(Connection connection = ConnectionManager.open()) { + try (Connection connection = ConnectionManager.open()) { ResultSet resultSet = connection.prepareStatement(GET_LAST_USER_ID_QUERY).executeQuery(); int id = 0; if (resultSet.next()) { id = (resultSet.getInt(1) + 1); } - Object[] params = new Object[]{id, userDto.getFirstName(), userDto.getLastName(), - userDto.getEmail(), TEST_USER_ENCODED_PASSWORD, true, 0, false}; + Object[] params = new Object[] {id, userDto.getFirstName(), userDto.getLastName(), userDto.getEmail(), "TEST_USER_ENCODED_PASSWORD", true, + 0, false}; execute(connection, SAVE_TEST_USER_QUERY, params); } catch (SQLException e) { throw new RuntimeException(e); @@ -52,7 +54,7 @@ public static void saveTestUser(UserDto userDto) { private static void execute(Connection connection, String query, Object[] params) throws SQLException { PreparedStatement statement = connection.prepareStatement(query); - for(int i = 0; i < params.length; i ++) { + for (int i = 0; i < params.length; i++) { Object param = params[i]; if (param instanceof Integer) { statement.setInt((i + 1), (Integer) param); diff --git a/src/test/java/com/digitalsanctuary/spring/user/service/LoginAttemptServiceTest.java b/src/test/java/com/digitalsanctuary/spring/user/service/LoginAttemptServiceTest.java index 07c0a6b..c90c82a 100644 --- a/src/test/java/com/digitalsanctuary/spring/user/service/LoginAttemptServiceTest.java +++ b/src/test/java/com/digitalsanctuary/spring/user/service/LoginAttemptServiceTest.java @@ -41,7 +41,7 @@ void setUp() { // Manually construct the service with mocked dependencies loginAttemptService = new LoginAttemptService(userRepository); - loginAttemptService.setFailedLoginAttempts(failedLoginAttempts); + loginAttemptService.setMaxFailedLoginAttempts(failedLoginAttempts); loginAttemptService.setAccountLockoutDuration(accountLockoutDuration); } diff --git a/src/test/java/com/digitalsanctuary/spring/user/service/UserVerificationServiceTest.java b/src/test/java/com/digitalsanctuary/spring/user/service/UserVerificationServiceTest.java index b8496b7..adefb74 100644 --- a/src/test/java/com/digitalsanctuary/spring/user/service/UserVerificationServiceTest.java +++ b/src/test/java/com/digitalsanctuary/spring/user/service/UserVerificationServiceTest.java @@ -40,10 +40,7 @@ void setUp() { testToken = new VerificationToken(); testToken.setUser(testUser); - userVerificationService = new UserVerificationService( - userRepository, - verificationTokenRepository - ); + userVerificationService = new UserVerificationService(userRepository, verificationTokenRepository); } @Test @@ -61,13 +58,13 @@ void validateVerificationToken_returnsValidIfTokenValid() { Assertions.assertEquals(result, UserService.TokenValidationResult.VALID); } - @Test - void validateVerificationToken_returnsExpiredIfTokenExpired() { - testToken.setExpiryDate(getExpirationDate(0)); - when(verificationTokenRepository.findByToken(anyString())).thenReturn(testToken); - UserService.TokenValidationResult result = userVerificationService.validateVerificationToken(anyString()); - Assertions.assertEquals(result, UserService.TokenValidationResult.EXPIRED); - } + // @Test + // void validateVerificationToken_returnsExpiredIfTokenExpired() { + // testToken.setExpiryDate(getExpirationDate(0)); + // when(verificationTokenRepository.findByToken(anyString())).thenReturn(testToken); + // UserService.TokenValidationResult result = userVerificationService.validateVerificationToken(anyString()); + // Assertions.assertEquals(result, UserService.TokenValidationResult.EXPIRED); + // } @Test void validateVerificationToken_returnInvalidTokenIfTokenNotFound() { diff --git a/src/test/java/com/digitalsanctuary/spring/user/ui/BaseUiTest.java b/src/test/java/com/digitalsanctuary/spring/user/ui/BaseUiTest.java deleted file mode 100644 index d846e68..0000000 --- a/src/test/java/com/digitalsanctuary/spring/user/ui/BaseUiTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.digitalsanctuary.spring.user.ui; - -import com.codeborne.selenide.Configuration; -import com.codeborne.selenide.Selenide; -import io.github.bonigarcia.wdm.WebDriverManager; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; - -public abstract class BaseUiTest { - private Driver driver; - - public enum Driver { - CHROME("chrome"), - OPERA("opera"), - FIREFOX("firefox"), - EDGE("edge"); - - private final String browser; - - Driver(String browser) { - this.browser = browser; - } - } - - public void setUp() { - switch (this.driver) { - case CHROME -> WebDriverManager.chromedriver().setup(); - case OPERA -> WebDriverManager.operadriver().setup(); - case FIREFOX -> WebDriverManager.firefoxdriver().setup(); - case EDGE -> WebDriverManager.edgedriver().setup(); - } - Configuration.browser = driver.browser; - Configuration.browserSize = "2560x1440"; - Configuration.webdriverLogsEnabled = true; - Configuration.headless = false; - } - - @BeforeEach - public void init() { - setUp(); - } - - @AfterEach - public void tearDown() { - Selenide.closeWebDriver(); - } - - void setDriver(Driver driver) { - this.driver = driver; - } - -} diff --git a/src/test/java/com/digitalsanctuary/spring/user/ui/SpringUserFrameworkUiTest.java b/src/test/java/com/digitalsanctuary/spring/user/ui/SpringUserFrameworkUiTest.java deleted file mode 100644 index da0f5c1..0000000 --- a/src/test/java/com/digitalsanctuary/spring/user/ui/SpringUserFrameworkUiTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.digitalsanctuary.spring.user.ui; - - -import com.digitalsanctuary.spring.user.dto.UserDto; -import com.digitalsanctuary.spring.user.ui.page.*; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import com.digitalsanctuary.spring.user.ui.data.UiTestData; -import com.digitalsanctuary.spring.user.jdbc.Jdbc; - -import static com.digitalsanctuary.spring.user.ui.data.UiTestData.*; - -public class SpringUserFrameworkUiTest extends BaseUiTest { - - private static final String URI = "http://localhost:8080/"; - - private static final UserDto testUser = UiTestData.getUserDto(); - - { - super.setDriver(Driver.CHROME); - } - - @AfterEach - public void deleteTestUser() { - Jdbc.deleteTestUser(testUser); - } - - @Test - public void successSignUp() { - SuccessRegisterPage registerPage = new RegisterPage(URI + "user/register.html") - .signUp(testUser.getFirstName(), testUser.getLastName(), testUser.getEmail(), - testUser.getPassword(), testUser.getMatchingPassword()); - String actualMessage = registerPage.message(); - Assertions.assertEquals(SUCCESS_SING_UP_MESSAGE, actualMessage); - } - - @Test - public void userAlreadyExistSignUp() { - Jdbc.saveTestUser(testUser); - RegisterPage registerPage = new RegisterPage(URI + "user/register.html"); - registerPage.signUp(testUser.getFirstName(), testUser.getLastName(), testUser.getEmail(), - testUser.getPassword(), testUser.getMatchingPassword()); - String actualMessage = registerPage.accountExistErrorMessage(); - Assertions.assertEquals(ACCOUNT_EXIST_ERROR_MESSAGE, actualMessage); - } - - /** - * checks that welcome message in success login page contains username - */ - @Test - public void successSignIn() { - Jdbc.saveTestUser(testUser); - LoginPage loginPage = new LoginPage(URI + "user/login.html"); - LoginSuccessPage loginSuccessPage = loginPage.signIn(testUser.getEmail(), testUser.getPassword()); - String welcomeMessage = loginSuccessPage.welcomeMessage(); - String firstName = testUser.getFirstName(); - Assertions.assertTrue(welcomeMessage.contains(firstName)); - } - - @Test - public void successResetPassword() { - Jdbc.saveTestUser(testUser); - ForgotPasswordPage forgotPasswordPage = new ForgotPasswordPage(URI + "user/forgot-password.html"); - SuccessResetPasswordPage successResetPasswordPage = forgotPasswordPage - .fillEmail(testUser.getEmail()) - .clickSubmitBtn(); - String actualMessage = successResetPasswordPage.message(); - Assertions.assertEquals(SUCCESS_RESET_PASSWORD_MESSAGE, actualMessage); - - } -} diff --git a/src/test/java/com/digitalsanctuary/spring/user/ui/data/UiTestData.java b/src/test/java/com/digitalsanctuary/spring/user/ui/data/UiTestData.java deleted file mode 100644 index 54d8aff..0000000 --- a/src/test/java/com/digitalsanctuary/spring/user/ui/data/UiTestData.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.digitalsanctuary.spring.user.ui.data; - -import com.digitalsanctuary.spring.user.dto.UserDto; -public class UiTestData { - - public static final String ACCOUNT_EXIST_ERROR_MESSAGE = "An account for that username/email already exists. " + - "Please enter a different email."; - - public static final String SUCCESS_SING_UP_MESSAGE = "Thank you for registering!"; - - public static final String SUCCESS_RESET_PASSWORD_MESSAGE = "You should receive a password reset email shortly"; - - public static final String TEST_USER_ENCODED_PASSWORD = "$2y$10$XIRn/npMMCGt21gpU6QAbeOAUSxj/C7A793YZe.a6AEvL0LhQwkqW"; - - public static UserDto getUserDto() { - UserDto userDto = new UserDto(); - userDto.setFirstName("testUiUser"); - userDto.setLastName("userUiTest"); - userDto.setEmail("testUiUser@bk.com"); - userDto.setPassword("testUiUserPassword"); - userDto.setMatchingPassword(userDto.getPassword()); - return userDto; - } -} diff --git a/src/test/java/com/digitalsanctuary/spring/user/ui/page/ForgotPasswordPage.java b/src/test/java/com/digitalsanctuary/spring/user/ui/page/ForgotPasswordPage.java deleted file mode 100644 index d2e2268..0000000 --- a/src/test/java/com/digitalsanctuary/spring/user/ui/page/ForgotPasswordPage.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.digitalsanctuary.spring.user.ui.page; - -import com.codeborne.selenide.Selenide; -import com.codeborne.selenide.SelenideElement; -import com.digitalsanctuary.spring.user.ui.BaseUiTest; -import org.openqa.selenium.By; - -import static com.codeborne.selenide.Selenide.$; -import static com.codeborne.selenide.Selenide.$x; - -public class ForgotPasswordPage extends BaseUiTest { - private final SelenideElement EMAIL_FIELD = $(By.id("email")); - private final SelenideElement SUBMIT_BTN = $x("//button"); - - public ForgotPasswordPage(String url) { - Selenide.open(url); - } - - public ForgotPasswordPage fillEmail(String email) { - EMAIL_FIELD.setValue(email); - return this; - } - - public SuccessResetPasswordPage clickSubmitBtn() { - SUBMIT_BTN.click(); - return new SuccessResetPasswordPage(); - } - -} diff --git a/src/test/java/com/digitalsanctuary/spring/user/ui/page/LoginPage.java b/src/test/java/com/digitalsanctuary/spring/user/ui/page/LoginPage.java deleted file mode 100644 index 40f711a..0000000 --- a/src/test/java/com/digitalsanctuary/spring/user/ui/page/LoginPage.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.digitalsanctuary.spring.user.ui.page; - -import com.codeborne.selenide.Selenide; -import com.codeborne.selenide.SelenideElement; - -public class LoginPage { - - private final SelenideElement EMAIL_FIELD = Selenide.$x("//input[@id='username']"); - - private final SelenideElement PASSWORD_FIELD = Selenide.$x("//input[@id='password']"); - - private final SelenideElement LOGIN_BUTTON = Selenide.$x("//button[@id='loginButton']"); - - public LoginPage(String url) { - Selenide.open(url); - } - - public LoginSuccessPage signIn(String email, String password) { - EMAIL_FIELD.setValue(email); - PASSWORD_FIELD.setValue(password); - LOGIN_BUTTON.click(); - return new LoginSuccessPage(); - } -} diff --git a/src/test/java/com/digitalsanctuary/spring/user/ui/page/LoginSuccessPage.java b/src/test/java/com/digitalsanctuary/spring/user/ui/page/LoginSuccessPage.java deleted file mode 100644 index 88fd484..0000000 --- a/src/test/java/com/digitalsanctuary/spring/user/ui/page/LoginSuccessPage.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.digitalsanctuary.spring.user.ui.page; - -import com.codeborne.selenide.ElementsCollection; -import com.codeborne.selenide.Selenide; - -public class LoginSuccessPage { - - private final ElementsCollection WELCOME_MESSAGE = Selenide.$$x("//section//div//span"); - - public String welcomeMessage() { - return String.format("%s %s", WELCOME_MESSAGE.get(0).text(), WELCOME_MESSAGE.get(1).text()); - } -} diff --git a/src/test/java/com/digitalsanctuary/spring/user/ui/page/RegisterPage.java b/src/test/java/com/digitalsanctuary/spring/user/ui/page/RegisterPage.java deleted file mode 100644 index f382a50..0000000 --- a/src/test/java/com/digitalsanctuary/spring/user/ui/page/RegisterPage.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.digitalsanctuary.spring.user.ui.page; - -import com.codeborne.selenide.Selenide; -import com.codeborne.selenide.SelenideElement; - -/** - * Register page - */ -public class RegisterPage { - - private final SelenideElement FIRST_NAME_FIELD = Selenide.$x("//input[@id='firstName']"); - - private final SelenideElement LAST_NAME_FIELD = Selenide.$x("//input[@id='lastName']"); - - private final SelenideElement EMAIL_FIELD = Selenide.$x("//input[@id='email']"); - - private final SelenideElement PASSWORD_FIELD = Selenide.$x("//input[@id='password']"); - - private final SelenideElement CONFIRM_PASSWORD_FIELD = Selenide.$x("//input[@id='matchPassword']"); - - private final SelenideElement SIGN_UP_BUTTON = Selenide.$x("//button[@id='signUpButton']"); - - private final SelenideElement ACCOUNT_EXIST_ERROR_MESSAGE = Selenide.$x("//div[@id='existingAccountError']//span"); - - public RegisterPage(String url) { - Selenide.open(url); - } - /** - * Filling register form and click signUp button - */ - public SuccessRegisterPage signUp(String firstName, String lastName, String email, String password, String confirmPassword) { - FIRST_NAME_FIELD.setValue(firstName); - LAST_NAME_FIELD.setValue(lastName); - EMAIL_FIELD.setValue(email); - PASSWORD_FIELD.setValue(password); - CONFIRM_PASSWORD_FIELD.setValue(confirmPassword); - SIGN_UP_BUTTON.click(); - return new SuccessRegisterPage(); - } - - public String accountExistErrorMessage() { - return ACCOUNT_EXIST_ERROR_MESSAGE.text(); - } - -} diff --git a/src/test/java/com/digitalsanctuary/spring/user/ui/page/SuccessRegisterPage.java b/src/test/java/com/digitalsanctuary/spring/user/ui/page/SuccessRegisterPage.java deleted file mode 100644 index c466011..0000000 --- a/src/test/java/com/digitalsanctuary/spring/user/ui/page/SuccessRegisterPage.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.digitalsanctuary.spring.user.ui.page; - -import com.codeborne.selenide.SelenideElement; - -import static com.codeborne.selenide.Selenide.$x; - -public class SuccessRegisterPage { - private final SelenideElement SUCCESS_MESSAGE = $x("//section//div//h1"); - - public String message() { - return SUCCESS_MESSAGE.text(); - } -} diff --git a/src/test/java/com/digitalsanctuary/spring/user/ui/page/SuccessResetPasswordPage.java b/src/test/java/com/digitalsanctuary/spring/user/ui/page/SuccessResetPasswordPage.java deleted file mode 100644 index 3019db8..0000000 --- a/src/test/java/com/digitalsanctuary/spring/user/ui/page/SuccessResetPasswordPage.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.digitalsanctuary.spring.user.ui.page; - -import com.codeborne.selenide.Selenide; -import com.codeborne.selenide.SelenideElement; -import com.digitalsanctuary.spring.user.ui.BaseUiTest; - -public class SuccessResetPasswordPage extends BaseUiTest { - private final SelenideElement SUCCESS_RESET_MESSAGE = Selenide.$x("//div[@class='container']//div[@class='container']//span"); - - public String message() { - return SUCCESS_RESET_MESSAGE.text(); - } -} diff --git a/src/main/resources/application.yml b/src/test/resources/application-test.yml similarity index 100% rename from src/main/resources/application.yml rename to src/test/resources/application-test.yml 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}