Skip to content

Commit c297b81

Browse files
authored
Merge pull request #44 from devondragon/issue-43-Add-Filter-for-Spring-Security-Form-Login-Support
Issue 43 add filter for spring security form login support
2 parents 2d1d51b + 396f685 commit c297b81

9 files changed

Lines changed: 518 additions & 166 deletions

File tree

README.md

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,16 +138,16 @@ public class LoginController {
138138

139139
@Autowired
140140
private TurnstileValidationService turnstileValidationService;
141-
141+
142142
@Autowired
143143
private UserService userService;
144-
144+
145145
@PostMapping("/login")
146-
public String login(Model model,
146+
public String login(Model model,
147147
@RequestParam String email,
148148
@RequestParam(name = "cf-turnstile-response", required = true) String turnstileResponse,
149149
HttpServletRequest request) {
150-
150+
151151
// Get the client IP address (recommended for security)
152152
String clientIpAddress = turnstileValidationService.getClientIpAddress(request);
153153

@@ -266,6 +266,55 @@ Spring Cloudflare Turnstile uses Spring Boot's auto-configuration to seamlessly
266266

267267
7. **DTO Layer**: Response objects map directly to Cloudflare's API responses.
268268

269+
## Optional: Configuring Turnstile Captcha Filter with Spring Security
270+
271+
This project provides an optional helper component, **TurnstileCaptchaFilter**, which can be used to add Turnstile captcha validation to your Spring Security Form Lgin flow.
272+
273+
### Configuration
274+
275+
Configure the following properties in your `application.properties` or `application.yml`:
276+
277+
```properties
278+
ds.cf.turnstile.login.submissionPath=/login
279+
ds.cf.turnstile.login.redirectUrl=/login?error=captcha
280+
ds.cf.turnstile.token.parameterName=cf-turnstile-response
281+
```
282+
283+
### Integration with Spring Security
284+
285+
To use the filter with form login, add it to your security configuration before the default authentication filter:
286+
287+
```java
288+
@Configuration
289+
@EnableWebSecurity
290+
public class SecurityConfig {
291+
292+
@Bean
293+
public SecurityFilterChain filterChain(HttpSecurity http,
294+
TurnstileCaptchaFilter turnstileCaptchaFilter) throws Exception {
295+
http
296+
// Add the Turnstile captcha filter before the default authentication filter
297+
.addFilterBefore(turnstileCaptchaFilter, UsernamePasswordAuthenticationFilter.class)
298+
.formLogin(form -> form
299+
.loginPage("/login")
300+
.defaultSuccessUrl("/home")
301+
.failureUrl("/login?error") // You might handle errors differently
302+
)
303+
.authorizeHttpRequests(auth -> auth
304+
.anyRequest().authenticated()
305+
);
306+
return http.build();
307+
}
308+
}
309+
```
310+
311+
### Notes
312+
313+
- The filter checks if the request's servlet path matches the configured login submission path and that the HTTP method is POST.
314+
- It validates the captcha token (expected to be sent under the configured parameter name) using the TurnstileValidationService.
315+
- On a successful captcha validation, the request continues through the filter chain. Otherwise, it logs a warning and redirects to the login page.
316+
- This component is completely optional; if you do not require captcha validation for your login flow, do not enable or configure this filter.
317+
269318
## Contributing
270319

271320
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to this project.

generate_changelog.py

Lines changed: 171 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,52 +3,186 @@
33
import subprocess
44
from openai import OpenAI
55
from datetime import date
6+
import re
7+
# tempfile no longer needed
68

7-
def get_git_commits():
9+
def get_git_commits_with_diffs():
810
# Get the last tag
9-
last_tag = subprocess.check_output(
10-
["git", "describe", "--tags", "--abbrev=0"], text=True
11+
try:
12+
last_tag = subprocess.check_output(
13+
["git", "describe", "--tags", "--abbrev=0"], text=True
14+
).strip()
15+
except subprocess.CalledProcessError:
16+
# If no tags exist, use the first commit
17+
last_tag = subprocess.check_output(
18+
["git", "rev-list", "--max-parents=0", "HEAD"], text=True
19+
).strip()
20+
print("No tags found. Using the first commit as reference.")
21+
22+
# Get commit hashes and messages since the last tag
23+
commit_info = subprocess.check_output(
24+
["git", "log", f"{last_tag}..HEAD", "--pretty=format:%H|%s"], text=True
1125
).strip()
1226

13-
# Get commit messages since the last tag
14-
commits = subprocess.check_output(
15-
["git", "log", f"{last_tag}..HEAD", "--pretty=format:%s"], text=True
16-
).strip()
27+
if not commit_info:
28+
return last_tag, []
29+
30+
commits_with_diffs = []
31+
for line in commit_info.split("\n"):
32+
if not line:
33+
continue
34+
35+
commit_hash, commit_message = line.split("|", 1)
36+
37+
# Get the diff for this commit
38+
diff = subprocess.check_output(
39+
["git", "show", "--stat", "--patch", commit_hash], text=True
40+
)
41+
42+
# Extract file changes
43+
files_changed = subprocess.check_output(
44+
["git", "show", "--name-status", commit_hash], text=True
45+
).strip()
46+
47+
commits_with_diffs.append({
48+
"hash": commit_hash,
49+
"message": commit_message,
50+
"diff": diff,
51+
"files_changed": files_changed
52+
})
53+
54+
return last_tag, commits_with_diffs
55+
56+
def categorize_commits(commits_with_diffs):
57+
"""Pre-categorize commits based on conventional commit prefixes"""
58+
categories = {
59+
"features": [],
60+
"fixes": [],
61+
"breaking_changes": [],
62+
"refactorings": [],
63+
"docs": [],
64+
"tests": [],
65+
"other": []
66+
}
67+
68+
for commit in commits_with_diffs:
69+
message = commit["message"].lower()
70+
71+
# Check for breaking changes first
72+
if "breaking change" in message or "!" in message.split(":", 1)[0]:
73+
categories["breaking_changes"].append(commit)
74+
# Then check for common prefixes
75+
elif message.startswith(("feat", "feature")):
76+
categories["features"].append(commit)
77+
elif message.startswith(("fix", "bugfix", "bug")):
78+
categories["fixes"].append(commit)
79+
elif message.startswith("refactor"):
80+
categories["refactorings"].append(commit)
81+
elif message.startswith(("doc", "docs")):
82+
categories["docs"].append(commit)
83+
elif message.startswith(("test", "tests")):
84+
categories["tests"].append(commit)
85+
else:
86+
# Attempt to categorize based on the diff
87+
if any(term in commit["diff"].lower() for term in ["fix", "bug", "issue", "error", "crash"]):
88+
categories["fixes"].append(commit)
89+
elif any(term in commit["diff"].lower() for term in ["feat", "feature", "add", "new", "implement"]):
90+
categories["features"].append(commit)
91+
else:
92+
categories["other"].append(commit)
93+
94+
return categories
95+
96+
def generate_changelog(commits_with_diffs, categorized_commits):
97+
if not commits_with_diffs:
98+
return "No commits to include in the changelog."
1799

18-
return last_tag, commits.split("\n")
100+
# Prepare the detailed diff information
101+
diff_content = "# Git Commit Information for Changelog Generation\n\n"
19102

20-
def generate_changelog(commits):
21-
if not commits:
22-
return "No commits to include in the changelog."
103+
# Add categorized commit information
104+
for category, commits in categorized_commits.items():
105+
if commits:
106+
diff_content += f"## {category.replace('_', ' ').title()}\n"
107+
for commit in commits:
108+
diff_content += f"### Commit: {commit['hash'][:8]} - {commit['message']}\n"
109+
110+
# Add file changes summary
111+
diff_content += "#### Files Changed:\n"
112+
diff_content += f"```\n{commit['files_changed']}\n```\n"
113+
114+
# Add a truncated diff only if it's very long (over 500 lines)
115+
diff_lines = commit['diff'].split('\n')
116+
if len(diff_lines) > 500:
117+
truncated_diff = '\n'.join(diff_lines[:500])
118+
truncated_diff += f"\n... (diff truncated, showing first 500 of {len(diff_lines)} lines)"
119+
else:
120+
truncated_diff = commit['diff']
23121

122+
diff_content += "#### Diff Preview:\n"
123+
diff_content += f"```diff\n{truncated_diff}\n```\n\n"
124+
125+
# Build the prompt with categorized information and diff data
24126
prompt = f"""
25-
You are a helpful assistant tasked with creating a changelog. Based on these Git commit messages, generate a clear, human-readable changelog:
127+
You are a skilled software developer tasked with creating a detailed changelog.
128+
I have provided you with git commit information including:
129+
1. Commit messages
130+
2. File changes
131+
3. Code diffs
132+
133+
Please generate a clear, comprehensive changelog based on this information.
134+
The commits have been pre-categorized, but please use the actual code changes to:
135+
- Improve category assignments if needed
136+
- Add more specific details about what changed in each commit
137+
- Extract key implementation details from the diffs
138+
- Identify significant changes that aren't clear from just the commit messages
139+
140+
Here is the detailed commit information:
141+
142+
{diff_content}
143+
144+
Format the changelog in Markdown as follows:
145+
### Features
146+
- Detailed feature descriptions here, with substantive information from the diffs
26147
27-
Commit messages:
28-
{commits}
148+
### Fixes
149+
- Detailed bug fix descriptions here, with substantive information from the diffs
29150
30-
Format the changelog as follows:
31-
### Features
32-
- List features here
151+
### Breaking Changes
152+
- Detailed descriptions of breaking changes here (if any), with clear explanations of what changed
33153
34-
### Fixes
35-
- List fixes here
154+
### Refactoring
155+
- Important code refactoring changes (if any)
36156
37-
### Breaking Changes
38-
- List breaking changes here (if any)
39-
"""
157+
### Documentation
158+
- Documentation updates (if any)
159+
160+
### Testing
161+
- Test-related changes (if any)
162+
163+
### Other Changes
164+
- Any other significant changes
165+
166+
Important: Focus on providing value to humans reading the changelog. Explain changes in user-centric terms where possible.
167+
"""
40168

41169
client = OpenAI(
42-
api_key=os.environ.get("OPENAI_API_TOKEN"), # This is the default and can be omitted
170+
api_key=os.environ.get("OPENAI_API_TOKEN"),
43171
)
172+
173+
# Use GPT-4o (without fallback)
44174
response = client.chat.completions.create(
45-
model="gpt-4",
175+
model="gpt-4o",
46176
messages=[
47-
{"role": "system", "content": "You are a helpful assistant for software development."},
177+
{"role": "system", "content": "You are a helpful assistant for software development with expertise in analyzing code changes and creating detailed changelogs."},
48178
{"role": "user", "content": prompt},
49179
],
50180
)
51-
return response.choices[0].message.content.strip()
181+
changelog = response.choices[0].message.content.strip()
182+
183+
# No temporary file to clean up anymore
184+
185+
return changelog
52186

53187
def update_changelog(version, changelog_content):
54188
changelog_file = "CHANGELOG.md"
@@ -65,13 +199,19 @@ def update_changelog(version, changelog_content):
65199
f.write(new_entry)
66200

67201
if __name__ == "__main__":
68-
last_tag, commits = get_git_commits()
69-
if not commits:
202+
last_tag, commits_with_diffs = get_git_commits_with_diffs()
203+
204+
if not commits_with_diffs:
70205
print("No new commits found.")
71-
exit()
206+
sys.exit(0)
207+
208+
print(f"Found {len(commits_with_diffs)} commits since {last_tag}")
72209

73-
print("Generating changelog...")
74-
changelog_content = generate_changelog("\n".join(commits))
210+
# Pre-categorize commits for better LLM results
211+
categorized_commits = categorize_commits(commits_with_diffs)
212+
213+
print("Generating detailed changelog...")
214+
changelog_content = generate_changelog(commits_with_diffs, categorized_commits)
75215

76216
print("\nGenerated Changelog:")
77217
print(changelog_content)
@@ -84,5 +224,4 @@ def update_changelog(version, changelog_content):
84224
new_version = input("Enter the new version (e.g., 1.0.0): ").strip()
85225

86226
update_changelog(new_version, changelog_content)
87-
88227
print(f"Changelog updated for version {new_version}!")

src/main/java/com/digitalsanctuary/cf/turnstile/TurnstileConfiguration.java

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,23 @@
1010
import com.digitalsanctuary.cf.turnstile.config.TurnstileHealthIndicator;
1111
import com.digitalsanctuary.cf.turnstile.config.TurnstileMetricsConfig;
1212
import com.digitalsanctuary.cf.turnstile.config.TurnstileServiceConfig;
13-
13+
import com.digitalsanctuary.cf.turnstile.filter.TurnstileCaptchaFilter;
1414
import io.micrometer.core.instrument.MeterRegistry;
1515
import jakarta.annotation.PostConstruct;
1616
import lombok.extern.slf4j.Slf4j;
1717

1818
/**
1919
* Main auto-configuration class for the Spring Cloudflare Turnstile integration.
2020
* <p>
21-
* This class serves as the entry point for Spring Boot's auto-configuration mechanism to
22-
* automatically set up Cloudflare Turnstile integration when the library is included in a project.
23-
* It imports the necessary configuration components such as property management, service configuration,
24-
* and metrics/monitoring configuration.
21+
* This class serves as the entry point for Spring Boot's auto-configuration mechanism to automatically set up Cloudflare Turnstile integration when
22+
* the library is included in a project. It imports the necessary configuration components such as property management, service configuration, and
23+
* metrics/monitoring configuration.
2524
* </p>
2625
* <p>
27-
* To use this auto-configuration, include this library in your Spring Boot project and configure
28-
* the required properties in your application.yml or application.properties file:
26+
* To use this auto-configuration, include this library in your Spring Boot project and configure the required properties in your application.yml or
27+
* application.properties file:
2928
* </p>
29+
*
3030
* <pre>
3131
* ds:
3232
* cf:
@@ -40,10 +40,10 @@
4040
* error-threshold: 10
4141
* </pre>
4242
* <p>
43-
* The {@link #onStartup()} method is annotated with {@link jakarta.annotation.PostConstruct} and is executed
44-
* after the bean initialization to log a confirmation message that the Cloudflare Turnstile Service has been loaded.
43+
* The {@link #onStartup()} method is annotated with {@link jakarta.annotation.PostConstruct} and is executed after the bean initialization to log a
44+
* confirmation message that the Cloudflare Turnstile Service has been loaded.
4545
* </p>
46-
*
46+
*
4747
* @see com.digitalsanctuary.cf.turnstile.config.TurnstileConfigProperties
4848
* @see com.digitalsanctuary.cf.turnstile.config.TurnstileServiceConfig
4949
* @see com.digitalsanctuary.cf.turnstile.service.TurnstileValidationService
@@ -53,15 +53,11 @@
5353
@Slf4j
5454
@Configuration
5555
@AutoConfiguration
56-
@Import({
57-
TurnstileServiceConfig.class,
58-
TurnstileConfigProperties.class
59-
})
56+
@Import({TurnstileServiceConfig.class, TurnstileConfigProperties.class, TurnstileCaptchaFilter.class})
6057
public class TurnstileConfiguration {
6158

6259
/**
63-
* Metrics configuration for Turnstile.
64-
* Only imported if MeterRegistry is available on the classpath.
60+
* Metrics configuration for Turnstile. Only imported if MeterRegistry is available on the classpath.
6561
*/
6662
@Configuration
6763
@ConditionalOnClass(MeterRegistry.class)
@@ -70,8 +66,7 @@ static class TurnstileMetricsConfiguration {
7066
}
7167

7268
/**
73-
* Health indicator configuration for Turnstile.
74-
* Only imported if Spring Actuator health is enabled.
69+
* Health indicator configuration for Turnstile. Only imported if Spring Actuator health is enabled.
7570
*/
7671
@Configuration
7772
@ConditionalOnEnabledHealthIndicator("turnstile")

0 commit comments

Comments
 (0)