Compare commits

..
26 Commits
Author SHA1 Message Date
auto 1a5be5021b Add JSON response handling for form activity check
Refactor `FormActiveController` to return JSON responses using `BooleanResponse`. Introduce exception handling for JSON processing errors and update `FormActiveData` to allow object initialization without parameters.
2024-08-10 03:10:02 +02:00
auto a9d4377599 Ensure NOT NULL constraints in SQL tables
Refactor SQL table creation scripts to add NOT NULL constraints where necessary. This change ensures data integrity by preventing null values in critical columns across the 'verify_form', 'form', 'rate_limit', and 'form_active' tables.
2024-08-10 02:32:47 +02:00
auto af44532d26 Add form activity checking and global rate limiting
Implemented new features to track form activity and enforce global rate limits. Added a `form_active` table and created endpoints to check form activity. Also, introduced a rate-limiting filter to restrict API requests to 30 per minute per IP.
2024-08-10 02:28:18 +02:00
auto fedb80f3c2 Refactor form request handling and add rate limiting.
Consolidate email verification logic into FormRequestHandler to simplify code maintenance. Implement a new rate limiting feature to restrict form submissions based on IP and email address, improving server security and performance.
2024-08-10 01:52:34 +02:00
auto f0c84e809f Add rate limiting functionality
Introduces a new 'rate_limit' table to track request counts by IP and email. Adds `RateLimitQuery` class for querying and inserting rate limits, and `RateLimitEntryDTO` for passing rate limit data.
2024-08-10 00:50:53 +02:00
auto 4f3db5ae8b workspace.xml update 2024-08-10 00:36:00 +02:00
auto c9fc81cfca Add StaffAppFormData handling in FormQuery
Imported StaffAppFormData and updated deserialization logic to include it as a case in the FormQuery class. This allows the processing of staff application forms when parsing form data from JSON.
2024-08-10 00:35:42 +02:00
auto 5e564fe9a7 Remove restriction on dot character in Discord name validation
This change allows Discord names to include dots anywhere within the name, increasing flexibility for valid usernames. The previous regex pattern incorrectly restricted dots, affecting valid user entries.
2024-08-10 00:35:29 +02:00
auto 9e0a33e782 Correct regex pattern for case-insensitive match
Updated the regex pattern in `StaffAppFormData.java` to ensure that the "yes" or "no" answers are case-insensitive. This improves the form validation to accept "Yes", "YES", "No", or "NO" without errors.
2024-08-07 21:34:25 +02:00
auto 514aaeb367 Refactor packages and add exception handler.
Renamed various classes to follow the "controlers" package structure for better organization and consistency. Added `ControllerExceptionHandler` to manage validation exceptions globally and improve error logging.
2024-08-07 21:34:03 +02:00
auto caabe7b923 Refactor form submission to use dynamic Discord URLs and emails
Updated form classes to return Optional URLs for Discord bot submissions. Refactored VerifyController to handle these Optionals and improved error handling when sending forms. Added receiver email method in form classes for more flexible form submissions.
2024-08-07 00:36:22 +02:00
auto f972436717 Add getDiscordBotUrl method to form classes
Implemented getDiscordBotUrl in form classes for dynamic URL handling. Updated VerifyController to use this method for constructing Discord bot URIs. This enhances flexibility and maintainability in form submission handling.
2024-08-07 00:01:15 +02:00
auto 2ad194fab2 Update Jackson config and refactor JSON handling
Introduced Jackson dependencies to replace Gson for JSON processing. Updated application properties and controllers to handle Jackson-specific exceptions. Refactored form serialization to use Jackson's `ObjectMapper` for better date handling and consistency.
2024-08-06 23:58:15 +02:00
auto 2c3cb48667 Add REST annotations and refactor properties file handling
Introduced `@RestController` and `@RequestMapping` in `StaffAppController` for standardized API endpoints. Refactored properties file handling in `PropertiesLoader` and `PropertiesWriter` to simplify file creation logic.
2024-08-06 23:17:06 +02:00
auto 04088c20e5 Update CORS allowed origins configuration
Consolidated the allowed origins into a single `allowedOrigins` call. This ensures both local and production environments are correctly configured for CORS.
2024-08-06 23:16:38 +02:00
auto 34255c93c8 Add REST annotations to StaffAppController
Introduce `@RestController` and `@RequestMapping` annotations to the `StaffAppController` class. This change standardizes the API endpoint and ensures all methods within this class are mapped correctly under `/api/apply`.
2024-08-06 22:23:04 +02:00
auto 01deea380a Add Staff Application Form handling
Introduced `StaffAppController` and `StaffAppFormData` to handle staff application form submissions. The controller now stores user data, initiates email verification, and provides appropriate responses based on the verification outcome. Additionally, updated `application.properties` and cleaned up IntelliJ workspace.xml.
2024-08-06 22:21:43 +02:00
auto d53ba82e3e Refactor HTML generation in ContactFormData
Extract the HTML generation logic to a reusable method in the Form class. This change reduces code duplication and enhances maintainability by centralizing the table generation functionality.
2024-08-06 22:21:30 +02:00
auto ac5448385d Rename StoreFormQuery package to forms.form
Updated the package name for consistency and better categorization. Adjusted imports in related test files to reflect the new package structure.
2024-08-06 22:21:07 +02:00
auto d8ab42b09c Update URI to point to Discord bot endpoint
Changed the URI in VerifyController to use the Discord bot's endpoint for form submission. This adjustment is part of the process to route form data correctly through the specified internal service.
2024-08-05 01:08:51 +02:00
auto 14f02e535f Enhance logging for mail verification details
Updated logging to provide more detailed information about user credentials and email actions. This helps in debugging and monitoring mail verification processes more effectively. Adjusted trace messages to include username and password for better traceability.
2024-08-05 01:03:12 +02:00
auto cbb5dd99d8 Refactor logging for better granularity
Updated logging levels in multiple classes to differentiate between debug and trace information. Improved log messages to support parameterized logs, ensuring sensitive or variable information is handled appropriately without concatenation.
2024-08-05 00:45:48 +02:00
auto f68a96c089 Add CORS support for production server
Updated the CORS configuration to include the production server URL. This change ensures that requests from the production environment are properly handled.
2024-08-05 00:23:06 +02:00
auto 8cb22aae08 Configure system property for conditional test execution
Added a system property 'doNotRunDatabaseTests' in the test configuration to allow conditional skipping of database tests. This helps in running tests selectively during different build scenarios.
2024-08-04 22:54:48 +02:00
auto 405c2a73bd Fix typo in Gradle build command in Jenkinsfile
Corrected the parameter for skipping database tests from `dontRunDatabaseTests` to `doNotRunDatabaseTests` in the Gradle build step. This ensures the intended tests are skipped during the build process.
2024-08-04 22:53:35 +02:00
auto cb7bb60178 Disable database tests in Jenkins
Disabled tests that require a database connection to prevent failures in Jenkins where there isn't a database. Updated Jenkinsfile to include a property that skips these tests during the build process.
2024-08-04 22:52:01 +02:00
34 changed files with 1250 additions and 178 deletions
+3
View File
@@ -40,3 +40,6 @@ bin/
### Mac OS ###
.DS_Store
*.bat
config/
+361 -52
View File
@@ -4,9 +4,11 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="ce59df2a-8d56-446a-867b-80e627daf479" name="Changes" comment="Add database configuration via command-line arguments&#10;&#10;A new configuration setup now allows specifying a path for database properties via command-line arguments during application startup. This update also changes the call signature for `DatabaseConnection.initialize()` method to accept a path argument. Similarly, methods in `PropertiesLoader`, `PropertiesWriter`, and `MailSettings` classes were also updated to use the specified path when working with properties files. The `TestForm` class's tests were updated accordingly to handle these changes.">
<change afterPath="$PROJECT_DIR$/build.sh" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<list default="true" id="ce59df2a-8d56-446a-867b-80e627daf479" name="Changes" comment="Ensure NOT NULL constraints in SQL tables&#10;&#10;Refactor SQL table creation scripts to add NOT NULL constraints where necessary. This change ensures data integrity by preventing null values in critical columns across the 'verify_form', 'form', 'rate_limit', and 'form_active' tables.">
<change afterPath="$PROJECT_DIR$/src/main/java/com/alttd/forms/controlers/form_active/BooleanResponse.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/alttd/forms/controlers/form_active/FormActiveController.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/alttd/forms/controlers/form_active/FormActiveController.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/alttd/forms/controlers/form_active/FormActiveData.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/alttd/forms/controlers/form_active/FormActiveData.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/resources/application.properties" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/resources/application.properties" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -47,13 +49,19 @@
<option name="RECENT_TEMPLATES">
<list>
<option value="Jsp File" />
<option value="Class" />
<option value="package-info" />
<option value="package.json" />
<option value="Class" />
<option value="Record" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="master" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="GitHubPullRequestSearchHistory">{
@@ -96,45 +104,58 @@
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;Gradle.Build forms.executor&quot;: &quot;Run&quot;,
&quot;Gradle.TestForm.executor&quot;: &quot;Run&quot;,
&quot;Gradle.TestForm.testFunctions.executor&quot;: &quot;Debug&quot;,
&quot;Gradle.TestForm.testRetrieveForm.executor&quot;: &quot;Run&quot;,
&quot;RequestMappingsPanelOrder0&quot;: &quot;0&quot;,
&quot;RequestMappingsPanelOrder1&quot;: &quot;1&quot;,
&quot;RequestMappingsPanelWidth0&quot;: &quot;75&quot;,
&quot;RequestMappingsPanelWidth1&quot;: &quot;75&quot;,
&quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;,
&quot;Spring Boot.Main.executor&quot;: &quot;Run&quot;,
&quot;Tomcat Server.Tomcat 10.1.17.executor&quot;: &quot;Run&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;ignore.virus.scanning.warn.message&quot;: &quot;true&quot;,
&quot;kotlin-language-version-configured&quot;: &quot;true&quot;,
&quot;last_opened_file_path&quot;: &quot;P:/Code/Plugins/forms&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;project.structure.last.edited&quot;: &quot;Project&quot;,
&quot;project.structure.proportion&quot;: &quot;0.0&quot;,
&quot;project.structure.side.proportion&quot;: &quot;0.0&quot;,
&quot;run.code.analysis.last.selected.profile&quot;: &quot;pProject Default&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;preferences.lookFeel&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"Downloaded.Files.Path.Enabled": "false",
"Gradle.Build forms.executor": "Run",
"Gradle.TestForm.executor": "Run",
"Gradle.TestForm.testFunctions.executor": "Debug",
"Gradle.TestForm.testRetrieveForm.executor": "Run",
"Repository.Attach.Annotations": "false",
"Repository.Attach.JavaDocs": "false",
"Repository.Attach.Sources": "false",
"RequestMappingsPanelOrder0": "0",
"RequestMappingsPanelOrder1": "1",
"RequestMappingsPanelWidth0": "75",
"RequestMappingsPanelWidth1": "75",
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
"Spring Boot.Main.executor": "Run",
"Tomcat Server.Tomcat 10.1.17.executor": "Run",
"git-widget-placeholder": "rate__limit",
"ignore.virus.scanning.warn.message": "true",
"kotlin-language-version-configured": "true",
"last_opened_file_path": "P:/Code/Plugins/forms",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"project.structure.last.edited": "Project",
"project.structure.proportion": "0.0",
"project.structure.side.proportion": "0.0",
"run.code.analysis.last.selected.profile": "pProject Default",
"settings.editor.selected.configurable": "preferences.lookFeel",
"vue.rearranger.settings.migration": "true"
},
&quot;keyToStringList&quot;: {
&quot;DatabaseDriversLRU&quot;: [
&quot;mysql&quot;,
&quot;mariadb&quot;
"keyToStringList": {
"DatabaseDriversLRU": [
"mysql",
"mariadb"
]
}
}</component>
<component name="RunManager" selected="Gradle.TestForm">
}]]></component>
<component name="RecentsManager">
<key name="CreateClassDialog.RecentsKey">
<recent name="com.alttd.forms.apply" />
</key>
<key name="CopyClassDialog.RECENTS_KEY">
<recent name="com.alttd.forms.controlers.form_active" />
<recent name="com.alttd.forms.form" />
</key>
</component>
<component name="RunManager" selected="Spring Boot.Main">
<configuration name="TestForm" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
<ExternalSystemSettings>
<option name="executionName" />
@@ -231,6 +252,54 @@
<predefined_log_file id="Tomcat Manager" />
<predefined_log_file id="Tomcat Host Manager" />
<predefined_log_file id="Tomcat Localhost Access" />
<RunnerSettings RunnerId="Debug">
<option name="DEBUG_PORT" value="51778" />
</RunnerSettings>
<ConfigurationWrapper VM_VAR="JAVA_OPTS" RunnerId="Cover">
<option name="USE_ENV_VARIABLES" value="true" />
<STARTUP>
<option name="USE_DEFAULT" value="true" />
<option name="SCRIPT" value="" />
<option name="VM_PARAMETERS" value="" />
<option name="PROGRAM_PARAMETERS" value="" />
</STARTUP>
<SHUTDOWN>
<option name="USE_DEFAULT" value="true" />
<option name="SCRIPT" value="" />
<option name="VM_PARAMETERS" value="" />
<option name="PROGRAM_PARAMETERS" value="" />
</SHUTDOWN>
</ConfigurationWrapper>
<ConfigurationWrapper VM_VAR="JAVA_OPTS" RunnerId="Debug">
<option name="USE_ENV_VARIABLES" value="true" />
<STARTUP>
<option name="USE_DEFAULT" value="true" />
<option name="SCRIPT" value="" />
<option name="VM_PARAMETERS" value="" />
<option name="PROGRAM_PARAMETERS" value="" />
</STARTUP>
<SHUTDOWN>
<option name="USE_DEFAULT" value="true" />
<option name="SCRIPT" value="" />
<option name="VM_PARAMETERS" value="" />
<option name="PROGRAM_PARAMETERS" value="" />
</SHUTDOWN>
</ConfigurationWrapper>
<ConfigurationWrapper VM_VAR="JAVA_OPTS" RunnerId="Profile">
<option name="USE_ENV_VARIABLES" value="true" />
<STARTUP>
<option name="USE_DEFAULT" value="true" />
<option name="SCRIPT" value="" />
<option name="VM_PARAMETERS" value="" />
<option name="PROGRAM_PARAMETERS" value="" />
</STARTUP>
<SHUTDOWN>
<option name="USE_DEFAULT" value="true" />
<option name="SCRIPT" value="" />
<option name="VM_PARAMETERS" value="" />
<option name="PROGRAM_PARAMETERS" value="" />
</SHUTDOWN>
</ConfigurationWrapper>
<ConfigurationWrapper VM_VAR="JAVA_OPTS" RunnerId="Run">
<option name="USE_ENV_VARIABLES" value="true" />
<STARTUP>
@@ -271,8 +340,8 @@
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-jdk-9f38398b9061-39b83d9b5494-intellij.indexing.shared.core-IU-241.18034.62" />
<option value="bundled-js-predefined-1d06a55b98c1-0b3e54e931b4-JavaScript-IU-241.18034.62" />
<option value="bundled-jdk-9823dce3aa75-b114ca120d71-intellij.indexing.shared.core-IU-242.20224.300" />
<option value="bundled-js-predefined-d6986cc7102b-410509235cf1-JavaScript-IU-242.20224.300" />
</set>
</attachedChunks>
</component>
@@ -309,7 +378,13 @@
<workItem from="1722697616738" duration="2649000" />
<workItem from="1722718451005" duration="2120000" />
<workItem from="1722786481232" duration="617000" />
<workItem from="1722801254695" duration="601000" />
<workItem from="1722801254695" duration="6135000" />
<workItem from="1722879625782" duration="358000" />
<workItem from="1722973564053" duration="6695000" />
<workItem from="1723057624940" duration="2895000" />
<workItem from="1723138548381" duration="1949000" />
<workItem from="1723230192640" duration="55000" />
<workItem from="1723242314809" duration="9799000" />
</task>
<task id="LOCAL-00001" summary="Initial commit for site for forms">
<option name="closed" value="true" />
@@ -359,7 +434,223 @@
<option name="project" value="LOCAL" />
<updated>1714332311321</updated>
</task>
<option name="localTasksCounter" value="7" />
<task id="LOCAL-00007" summary="Add DB config via command-line arguments&#10;&#10;A new setup allows specifying database properties path using command-line arguments during startup. The `DatabaseConnection.initialize()` method signature has been updated to accept a path argument. Methods in `PropertiesLoader`, `PropertiesWriter`, and `MailSettings` classes, along with tests in the `TestForm` class, were adjusted accordingly.">
<option name="closed" value="true" />
<created>1722804244115</created>
<option name="number" value="00007" />
<option name="presentableId" value="LOCAL-00007" />
<option name="project" value="LOCAL" />
<updated>1722804244115</updated>
</task>
<task id="LOCAL-00008" summary="Add Jenkins pipeline for Gradle build and notifications&#10;&#10;This commit introduces a new Jenkinsfile configured to build the project using Gradle and archive the resulting artifacts. Additionally, it includes a stage to send build notifications to Discord with details about the build status.">
<option name="closed" value="true" />
<created>1722804255328</created>
<option name="number" value="00008" />
<option name="presentableId" value="LOCAL-00008" />
<option name="project" value="LOCAL" />
<updated>1722804255328</updated>
</task>
<task id="LOCAL-00009" summary="Disable database tests in Jenkins&#10;&#10;Disabled tests that require a database connection to prevent failures in Jenkins where there isn't a database. Updated Jenkinsfile to include a property that skips these tests during the build process.">
<option name="closed" value="true" />
<created>1722804722354</created>
<option name="number" value="00009" />
<option name="presentableId" value="LOCAL-00009" />
<option name="project" value="LOCAL" />
<updated>1722804722354</updated>
</task>
<task id="LOCAL-00010" summary="Fix typo in Gradle build command in Jenkinsfile&#10;&#10;Corrected the parameter for skipping database tests from `dontRunDatabaseTests` to `doNotRunDatabaseTests` in the Gradle build step. This ensures the intended tests are skipped during the build process.">
<option name="closed" value="true" />
<created>1722804816532</created>
<option name="number" value="00010" />
<option name="presentableId" value="LOCAL-00010" />
<option name="project" value="LOCAL" />
<updated>1722804816532</updated>
</task>
<task id="LOCAL-00011" summary="Configure system property for conditional test execution&#10;&#10;Added a system property 'doNotRunDatabaseTests' in the test configuration to allow conditional skipping of database tests. This helps in running tests selectively during different build scenarios.">
<option name="closed" value="true" />
<created>1722804890007</created>
<option name="number" value="00011" />
<option name="presentableId" value="LOCAL-00011" />
<option name="project" value="LOCAL" />
<updated>1722804890007</updated>
</task>
<task id="LOCAL-00012" summary="Add CORS support for production server&#10;&#10;Updated the CORS configuration to include the production server URL. This change ensures that requests from the production environment are properly handled.">
<option name="closed" value="true" />
<created>1722810187257</created>
<option name="number" value="00012" />
<option name="presentableId" value="LOCAL-00012" />
<option name="project" value="LOCAL" />
<updated>1722810187257</updated>
</task>
<task id="LOCAL-00013" summary="Refactor logging for better granularity&#10;&#10;Updated logging levels in multiple classes to differentiate between debug and trace information. Improved log messages to support parameterized logs, ensuring sensitive or variable information is handled appropriately without concatenation.">
<option name="closed" value="true" />
<created>1722811549943</created>
<option name="number" value="00013" />
<option name="presentableId" value="LOCAL-00013" />
<option name="project" value="LOCAL" />
<updated>1722811549943</updated>
</task>
<task id="LOCAL-00014" summary="Enhance logging for mail verification details&#10;&#10;Updated logging to provide more detailed information about user credentials and email actions. This helps in debugging and monitoring mail verification processes more effectively. Adjusted trace messages to include username and password for better traceability.">
<option name="closed" value="true" />
<created>1722812594171</created>
<option name="number" value="00014" />
<option name="presentableId" value="LOCAL-00014" />
<option name="project" value="LOCAL" />
<updated>1722812594171</updated>
</task>
<task id="LOCAL-00015" summary="Update URI to point to Discord bot endpoint&#10;&#10;Changed the URI in VerifyController to use the Discord bot's endpoint for form submission. This adjustment is part of the process to route form data correctly through the specified internal service.">
<option name="closed" value="true" />
<created>1722812932077</created>
<option name="number" value="00015" />
<option name="presentableId" value="LOCAL-00015" />
<option name="project" value="LOCAL" />
<updated>1722812932077</updated>
</task>
<task id="LOCAL-00016" summary="Rename StoreFormQuery package to forms.form&#10;&#10;Updated the package name for consistency and better categorization. Adjusted imports in related test files to reflect the new package structure.">
<option name="closed" value="true" />
<created>1722975667414</created>
<option name="number" value="00016" />
<option name="presentableId" value="LOCAL-00016" />
<option name="project" value="LOCAL" />
<updated>1722975667414</updated>
</task>
<task id="LOCAL-00017" summary="Refactor HTML generation in ContactFormData&#10;&#10;Extract the HTML generation logic to a reusable method in the Form class. This change reduces code duplication and enhances maintainability by centralizing the table generation functionality.">
<option name="closed" value="true" />
<created>1722975690755</created>
<option name="number" value="00017" />
<option name="presentableId" value="LOCAL-00017" />
<option name="project" value="LOCAL" />
<updated>1722975690755</updated>
</task>
<task id="LOCAL-00018" summary="Add Staff Application Form handling&#10;&#10;Introduced `StaffAppController` and `StaffAppFormData` to handle staff application form submissions. The controller now stores user data, initiates email verification, and provides appropriate responses based on the verification outcome. Additionally, updated `application.properties` and cleaned up IntelliJ workspace.xml.">
<option name="closed" value="true" />
<created>1722975703502</created>
<option name="number" value="00018" />
<option name="presentableId" value="LOCAL-00018" />
<option name="project" value="LOCAL" />
<updated>1722975703502</updated>
</task>
<task id="LOCAL-00019" summary="Add REST annotations to StaffAppController&#10;&#10;Introduce `@RestController` and `@RequestMapping` annotations to the `StaffAppController` class. This change standardizes the API endpoint and ensures all methods within this class are mapped correctly under `/api/apply`.">
<option name="closed" value="true" />
<created>1722975785066</created>
<option name="number" value="00019" />
<option name="presentableId" value="LOCAL-00019" />
<option name="project" value="LOCAL" />
<updated>1722975785066</updated>
</task>
<task id="LOCAL-00020" summary="Update CORS allowed origins configuration&#10;&#10;Consolidated the allowed origins into a single `allowedOrigins` call. This ensures both local and production environments are correctly configured for CORS.">
<option name="closed" value="true" />
<created>1722978999091</created>
<option name="number" value="00020" />
<option name="presentableId" value="LOCAL-00020" />
<option name="project" value="LOCAL" />
<updated>1722978999091</updated>
</task>
<task id="LOCAL-00021" summary="Add REST annotations and refactor properties file handling&#10;&#10;Introduced `@RestController` and `@RequestMapping` in `StaffAppController` for standardized API endpoints. Refactored properties file handling in `PropertiesLoader` and `PropertiesWriter` to simplify file creation logic.">
<option name="closed" value="true" />
<created>1722979026190</created>
<option name="number" value="00021" />
<option name="presentableId" value="LOCAL-00021" />
<option name="project" value="LOCAL" />
<updated>1722979026190</updated>
</task>
<task id="LOCAL-00022" summary="Update Jackson config and refactor JSON handling&#10;&#10;Introduced Jackson dependencies to replace Gson for JSON processing. Updated application properties and controllers to handle Jackson-specific exceptions. Refactored form serialization to use Jackson's `ObjectMapper` for better date handling and consistency.">
<option name="closed" value="true" />
<created>1722981496092</created>
<option name="number" value="00022" />
<option name="presentableId" value="LOCAL-00022" />
<option name="project" value="LOCAL" />
<updated>1722981496092</updated>
</task>
<task id="LOCAL-00023" summary="Add getDiscordBotUrl method to form classes&#10;&#10;Implemented getDiscordBotUrl in form classes for dynamic URL handling. Updated VerifyController to use this method for constructing Discord bot URIs. This enhances flexibility and maintainability in form submission handling.">
<option name="closed" value="true" />
<created>1722981675331</created>
<option name="number" value="00023" />
<option name="presentableId" value="LOCAL-00023" />
<option name="project" value="LOCAL" />
<updated>1722981675331</updated>
</task>
<task id="LOCAL-00024" summary="Refactor form submission to use dynamic Discord URLs and emails&#10;&#10;Updated form classes to return Optional URLs for Discord bot submissions. Refactored VerifyController to handle these Optionals and improved error handling when sending forms. Added receiver email method in form classes for more flexible form submissions.">
<option name="closed" value="true" />
<created>1722983786557</created>
<option name="number" value="00024" />
<option name="presentableId" value="LOCAL-00024" />
<option name="project" value="LOCAL" />
<updated>1722983786557</updated>
</task>
<task id="LOCAL-00025" summary="Refactor packages and add exception handler.&#10;&#10;Renamed various classes to follow the &quot;controlers&quot; package structure for better organization and consistency. Added `ControllerExceptionHandler` to manage validation exceptions globally and improve error logging.">
<option name="closed" value="true" />
<created>1723059243750</created>
<option name="number" value="00025" />
<option name="presentableId" value="LOCAL-00025" />
<option name="project" value="LOCAL" />
<updated>1723059243750</updated>
</task>
<task id="LOCAL-00026" summary="Correct regex pattern for case-insensitive match&#10;&#10;Updated the regex pattern in `StaffAppFormData.java` to ensure that the &quot;yes&quot; or &quot;no&quot; answers are case-insensitive. This improves the form validation to accept &quot;Yes&quot;, &quot;YES&quot;, &quot;No&quot;, or &quot;NO&quot; without errors.">
<option name="closed" value="true" />
<created>1723059265558</created>
<option name="number" value="00026" />
<option name="presentableId" value="LOCAL-00026" />
<option name="project" value="LOCAL" />
<updated>1723059265558</updated>
</task>
<task id="LOCAL-00027" summary="Remove restriction on dot character in Discord name validation&#10;&#10;This change allows Discord names to include dots anywhere within the name, increasing flexibility for valid usernames. The previous regex pattern incorrectly restricted dots, affecting valid user entries.">
<option name="closed" value="true" />
<created>1723242929470</created>
<option name="number" value="00027" />
<option name="presentableId" value="LOCAL-00027" />
<option name="project" value="LOCAL" />
<updated>1723242929470</updated>
</task>
<task id="LOCAL-00028" summary="Add StaffAppFormData handling in FormQuery&#10;&#10;Imported StaffAppFormData and updated deserialization logic to include it as a case in the FormQuery class. This allows the processing of staff application forms when parsing form data from JSON.">
<option name="closed" value="true" />
<created>1723242942749</created>
<option name="number" value="00028" />
<option name="presentableId" value="LOCAL-00028" />
<option name="project" value="LOCAL" />
<updated>1723242942749</updated>
</task>
<task id="LOCAL-00029" summary="workspace.xml update">
<option name="closed" value="true" />
<created>1723242960642</created>
<option name="number" value="00029" />
<option name="presentableId" value="LOCAL-00029" />
<option name="project" value="LOCAL" />
<updated>1723242960642</updated>
</task>
<task id="LOCAL-00030" summary="Add rate limiting functionality&#10;&#10;Introduces a new 'rate_limit' table to track request counts by IP and email. Adds `RateLimitQuery` class for querying and inserting rate limits, and `RateLimitEntryDTO` for passing rate limit data.">
<option name="closed" value="true" />
<created>1723243853994</created>
<option name="number" value="00030" />
<option name="presentableId" value="LOCAL-00030" />
<option name="project" value="LOCAL" />
<updated>1723243853994</updated>
</task>
<task id="LOCAL-00031" summary="Refactor form request handling and add rate limiting.&#10;&#10;Consolidate email verification logic into FormRequestHandler to simplify code maintenance. Implement a new rate limiting feature to restrict form submissions based on IP and email address, improving server security and performance.">
<option name="closed" value="true" />
<created>1723247556185</created>
<option name="number" value="00031" />
<option name="presentableId" value="LOCAL-00031" />
<option name="project" value="LOCAL" />
<updated>1723247556185</updated>
</task>
<task id="LOCAL-00032" summary="Add form activity checking and global rate limiting&#10;&#10;Implemented new features to track form activity and enforce global rate limits. Added a `form_active` table and created endpoints to check form activity. Also, introduced a rate-limiting filter to restrict API requests to 30 per minute per IP.">
<option name="closed" value="true" />
<created>1723249700073</created>
<option name="number" value="00032" />
<option name="presentableId" value="LOCAL-00032" />
<option name="project" value="LOCAL" />
<updated>1723249700073</updated>
</task>
<task id="LOCAL-00033" summary="Ensure NOT NULL constraints in SQL tables&#10;&#10;Refactor SQL table creation scripts to add NOT NULL constraints where necessary. This change ensures data integrity by preventing null values in critical columns across the 'verify_form', 'form', 'rate_limit', and 'form_active' tables.">
<option name="closed" value="true" />
<created>1723249968746</created>
<option name="number" value="00033" />
<option name="presentableId" value="LOCAL-00033" />
<option name="project" value="LOCAL" />
<updated>1723249968746</updated>
</task>
<option name="localTasksCounter" value="34" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@@ -377,13 +668,31 @@
</option>
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="Initial commit for site for forms" />
<MESSAGE value="Refactor code to use Form objects instead of JSON strings&#10;&#10;Several parts of the code have been altered to use Form objects instead of JSON strings. Changes include updating the FormQueryResult record type to hold an Optional&lt;Form&gt; instead of an Optional&lt;String&gt;, altering methods in the StoreFormQuery class to insert Form data into the database and replacing JSON handling methods in the FormQuery class with Form object oriented methods. A 'form_class' field has also been added to the 'form' table in the database to aid form identification and reconstruction from stored data." />
<MESSAGE value="Add TestForm class and set up related unit tests&#10;&#10;The changes add a new TestForm class to implement unit tests for form handling in the application. The included tests verify the storing, retrieving, and verification of forms from a database, as well as form object creation and confirmation of form data. This is a step towards improving the code's reliability and making it easier to catch potential bugs or issues." />
<MESSAGE value="Remove unnecessary Gson imports in ContactFormData&#10;&#10;The Gson and GsonBuilder imports in the ContactFormData class were unused and have been removed. This cleanup simplifies the code and reduces unnecessary dependencies.&#10;" />
<MESSAGE value="Mark fields as final in VerificationData and ContactFormData&#10;&#10;The 'code', 'eMail' fields in the VerificationData class and 'username', 'email', 'question' fields in the ContactFormData class are now marked as 'final'. At the same time, some unused imports from ContactController, ContactFormData, FormQuery, and VerifyController have been removed for code cleanliness." />
<MESSAGE value="Update Form HTML output and disable validation auto-configuration&#10;&#10;The form-to-HTML output process has been switched from a `&lt;div&gt;` approach to using a `StringBuilder` with a table structure in `ContactFormData.java`. Also, the spring validation auto-configuration has been disabled by adding `exclude = ValidationAutoConfiguration.class` in the `@SpringBootApplication` annotation of `Main.java`. Some changes in `.idea/workspace.xml` and `VerifyController.java` were made as well." />
<MESSAGE value="Add database configuration via command-line arguments&#10;&#10;A new configuration setup now allows specifying a path for database properties via command-line arguments during application startup. This update also changes the call signature for `DatabaseConnection.initialize()` method to accept a path argument. Similarly, methods in `PropertiesLoader`, `PropertiesWriter`, and `MailSettings` classes were also updated to use the specified path when working with properties files. The `TestForm` class's tests were updated accordingly to handle these changes." />
<option name="LAST_COMMIT_MESSAGE" value="Add database configuration via command-line arguments&#10;&#10;A new configuration setup now allows specifying a path for database properties via command-line arguments during application startup. This update also changes the call signature for `DatabaseConnection.initialize()` method to accept a path argument. Similarly, methods in `PropertiesLoader`, `PropertiesWriter`, and `MailSettings` classes were also updated to use the specified path when working with properties files. The `TestForm` class's tests were updated accordingly to handle these changes." />
<MESSAGE value="Configure system property for conditional test execution&#10;&#10;Added a system property 'doNotRunDatabaseTests' in the test configuration to allow conditional skipping of database tests. This helps in running tests selectively during different build scenarios." />
<MESSAGE value="Add CORS support for production server&#10;&#10;Updated the CORS configuration to include the production server URL. This change ensures that requests from the production environment are properly handled." />
<MESSAGE value="Refactor logging for better granularity&#10;&#10;Updated logging levels in multiple classes to differentiate between debug and trace information. Improved log messages to support parameterized logs, ensuring sensitive or variable information is handled appropriately without concatenation." />
<MESSAGE value="Enhance logging for mail verification details&#10;&#10;Updated logging to provide more detailed information about user credentials and email actions. This helps in debugging and monitoring mail verification processes more effectively. Adjusted trace messages to include username and password for better traceability." />
<MESSAGE value="Update URI to point to Discord bot endpoint&#10;&#10;Changed the URI in VerifyController to use the Discord bot's endpoint for form submission. This adjustment is part of the process to route form data correctly through the specified internal service." />
<MESSAGE value="Rename StoreFormQuery package to forms.form&#10;&#10;Updated the package name for consistency and better categorization. Adjusted imports in related test files to reflect the new package structure." />
<MESSAGE value="Refactor HTML generation in ContactFormData&#10;&#10;Extract the HTML generation logic to a reusable method in the Form class. This change reduces code duplication and enhances maintainability by centralizing the table generation functionality." />
<MESSAGE value="Add Staff Application Form handling&#10;&#10;Introduced `StaffAppController` and `StaffAppFormData` to handle staff application form submissions. The controller now stores user data, initiates email verification, and provides appropriate responses based on the verification outcome. Additionally, updated `application.properties` and cleaned up IntelliJ workspace.xml." />
<MESSAGE value="Add REST annotations to StaffAppController&#10;&#10;Introduce `@RestController` and `@RequestMapping` annotations to the `StaffAppController` class. This change standardizes the API endpoint and ensures all methods within this class are mapped correctly under `/api/apply`." />
<MESSAGE value="Update CORS allowed origins configuration&#10;&#10;Consolidated the allowed origins into a single `allowedOrigins` call. This ensures both local and production environments are correctly configured for CORS." />
<MESSAGE value="Add REST annotations and refactor properties file handling&#10;&#10;Introduced `@RestController` and `@RequestMapping` in `StaffAppController` for standardized API endpoints. Refactored properties file handling in `PropertiesLoader` and `PropertiesWriter` to simplify file creation logic." />
<MESSAGE value="Update Jackson config and refactor JSON handling&#10;&#10;Introduced Jackson dependencies to replace Gson for JSON processing. Updated application properties and controllers to handle Jackson-specific exceptions. Refactored form serialization to use Jackson's `ObjectMapper` for better date handling and consistency." />
<MESSAGE value="Add getDiscordBotUrl method to form classes&#10;&#10;Implemented getDiscordBotUrl in form classes for dynamic URL handling. Updated VerifyController to use this method for constructing Discord bot URIs. This enhances flexibility and maintainability in form submission handling." />
<MESSAGE value="Refactor form submission to use dynamic Discord URLs and emails&#10;&#10;Updated form classes to return Optional URLs for Discord bot submissions. Refactored VerifyController to handle these Optionals and improved error handling when sending forms. Added receiver email method in form classes for more flexible form submissions." />
<MESSAGE value="Add validation exception handling to StaffAppController&#10;&#10;Introduced an `ExceptionHandler` for `MethodArgumentNotValidException` in `StaffAppController` to return detailed validation error messages. Updated the regex pattern in `StaffAppFormData` to be case-insensitive. This enhances error reporting and user input validation in the application." />
<MESSAGE value="Refactor packages and add exception handler.&#10;&#10;Renamed various classes to follow the &quot;controlers&quot; package structure for better organization and consistency. Added `ControllerExceptionHandler` to manage validation exceptions globally and improve error logging." />
<MESSAGE value="Correct regex pattern for case-insensitive match&#10;&#10;Updated the regex pattern in `StaffAppFormData.java` to ensure that the &quot;yes&quot; or &quot;no&quot; answers are case-insensitive. This improves the form validation to accept &quot;Yes&quot;, &quot;YES&quot;, &quot;No&quot;, or &quot;NO&quot; without errors." />
<MESSAGE value="Remove restriction on dot character in Discord name validation&#10;&#10;This change allows Discord names to include dots anywhere within the name, increasing flexibility for valid usernames. The previous regex pattern incorrectly restricted dots, affecting valid user entries." />
<MESSAGE value="Add StaffAppFormData handling in FormQuery&#10;&#10;Imported StaffAppFormData and updated deserialization logic to include it as a case in the FormQuery class. This allows the processing of staff application forms when parsing form data from JSON." />
<MESSAGE value="workspace.xml update" />
<MESSAGE value="Add rate limiting functionality&#10;&#10;Introduces a new 'rate_limit' table to track request counts by IP and email. Adds `RateLimitQuery` class for querying and inserting rate limits, and `RateLimitEntryDTO` for passing rate limit data." />
<MESSAGE value="Add IP rate limiting for form submissions&#10;&#10;Added IP rate limiting to prevent abuse in form submissions by tracking IP and email address attempts. Updated the form submission methods to include IP in email verification and added detailed HTML email content." />
<MESSAGE value="Refactor form request handling and add rate limiting.&#10;&#10;Consolidate email verification logic into FormRequestHandler to simplify code maintenance. Implement a new rate limiting feature to restrict form submissions based on IP and email address, improving server security and performance." />
<MESSAGE value="Add form activity checking and global rate limiting&#10;&#10;Implemented new features to track form activity and enforce global rate limits. Added a `form_active` table and created endpoints to check form activity. Also, introduced a rate-limiting filter to restrict API requests to 30 per minute per IP." />
<MESSAGE value="Ensure NOT NULL constraints in SQL tables&#10;&#10;Refactor SQL table creation scripts to add NOT NULL constraints where necessary. This change ensures data integrity by preventing null values in critical columns across the 'verify_form', 'form', 'rate_limit', and 'form_active' tables." />
<option name="LAST_COMMIT_MESSAGE" value="Ensure NOT NULL constraints in SQL tables&#10;&#10;Refactor SQL table creation scripts to add NOT NULL constraints where necessary. This change ensures data integrity by preventing null values in critical columns across the 'verify_form', 'form', 'rate_limit', and 'form_active' tables." />
</component>
</project>
Vendored
+1 -1
View File
@@ -3,7 +3,7 @@ pipeline {
stages {
stage('Gradle') {
steps {
sh 'bash gradlew build'
sh 'bash gradlew build -DdoNotRunDatabaseTests=true'
}
}
stage('Archive') {
+2 -1
View File
@@ -27,10 +27,11 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation group: 'javax.mail', name: 'mail', version: '1.4'
implementation 'org.springframework.boot:spring-boot-starter-logging'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'org.mariadb.jdbc:mariadb-java-client:2.1.2'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.12.3'
}
test {
systemProperty 'doNotRunDatabaseTests', System.getProperty('doNotRunDatabaseTests')
useJUnitPlatform()
}
+2
View File
@@ -1,6 +1,7 @@
package com.alttd.forms;
import com.alttd.forms.database.DatabaseConnection;
import com.alttd.forms.mail.rate_limitter.FormRateLimit;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration;
@@ -16,6 +17,7 @@ public class Main {
path = args[0];
SpringApplication.run(Main.class, args);
DatabaseConnection.initialize(args[0]);
FormRateLimit.initialize(args[0]);
}
public static String getConfigPath() {
@@ -15,7 +15,7 @@ public class WebConfig implements WebMvcConfigurer { //TODO this can be done wit
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000")
.allowedOrigins("http://localhost:3000", "https://forms.alttd.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true);
@@ -1,38 +0,0 @@
package com.alttd.forms.contact;
import com.alttd.forms.mail.verification.VerificationResult;
import com.alttd.forms.mail.verification.Verify;
import jakarta.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.concurrent.CompletableFuture;
@RestController
@RequestMapping("/api/contact")
public class ContactController {
private static final Logger logger = LoggerFactory.getLogger(ContactController.class);
@PostMapping("/submitContactForm")
public CompletableFuture<ResponseEntity<String>> submitForm(@Valid @RequestBody ContactFormData formData) {
logger.debug(formData.toString());
CompletableFuture<Integer> storeFormForVerificationCode = new StoreFormQuery().storeFormForVerificationCode(formData.email, formData);
return storeFormForVerificationCode.thenCompose(code -> Verify.verifyEmail(formData.email, code).thenApply(verificationResult -> {
if (verificationResult == VerificationResult.VERIFICATION_SENT) {
//TODO if this is ok tell the user they have x min to verify if they fail to do so they have to remake the form
return ResponseEntity.ok("User Data received and email verification sent.");
} else {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Failed to send verification email. Reason: " + verificationResult.name());
}
})).exceptionally(throwable -> {
logger.error("Failed to store form", throwable);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to store your form");
});
}
}
@@ -0,0 +1,47 @@
package com.alttd.forms.controlers;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class ControllerExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(ControllerExceptionHandler.class);
public ControllerExceptionHandler() {
logger.info("ControllerExceptionHandler initialized"); // Ensure this gets logged
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
logger.debug("invalid request", ex);
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
if (logger.isDebugEnabled()) {
try {
String errorsAsString = new ObjectMapper().writeValueAsString(errors);
logger.debug("Invalid parameters\n{}", errorsAsString);
} catch (JsonProcessingException e) {
logger.warn("Failed to parse errors to json", e);
}
}
return ResponseEntity.badRequest().body(errors);
}
}
@@ -0,0 +1,41 @@
package com.alttd.forms.controlers;
import com.alttd.forms.form.Form;
import com.alttd.forms.form.StoreFormQuery;
import com.alttd.forms.mail.verification.VerificationResult;
import com.alttd.forms.mail.verification.Verify;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import java.util.concurrent.CompletableFuture;
public class FormRequestHandler {
private static final Logger logger = LoggerFactory.getLogger(FormRequestHandler.class);
public static CompletableFuture<ResponseEntity<String>> handleRequestWithVerifyMail(Form form, String ip) {
CompletableFuture<Integer> storeFormForVerificationCode = new StoreFormQuery().storeFormForVerificationCode(form.getSender(), form);
return storeFormForVerificationCode.thenCompose(code -> Verify.verifyEmail(ip, form.getSender(), code, form).thenApply(verificationResult -> {
if (verificationResult == VerificationResult.VERIFICATION_SENT) {
//TODO if this is ok tell the user they have x min to verify if they fail to do so they have to remake the form
logger.trace("Staff application form stored and requested verification from user");
return ResponseEntity.ok("User Data received and email verification sent.");
} else if (verificationResult == VerificationResult.RATE_LIMIT_EXCEEDED) {
logger.trace("User hit the rate limit email: {} ip: {}", form.getSender(), ip);
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("You have sent too many forms today, " +
"please wait up to an hour before trying again. " +
"You can resubmit your form by hitting submit again later. " +
"Continued use of the form will extent the time you have to wait.");
} else {
logger.trace("Failed to send verification email {}", verificationResult.name());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Failed to send verification email. Reason: " + verificationResult.name());
}
})).exceptionally(throwable -> {
logger.error("Failed to store form", throwable);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to store your form");
});
}
}
@@ -0,0 +1,26 @@
package com.alttd.forms.controlers.apply;
import com.alttd.forms.controlers.FormRequestHandler;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.concurrent.CompletableFuture;
@RestController
@RequestMapping("/api/apply")
public class StaffAppController {
private static final Logger logger = LoggerFactory.getLogger(StaffAppController.class);
@PostMapping("/staffApplication")
public CompletableFuture<ResponseEntity<String>> submitForm(@Valid @RequestBody StaffAppFormData formData, HttpServletRequest request) {
logger.debug("submitForm");
logger.trace(formData.toString());
return FormRequestHandler.handleRequestWithVerifyMail(formData, request.getRemoteAddr());
}
}
@@ -0,0 +1,134 @@
package com.alttd.forms.controlers.apply;
import com.alttd.forms.form.Form;
import jakarta.validation.constraints.*;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;
import java.time.LocalDate;
import java.util.Optional;
public class StaffAppFormData extends Form {
public StaffAppFormData(String username, String email, String discord, String pc_requirements, int age, String pronoun, LocalDate join_date, int avg_time, String available_days, String available_time, String staff_experience, String plugin_experience, String why_staff, String expectations_mod, String other) {
this.username = username;
this.email = email;
this.discord = discord;
this.pc_requirements = pc_requirements;
this.age = age;
this.pronoun = pronoun;
this.join_date = join_date;
this.avg_time = avg_time;
this.available_days = available_days;
this.available_time = available_time;
this.staff_experience = staff_experience;
this.plugin_experience = plugin_experience;
this.why_staff = why_staff;
this.expectations_mod = expectations_mod;
this.other = other;
}
@NotEmpty(message = "Username is required")
@Length(min = 3, max = 16, message = "Username should be between 3 and 16 characters")
@Pattern(regexp = "^[a-zA-Z0-9_]*$", message = "Username should only include alphanumeric characters and underscore")
public final String username;
@NotEmpty(message = "E-mail address is required")
@Email(message = "Invalid email")
@Length(min = 3, max = 254, message = "Email should be between 3 and 254 characters")
public final String email;
@NotEmpty(message = "Discord name is required")
@Length(min = 2, max = 32, message = "Discord name should be between 2 and 32 characters")
@Pattern(regexp = "^([a-z0-9._]{2,32})$", message = "Please enter a valid Discord name")
public final String discord;
@NotEmpty(message = "An answer is required")
@Length(min = 2, max = 3, message = "Please answer yes or no")
@Pattern(regexp = "(?i)^(yes|no)$", message = "Yes or no")
public final String pc_requirements;
@Min(value = 0, message = "Please enter a valid age")
@Max(value = 999, message = "We do not accept players older than 999 years old sorry!")
public final int age;
@Length(max = 16, message = "Pronouns can't be longer than 16 characters")
public final String pronoun;
@NotNull(message = "Your join date is required, if you're not sure enter an estimated date")
public final LocalDate join_date;
@Range(min = 0, max = 168, message = "The only valid values are 0-168")
public final int avg_time;
@NotEmpty(message = "Available days are required")
@Length(min = 6, max = 128, message = "Available days should be between 6 and 128 characters")
public final String available_days;
@NotEmpty(message = "Available time is required")
@Length(min = 3, max = 256, message = "Available time should be between 3 and 256 characters")
public final String available_time;
@NotEmpty(message = "Staff experience is required")
@Length(min = 2, max = 2000, message = "Experience should be between 2 and 2000 characters")
public final String staff_experience;
@NotEmpty(message = "Plugin experience is required")
@Length(min = 2, max = 2000, message = "Experience should be between 2 and 2000 characters")
public final String plugin_experience;
@NotEmpty(message = "Reason for wanting to be a moderator is required")
@Length(min = 2, max =2000, message = "Reason should be between 2 and 2000 characters")
public final String why_staff;
@NotEmpty(message = "Expectations of a moderator is required")
@Length(min = 2, max = 2000, message = "Expectation should be between 2 and 2000 characters")
public final String expectations_mod;
@Length(max = 2000, message = "Text can't be longer than 2000 characters")
public final String other;
@Override
public String toString() {
return "StaffAppFormData{" +
"username='" + username + '\'' +
", email='" + email + '\'' +
", discord='" + discord + '\'' +
", pc_requirements='" + pc_requirements + '\'' +
", age=" + age +
", pronoun='" + pronoun + '\'' +
", join_date=" + join_date +
", avg_time=" + avg_time +
", available_days='" + available_days + '\'' +
", available_time='" + available_time + '\'' +
", staff_experience='" + staff_experience + '\'' +
", plugin_experience='" + plugin_experience + '\'' +
", why_staff='" + why_staff + '\'' +
", expectations_mod='" + expectations_mod + '\'' +
", other='" + other + '\'' +
'}';
}
@Override
public Optional<String> getDiscordBotUrl() {
return Optional.empty();
}
@Override
public String getReceiver() {
return "[email protected]";
}
@Override
public String getSender() {
return email;
}
@Override
public String toHtml() {
String[] fields = {"Username", "Email", "Discord", "PC requirements", "Age", "Pronoun", "Join date", "Avg time", "Available days", "Available time", "Staff experience", "Plugin experience", "Why staff", "Expectations mod", "Other"};
String[] values = {username, email, discord, pc_requirements, String.valueOf(age), pronoun, join_date.toString(), String.valueOf(avg_time), available_days, available_time, staff_experience, plugin_experience, why_staff, expectations_mod, other};
return toHtml(fields, values);
}
}
@@ -0,0 +1,26 @@
package com.alttd.forms.controlers.contact;
import com.alttd.forms.controlers.FormRequestHandler;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.concurrent.CompletableFuture;
@RestController
@RequestMapping("/api/contact")
public class ContactController {
private static final Logger logger = LoggerFactory.getLogger(ContactController.class);
@PostMapping("/submitContactForm")
public CompletableFuture<ResponseEntity<String>> submitForm(@Valid @RequestBody ContactFormData formData, HttpServletRequest request) {
logger.debug("submitForm");
logger.trace(formData.toString());
return FormRequestHandler.handleRequestWithVerifyMail(formData, request.getRemoteAddr());
}
}
@@ -1,4 +1,4 @@
package com.alttd.forms.contact;
package com.alttd.forms.controlers.contact;
import com.alttd.forms.form.Form;
import jakarta.validation.constraints.Email;
@@ -6,6 +6,8 @@ import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import org.hibernate.validator.constraints.Length;
import java.util.Optional;
public class ContactFormData extends Form {
public ContactFormData(String username, String email, String question) {
@@ -36,28 +38,26 @@ public class ContactFormData extends Form {
'}';
}
@Override
public Optional<String> getDiscordBotUrl() {
return Optional.of("http://discordbot:8001/api/contact/submitContactForm");
}
@Override
public String getReceiver() {
return "[email protected]";
}
@Override
public String getSender() {
return email;
}
@Override
public String toHtml() {
StringBuilder htmlOutput = new StringBuilder();
htmlOutput.append("<table style='border-collapse: collapse; width: 100%;'>");
String[] fields = {"Username", "Email", "Question"};
String[] values = {username, email, question};
for (int i = 0; i < fields.length; i++) {
htmlOutput.append("<tr style='border: 1px solid #ddd;'>");
htmlOutput.append("<td style='border: 1px solid #ddd; padding: 10px; font-weight: bold;'>");
htmlOutput.append(fields[i]);
htmlOutput.append("</td>");
htmlOutput.append("<td style='border: 1px solid #ddd; padding: 10px;'>");
htmlOutput.append(values[i]);
htmlOutput.append("</td>");
htmlOutput.append("</tr>");
}
htmlOutput.append("</table>");
return htmlOutput.toString();
return toHtml(fields, values);
}
}
@@ -0,0 +1,4 @@
package com.alttd.forms.controlers.form_active;
public record BooleanResponse(boolean isActive) {
}
@@ -0,0 +1,47 @@
package com.alttd.forms.controlers.form_active;
import com.alttd.forms.form.FormSettingsQuery;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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 java.util.concurrent.CompletableFuture;
@RestController
@RequestMapping("/api/checks")
public class FormActiveController {
private static final Logger logger = LoggerFactory.getLogger(FormActiveController.class);
@PostMapping("/formActive")
public CompletableFuture<ResponseEntity<String>> formActiveRequest(@Valid @RequestBody FormActiveData formData) {
logger.debug("formActive");
logger.trace(formData.toString());
FormSettingsQuery formSettingsQuery = new FormSettingsQuery();
return formSettingsQuery
.isActive(formData.formName)
.thenApply(result -> {
try {
if (result.isEmpty()) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to check if the form is active");
} else if (result.get()) {
return ResponseEntity.ok(new ObjectMapper().writeValueAsString(new BooleanResponse(true)));
} else {
return ResponseEntity.ok(new ObjectMapper().writeValueAsString(new BooleanResponse(false)));
}
} catch (JsonProcessingException e) {
logger.error("Failed to parse BooleanResponse", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to respond");
}
});
}
}
@@ -0,0 +1,20 @@
package com.alttd.forms.controlers.form_active;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import org.hibernate.validator.constraints.Length;
public class FormActiveData {
public FormActiveData() {}
public FormActiveData(String formName) {
this.formName = formName;
}
@NotEmpty(message = "You have to provide a form name")
@Length(min = 1, max = 64, message = "Usernames have to be between 3 and 16 characters")
@Pattern(regexp = "[a-zA-Z]{1,64}", message = "This is an invalid form name")
public String formName;
}
@@ -0,0 +1,17 @@
package com.alttd.forms.controlers.global_rate_limiter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<GlobalRateLimitingFilter> rateLimitingFilter() {
FilterRegistrationBean<GlobalRateLimitingFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new GlobalRateLimitingFilter());
registrationBean.addUrlPatterns("/api/*");
return registrationBean;
}
}
@@ -0,0 +1,67 @@
package com.alttd.forms.controlers.global_rate_limiter;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Component
public class GlobalRateLimitingFilter implements Filter {
private static final int MAX_REQUESTS_PER_MINUTE = 30;
private final Map<String, RequestCounter> ipRequestMap = new ConcurrentHashMap<>();
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String clientIp = httpRequest.getRemoteAddr();
RequestCounter requestCounter = ipRequestMap.computeIfAbsent(clientIp, k -> new RequestCounter(0, Instant.now().getEpochSecond()));
synchronized (requestCounter) {
long currentTime = Instant.now().getEpochSecond();
if (currentTime - requestCounter.timestamp > 60) {
// Reset counter every 60 seconds
requestCounter.timestamp = currentTime;
requestCounter.count.set(0);
}
if (requestCounter.count.incrementAndGet() > MAX_REQUESTS_PER_MINUTE) {
httpResponse.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
return;
}
}
chain.doFilter(request, response);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
private static class RequestCounter {
AtomicInteger count;
long timestamp;
public RequestCounter(int count, long timestamp) {
this.count = new AtomicInteger(count);
this.timestamp = timestamp;
}
}
}
@@ -12,8 +12,45 @@ public class Database {
public static void createTables() {
String[] createTables = {
"CREATE TABLE IF NOT EXISTS verify_form (e_mail VARCHAR(256), verification_code INT, formId INT, PRIMARY KEY(e_mail, verification_code))",
"CREATE TABLE IF NOT EXISTS form (formId INT AUTO_INCREMENT, creation_date BIGINT, form_json TEXT, form_class VARCHAR(64), PRIMARY KEY(formId))"
// language=SQL
"""
CREATE TABLE IF NOT EXISTS verify_form(
e_mail VARCHAR(256) NOT NULL,
verification_code INT NOT NULL,
formId INT NOT NULL,
PRIMARY KEY(e_mail, verification_code)
)
""",
// language=SQL
"""
CREATE TABLE IF NOT EXISTS form(
formId INT AUTO_INCREMENT NOT NULL,
creation_date BIGINT NOT NULL,
form_json TEXT NOT NULL,
form_class VARCHAR(64) NOT NULL,
PRIMARY KEY(formId)
)
""",
// language=SQL
"""
CREATE TABLE IF NOT EXISTS rate_limit(
id INT AUTO_INCREMENT NOT NULL,
time TIMESTAMP NOT NULL,
ip VARCHAR(45) NOT NULL,
mail VARCHAR(256) NOT NULL,
PRIMARY KEY(id)
)
""",
// language=SQL
"""
CREATE TABLE IF NOT EXISTS form_active(
id INT AUTO_INCREMENT NOT NULL,
name VARCHAR(64) NOT NULL,
active_from TIMESTAMP NOT NULL,
active_until TIMESTAMP NOT NULL,
PRIMARY KEY(id)
)
"""
};
Connection connection = DatabaseConnection.getConnection();
for (String query : createTables) {
+39 -3
View File
@@ -1,15 +1,51 @@
package com.alttd.forms.form;
import com.google.gson.Gson;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.util.Optional;
public abstract class Form {
public String toJsonString() {
return new Gson().toJson(this);
public String toJsonString() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
return objectMapper.writeValueAsString(this);
}
public abstract String toHtml();
public String toHtml(String[] fields, String[] values) {
StringBuilder htmlOutput = new StringBuilder();
//language=HTML
htmlOutput.append("<table style='border-collapse: collapse; width: 100%;'>");
for (int i = 0; i < fields.length; i++) {
htmlOutput.append(
String.format(
//language=HTML
"""
<tr style='border: 1px solid #ddd;'>
<td style='border: 1px solid #ddd; padding: 10px; font-weight: bold;'>
%s
</td>
<td style='border: 1px solid #ddd; padding: 10px;'>
%s
</td>
</tr>
""", fields[i], values[i]));
}
//language=HTML
htmlOutput.append("</table>");
return htmlOutput.toString();
}
@Override
public abstract String toString();
public abstract Optional<String> getDiscordBotUrl();
public abstract String getReceiver();
public abstract String getSender();
}
@@ -0,0 +1,43 @@
package com.alttd.forms.form;
import com.alttd.forms.database.DatabaseConnection;
import jakarta.validation.constraints.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Instant;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
public class FormSettingsQuery {
private static final Logger logger = LoggerFactory.getLogger(FormSettingsQuery.class);
public CompletableFuture<Optional<Boolean>> isActive(@NotNull String formName) {
return CompletableFuture.supplyAsync(() -> {
Connection connection = DatabaseConnection.getConnection();
String sql = "SELECT active_from, active_until FROM form_active WHERE name = ?";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, formName);
ResultSet resultSet = stmt.executeQuery();
if (!resultSet.next()) {
return Optional.of(true);
}
Instant activeFrom = resultSet.getTimestamp("active_from").toInstant();
Instant activeUntil = resultSet.getTimestamp("active_until").toInstant();
Instant now = Instant.now();
if (activeFrom.isAfter(now) || activeUntil.isBefore(now))
return Optional.of(false);
return Optional.of(true);
} catch (SQLException e) {
logger.error("Failed to check if the {} form is active", formName ,e);
return Optional.empty();
}
});
}
}
@@ -1,4 +1,4 @@
package com.alttd.forms.contact;
package com.alttd.forms.form;
import com.alttd.forms.database.DatabaseConnection;
@@ -8,7 +8,7 @@ import java.util.Optional;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import com.alttd.forms.form.Form;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -42,6 +42,9 @@ public class StoreFormQuery {
} catch (SQLException e) {
logger.error("Failed insert form query for: " + form, e);
return Optional.empty();
} catch (JsonProcessingException e) {
logger.error("Invalid class for JSON, failed insert form query for: " + form, e);
return Optional.empty();
}
}
@@ -61,13 +64,17 @@ public class StoreFormQuery {
}
public CompletableFuture<Integer> storeFormForVerificationCode(String eMail, Form form) {
logger.debug("storeFormForVerificationCode");
Connection connection = DatabaseConnection.getConnection();
logger.trace("Connection: {}", connection);
return CompletableFuture.supplyAsync(() -> {
Optional<Long> optionalFormId = insertForm(connection, form);
logger.trace("Form id: {}", optionalFormId);
if (optionalFormId.isEmpty()) {
throw new RuntimeException("Failed to store form");
}
Optional<Integer> verificationCode = insertVerificationCodeForForm(connection, eMail, optionalFormId.get());
logger.trace("Verification code: {}", verificationCode);
if (verificationCode.isEmpty()) {
throw new RuntimeException("Failed to set verification code");
}
@@ -19,7 +19,7 @@ public class MailForm {
Properties mailProperties = MailSettings.getMailProperties();
Optional<PasswordAuthentication> accountDetails = MailSettings.getAccountDetails();
if (accountDetails.isEmpty()) {
logger.error("No account details, can't send email to " + receiver + " with data " + form.toString());
logger.error("No account details, can't send email to {} with data {}", receiver, form.toString());
return;
}
PasswordAuthentication passwordAuthentication = accountDetails.get();
@@ -37,9 +37,9 @@ public class MailForm {
message.setContent(form.toHtml(), "text/html");
try {
Transport.send(message);
logger.debug("Send mail to " + receiver + " containing " + form) ;
logger.debug("Send mail to {} containing {}", receiver, form);
} catch (MessagingException e) {
logger.error("Unable to send mail to " + receiver + " with data " + form, e);
logger.error("Unable to send mail to {} with data {}", receiver, form, e);
}
} catch (MessagingException e) {
logger.error("Failed to create MimeMessage", e);
@@ -0,0 +1,87 @@
package com.alttd.forms.mail.rate_limitter;
import com.alttd.forms.database.DatabaseConnection;
import com.alttd.forms.properties.PropertiesLoader;
import com.alttd.forms.properties.PropertiesWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.SQLException;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.CompletableFuture;
public class FormRateLimit {
private static final Logger logger = LoggerFactory.getLogger(FormRateLimit.class);
private static FormRateLimit instance;
private Properties properties;
private FormRateLimit(String path) {
FormRateLimit.instance = this;
loadProperties(path);
}
public static FormRateLimit getInstance() {
return FormRateLimit.instance;
}
public static void initialize(String path) {
if (instance != null)
return;
FormRateLimit.instance = new FormRateLimit(path);
}
private void loadProperties(String path) {
String fileName = "form.properties";
Optional<Properties> optionalProperties = PropertiesLoader.loadProperties(path, fileName);
if (optionalProperties.isPresent()) {
properties = optionalProperties.get();
return;
}
properties = new Properties();
properties.setProperty("rate-limit-ip-count", "10");
properties.setProperty("rate-limit-mail-count", "10");
properties.setProperty("rate-limit-ip-minutes", "60");
properties.setProperty("rate-limit-mail-minutes", "60");
PropertiesWriter.writeProperties(properties, path, fileName);
}
public CompletableFuture<Optional<Boolean>> isRateLimited(String ip, String mail) {
return CompletableFuture.supplyAsync(
() -> {
RateLimitQuery rateLimitQuery = new RateLimitQuery(DatabaseConnection.getConnection());
try {
if (isIpRateLimited(rateLimitQuery, ip) || isMailRateLimited(rateLimitQuery, mail))
return Optional.of(true);
} catch (SQLException e) {
logger.error("Failed rate limit query for ip: {}, mail: {}", ip, mail, e);
return Optional.empty();
} catch (NumberFormatException e) {
logger.error("Failed loading numbers from properties", e);
return Optional.empty();
}
return Optional.of(false);
});
}
private boolean isIpRateLimited(RateLimitQuery rateLimitQuery, String ip) throws SQLException, NumberFormatException {
int minutes = Integer.parseInt(properties.getProperty("rate-limit-ip-minutes"));
Instant after = Instant.now().minus(Duration.ofMinutes(minutes));
int ipHits = rateLimitQuery.getIpHits(ip, after);
int ipRateLimit = Integer.parseInt(properties.getProperty("rate-limit-ip-count"));
return ipHits >= ipRateLimit;
}
private boolean isMailRateLimited(RateLimitQuery rateLimitQuery, String mail) throws SQLException, NumberFormatException {
int minutes = Integer.parseInt(properties.getProperty("rate-limit-mail-minutes"));
Instant after = Instant.now().minus(Duration.ofMinutes(minutes));
int ipHits = rateLimitQuery.getMailHits(mail, after);
int ipRateLimit = Integer.parseInt(properties.getProperty("rate-limit-mail-count"));
return ipHits >= ipRateLimit;
}
}
@@ -0,0 +1,6 @@
package com.alttd.forms.mail.rate_limitter;
import java.time.Instant;
public record RateLimitEntryDTO(Instant time, String ip, String mail) {
}
@@ -0,0 +1,64 @@
package com.alttd.forms.mail.rate_limitter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.*;
import java.time.Instant;
public class RateLimitQuery {
private static final Logger logger = LoggerFactory.getLogger(RateLimitQuery.class);
private final Connection connection;
protected RateLimitQuery(Connection connection) {
this.connection = connection;
}
protected int getIpHits(String ip, Instant after) throws SQLException {
String sql = "SELECT COUNT(*) AS hits FROM rate_limit WHERE ip = ? AND time > ?";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, ip);
stmt.setTimestamp(2, Timestamp.from(after));
ResultSet resultSet = stmt.executeQuery();
if (!resultSet.next()) {
return 0;
}
return resultSet.getInt("hits");
} catch (SQLException e) {
logger.error("Failed get ip hits query for ip: {}", ip, e);
throw e;
}
}
protected int getMailHits(String mail, Instant after) throws SQLException {
String sql = "SELECT COUNT(*) AS hits FROM rate_limit WHERE mail = ? AND time > ?";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, mail);
stmt.setTimestamp(2, Timestamp.from(after));
ResultSet resultSet = stmt.executeQuery();
if (!resultSet.next()) {
return 0;
}
return resultSet.getInt("hits");
} catch (SQLException e) {
logger.error("Failed get mail hits query for ip: {}", mail, e);
throw e;
}
}
protected boolean insertRateLimitEntry(RateLimitEntryDTO entry) throws SQLException {
String sql = "INSERT INTO rate_limit (time, ip, mail) VALUES (?, ?, ?)";
try {
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setTimestamp(1, Timestamp.from(entry.time()));
stmt.setString(2, entry.ip());
stmt.setString(3, entry.mail());
return stmt.executeUpdate() > 0;
} catch (SQLException e) {
logger.error("Failed to store rate limit for ip: {}, mail: {}, time: {}", entry.ip(), entry.mail(), entry.time(), e);
throw e;
}
}
}
@@ -1,6 +1,6 @@
package com.alttd.forms.mail.verification;
public enum VerificationResult {
NO_MAIL_ACCOUNT, FAILED_TO_SEND, VERIFICATION_SENT,
NO_MAIL_ACCOUNT, FAILED_TO_SEND, VERIFICATION_SENT, RATE_LIMIT_EXCEEDED, FAILED_TO_RETRIEVE_RATE_LIMIT_DATA;
}
@@ -1,6 +1,8 @@
package com.alttd.forms.mail.verification;
import com.alttd.forms.form.Form;
import com.alttd.forms.mail.MailSettings;
import com.alttd.forms.mail.rate_limitter.FormRateLimit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -15,39 +17,77 @@ public class Verify {
private static final Logger logger = LoggerFactory.getLogger(Verify.class);
public static CompletableFuture<VerificationResult> verifyEmail(String address, int code) {
public static CompletableFuture<VerificationResult> verifyEmail(String ip, String address, int code, Form form) {
logger.debug("verifyEmail");
Properties mailProperties = MailSettings.getMailProperties();
logger.debug("mailProperties: {}", mailProperties);
Optional<PasswordAuthentication> accountDetails = MailSettings.getAccountDetails();
if (accountDetails.isEmpty()) {
logger.debug("accountDetails is empty");
return CompletableFuture.completedFuture(VerificationResult.NO_MAIL_ACCOUNT);
}
PasswordAuthentication passwordAuthentication = accountDetails.get();
logger.trace("accountDetails: user {} pass {}", passwordAuthentication.getUserName(), passwordAuthentication.getPassword());
Session session = MailSettings.getSession(mailProperties, passwordAuthentication);
//TODO rate limit sending mail from IP and to specific e-mail addresses (max 1 per minute and max 10 per day)
//TODO include a link to all emails that people can click to block us from sending mail to them so no one can use us to spam ppl
return CompletableFuture.supplyAsync(() -> sendMail(ip, address, code, session, passwordAuthentication, form));
}
private static VerificationResult sendMail(String ip, String address, int code, Session session, PasswordAuthentication passwordAuthentication, Form form) {
Optional<Boolean> isRateLimited = FormRateLimit.getInstance().isRateLimited(ip, address).join();
if (isRateLimited.isEmpty()) {
return VerificationResult.FAILED_TO_RETRIEVE_RATE_LIMIT_DATA;
} else if (isRateLimited.get()) {
return VerificationResult.RATE_LIMIT_EXCEEDED;
}
Message message;
try {
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(passwordAuthentication.getUserName()));
message.setRecipients(
Message.RecipientType.TO,
InternetAddress.parse(address)
);
message.setSubject("Altitude Email Verification");
message.setText("Please verify your email by entering the following code on the page you made the form in\n" + code); //TODO pretty html stuff
//TODO include the form they filled in (also in pretty html stuff)
return CompletableFuture.supplyAsync(() -> {
try {
Transport.send(message);
return VerificationResult.VERIFICATION_SENT;
} catch (MessagingException e) {
return VerificationResult.FAILED_TO_SEND;
}
});
message = getMail(address, code, session, passwordAuthentication, form);
} catch (MessagingException e) {
logger.error("Failed to create MimeMessage", e);
return CompletableFuture.completedFuture(VerificationResult.FAILED_TO_SEND);
return VerificationResult.FAILED_TO_SEND;
}
try {
logger.trace("Sending mail");
Transport.send(message);
logger.trace("Sending mail succeeded");
return VerificationResult.VERIFICATION_SENT;
} catch (MessagingException e) {
logger.error("Failed to send mail", e);
return VerificationResult.FAILED_TO_SEND;
}
}
private static Message getMail(String address, int code, Session session, PasswordAuthentication passwordAuthentication, Form form) throws MessagingException {
logger.trace("Creating mail");
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(passwordAuthentication.getUserName()));
logger.trace("Set from to {}", passwordAuthentication.getUserName());
message.setRecipients(
Message.RecipientType.TO,
InternetAddress.parse(address)
);
logger.trace("Set recipients");
message.setSubject("Altitude Email Verification");
String body = String.format(
//language=HTML
"""
<title>
Email Verification
</title>
<p>Please verify your email by entering the following code on the page you made the form in:</p>
<p>%s</p>
<p></p>
<p>This is the form you're submitting:</p>
""", code);
body += form.toHtml();
message.setContent(body, "text/html");
logger.trace("Set code: {}", code);
return message;
}
}
@@ -26,7 +26,7 @@ public class PropertiesLoader {
}
file = new File(currentJarPath.get(), fileName);
} else {
file = Path.of(URI.create("file://" + path + File.separator + fileName)).toFile();
file = new File(path, fileName);
}
@@ -25,7 +25,7 @@ public class PropertiesWriter {
}
file = new File(currentJarPath.get(), fileName);
} else {
file = Path.of(URI.create("file://" + path + File.separator + fileName)).toFile();
file = new File(path, fileName);
}
if (file.exists()) {
@@ -1,9 +1,12 @@
package com.alttd.forms.verify_mail;
import com.alttd.forms.contact.ContactFormData;
import com.alttd.forms.controlers.apply.StaffAppFormData;
import com.alttd.forms.controlers.contact.ContactFormData;
import com.alttd.forms.database.DatabaseConnection;
import com.alttd.forms.form.Form;
import com.google.gson.Gson;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -26,12 +29,12 @@ public class FormQuery {
stmt.setString(2, eMail);
ResultSet resultSet = stmt.executeQuery();
if (!resultSet.next()) {
logger.warn("A user tried to enter an invalid code: " + verificationCode + " with email: " + eMail);
logger.warn("A user tried to enter an invalid code: {} with email: {}", verificationCode, eMail);
return Optional.empty();
}
return Optional.of(resultSet.getInt("formId"));
} catch (SQLException e) {
logger.error("Failed select form query for verification code: " + verificationCode + " with e-mail " + eMail, e);
logger.error("Failed select form query for verification code: {} with e-mail {}", verificationCode, eMail, e);
throw e;
}
}
@@ -43,7 +46,7 @@ public class FormQuery {
stmt.setInt(1, formId);
ResultSet resultSet = stmt.executeQuery();
if (!resultSet.next()) {
logger.warn("Could not find form with id: " + formId);
logger.warn("Could not find form with id: {}", formId);
return Optional.empty();
}
String json = resultSet.getString("form_json");
@@ -53,17 +56,25 @@ public class FormQuery {
} catch (IllegalArgumentException e) {
logger.error("Invalid form class in database", e);
return Optional.empty();
} catch (JsonProcessingException e) {
logger.error("Invalid form json in database", e);
return Optional.empty();
}
} catch (SQLException e) {
logger.error("Failed select form query for form with id: " + formId, e);
logger.error("Failed select form query for form with id: {}", formId, e);
throw e;
}
}
private Form getForm(String className, String json) throws IllegalArgumentException {
private Form getForm(String className, String json) throws IllegalArgumentException, JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
switch (className) {
case "ContactFormData" -> {
return new Gson().fromJson(json, ContactFormData.class);
return objectMapper.readValue(json, ContactFormData.class);
}
case "StaffAppFormData" -> {
return objectMapper.readValue(json, StaffAppFormData.class);
}
default -> throw new IllegalArgumentException("Invalid form class name: " + className);
}
@@ -87,6 +98,7 @@ public class FormQuery {
}
if (formId.isEmpty()) {
logger.trace("Unable to find form with code: {} and email {}", code, eMail);
return new FormQueryResult(Optional.empty(), "Unable to find form for a user with this code and e-mail");
}
@@ -1,6 +1,8 @@
package com.alttd.forms.verify_mail;
import com.alttd.forms.form.Form;
import com.alttd.forms.mail.mail_forms.MailForm;
import com.fasterxml.jackson.core.JsonProcessingException;
import jakarta.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -17,6 +19,7 @@ import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@RestController
@@ -27,35 +30,57 @@ public class VerifyController {
@PostMapping("/form")
public CompletableFuture<ResponseEntity<String>> validateEmailFromForm(@Valid @RequestBody VerificationData verificationData) {
logger.debug(verificationData.toString());
logger.debug("validateEmailFromForm");
logger.trace("verificationData: {}", verificationData);
return new FormQuery().getFormForCode(verificationData.code, verificationData.eMail).thenApply(result -> result.form()
.map(form -> {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("http://51.222.255.92:8001/api/contact/submitContactForm"))//TODO get uri from form
.header("Content-Type", "application/json;charset=UTF-8")
.POST(HttpRequest.BodyPublishers.ofString(form.toJsonString(), StandardCharsets.UTF_8))
.build();
HttpClient client = HttpClient.newHttpClient();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() < 200 || response.statusCode() > 200) {
logger.error(String.format("Failed to send form to Discord. Got status code [%d], with body\n%s", response.statusCode(), response.body()));
//TODO handle failure
//TODO strings to config
return ResponseEntity.ok("Failed to send form to Discord, please contact us at [email protected]");
Optional<String> discordBotUrl = form.getDiscordBotUrl();
if (discordBotUrl.isPresent()){
Optional<ResponseEntity<String>> stringResponseEntity = sendDiscordMessage(discordBotUrl.get(), form);
if (stringResponseEntity.isPresent()){
return stringResponseEntity.get();
}
} catch (URISyntaxException e) {
logger.error("Unable to create URI for posting form", e); //TODO more clear
return ResponseEntity.ok("Unable to create URI for posting form, please contact us at [email protected]");
} catch (IOException | InterruptedException e) {
logger.error("Unable to send form to Discord", e); //TODO more clear
}
MailForm.sendForm(form.getReceiver(), form);
try {
return ResponseEntity.ok(form.toJsonString());
} catch (JsonProcessingException e) {
logger.error("Unable to send form to Discord, invalid JSON", e);
return ResponseEntity.ok("Unable to send form to Discord, please contact us at [email protected]");
}
MailForm.sendForm("[email protected]", form);
return ResponseEntity.ok(form.toJsonString());
})
.orElse(ResponseEntity.ok(result.failReason()))
).exceptionally(throwable -> ResponseEntity.internalServerError()
.body("The server was unable to process your request, if this issue persists please contact [email protected]"));
}
private Optional<ResponseEntity<String>> sendDiscordMessage(String discordBotUrl, Form form) {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI(discordBotUrl))
.header("Content-Type", "application/json;charset=UTF-8")
.POST(HttpRequest.BodyPublishers.ofString(form.toJsonString(), StandardCharsets.UTF_8))
.build();
logger.trace("request: {}", request);
HttpClient client = HttpClient.newHttpClient();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
logger.trace("response: {}", response);
if (response.statusCode() < 200 || response.statusCode() > 200) {
logger.error(String.format("Failed to send form to Discord. Got status code [%d], with body\n%s", response.statusCode(), response.body()));
//TODO handle failure
//TODO strings to config
return Optional.of(ResponseEntity.ok("Failed to send form to Discord, please contact us at [email protected]"));
}
} catch (URISyntaxException e) {
logger.error("Unable to create URI for posting form", e); //TODO more clear
return Optional.of(ResponseEntity.ok("Unable to create URI for posting form, please contact us at [email protected]"));
} catch (JsonProcessingException e) {
logger.error("Unable to send form to Discord, invalid JSON", e);
return Optional.of(ResponseEntity.ok("Unable to send form to Discord, please contact us at [email protected]"));
} catch (IOException | InterruptedException e) {
logger.error("Unable to send form to Discord", e); //TODO more clear
return Optional.of(ResponseEntity.ok("Unable to send form to Discord, please contact us at [email protected]"));
}
return Optional.empty();
}
}
+3 -1
View File
@@ -1,2 +1,4 @@
logging.level.com.alttd.forms=warn
logging.level.com.alttd.forms=WARN
server.port=8002
spring.jackson.date-format=yyyy-MM-dd
spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=false
+9 -2
View File
@@ -1,20 +1,25 @@
import com.alttd.forms.contact.ContactFormData;
import com.alttd.forms.contact.StoreFormQuery;
import com.alttd.forms.controlers.contact.ContactFormData;
import com.alttd.forms.form.StoreFormQuery;
import com.alttd.forms.database.DatabaseConnection;
import com.alttd.forms.form.Form;
import com.alttd.forms.verify_mail.FormQuery;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.util.Optional;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
public class TestForm {
private static int code;
@BeforeAll
@Disabled
public static void testInsert() {
assumeTrue(System.getProperty("doNotRunDatabaseTests") == null);
Assertions.assertDoesNotThrow(() -> DatabaseConnection.initialize());
StoreFormQuery storeFormQuery = new StoreFormQuery();
ContactFormData contactFormData = new ContactFormData("akastijn", "[email protected]", "This is a test question.");
@@ -27,7 +32,9 @@ public class TestForm {
}
@Test
@Disabled
public void testRetrieveForm() {
assumeTrue(System.getProperty("doNotRunDatabaseTests") == null);
Assertions.assertDoesNotThrow(() -> DatabaseConnection.initialize());
new FormQuery().getFormForCode(String.valueOf(code), "[email protected]").thenAccept(result -> {
Assertions.assertTrue(result.failReason().isEmpty());