Skip to content

Commit 1baf01b

Browse files
committed
Add api.osable.net/questions controller.
Only return error pages on osable.net requests Add CSRF exception debugging for development Update frontend submodule
1 parent 1ce387e commit 1baf01b

File tree

6 files changed

+83
-2
lines changed

6 files changed

+83
-2
lines changed

core/src/main/kotlin/net/osable/core/SecurityConfiguration.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package net.osable.core
22

3+
import org.springframework.beans.factory.annotation.Autowired
34
import org.springframework.context.annotation.Bean
45
import org.springframework.context.annotation.Configuration
6+
import org.springframework.core.env.Environment
57
import org.springframework.http.HttpStatus
68
import org.springframework.security.config.annotation.web.builders.HttpSecurity
79
import org.springframework.security.web.SecurityFilterChain
@@ -15,6 +17,8 @@ import org.springframework.web.bind.annotation.RestController
1517
@Configuration
1618
class SecurityConfiguration {
1719

20+
@Autowired private lateinit var environment: Environment
21+
1822
@Bean fun filterChain(http: HttpSecurity): SecurityFilterChain {
1923
http.authorizeHttpRequests {
2024

@@ -33,6 +37,15 @@ class SecurityConfiguration {
3337
it.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
3438
}.oauth2Client()
3539

40+
41+
// Useful for debugging CSRF
42+
if (environment.activeProfiles.contains("development")) {
43+
http.exceptionHandling().accessDeniedHandler { request, response, accessDeniedException ->
44+
println("Access denied. Cause: ${accessDeniedException.cause} | Message: ${accessDeniedException.message}")
45+
accessDeniedException.printStackTrace()
46+
}
47+
}
48+
3649
return http.build()
3750
}
3851

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package net.osable.core.model
2+
3+
class ContactRequest(
4+
private val name: String,
5+
private val email: String,
6+
private val message: String
7+
) {
8+
9+
fun asContentString() = "Name: $name\nEmail: $email\nMessage: $message"
10+
11+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package net.osable.core.model
2+
3+
class DiscordRequestBody (
4+
val username: String = "OSable",
5+
val avatar_url: String = "https://cdn.discordapp.com/avatars/872531727908753428/c0640a6bf06abb467ca5392591c32c6b.webp",
6+
val content: String
7+
)

core/src/main/kotlin/net/osable/core/web/ErrorController.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package net.osable.core.web
22

33
import jakarta.servlet.http.HttpServletRequest
4+
import jakarta.servlet.http.HttpServletResponse
45
import net.osable.core.Error
56
import net.osable.core.getErrorCode
67
import net.osable.core.getHttpStatusMessage
@@ -11,8 +12,15 @@ import org.springframework.web.servlet.ModelAndView
1112
@Controller
1213
class ErrorController : org.springframework.boot.web.servlet.error.ErrorController {
1314

14-
@RequestMapping("/error") fun defaultErrorMapping(request: HttpServletRequest) = ModelAndView("templates/error").apply {
15+
@RequestMapping("/error", headers = ["Host=osable.net"])
16+
fun defaultErrorMapping(request: HttpServletRequest) = ModelAndView("templates/error").apply {
1517
addObject("error", Error(request.getErrorCode(), getHttpStatusMessage(request.getErrorCode())))
1618
}
1719

20+
@RequestMapping("/error")
21+
fun apiErrorMapping(request: HttpServletRequest, response: HttpServletResponse) {
22+
val errorCode = request.getErrorCode()
23+
response.sendError(errorCode, getHttpStatusMessage(errorCode))
24+
}
25+
1826
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package net.osable.core.web
2+
3+
import net.osable.core.model.ContactRequest
4+
import net.osable.core.model.DiscordRequestBody
5+
import org.springframework.beans.factory.annotation.Value
6+
import org.springframework.http.HttpHeaders
7+
import org.springframework.http.HttpStatus
8+
import org.springframework.http.MediaType
9+
import org.springframework.stereotype.Controller
10+
import org.springframework.web.bind.annotation.CrossOrigin
11+
import org.springframework.web.bind.annotation.ModelAttribute
12+
import org.springframework.web.bind.annotation.PostMapping
13+
import org.springframework.web.bind.annotation.ResponseStatus
14+
import org.springframework.web.reactive.function.client.WebClient
15+
16+
@Controller
17+
class FormController {
18+
19+
private val webClient = WebClient.builder()
20+
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
21+
.build()
22+
23+
@Value("#{environment.QUESTIONS_WEBHOOK_URL}")
24+
private lateinit var webhookURL: String
25+
26+
@PostMapping("/questions", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE], headers = ["Host=api.osable.net"])
27+
// Method has a void return type, spring MVC tries to find a /questions page to redirect to without this annotation
28+
@ResponseStatus(HttpStatus.OK)
29+
// Allow cross-origin requests from ourselves
30+
@CrossOrigin(origins = ["https://osable.net"])
31+
fun questionsContactRoute(@ModelAttribute contactRequest: ContactRequest) {
32+
webClient.post()
33+
.uri(webhookURL)
34+
.bodyValue(DiscordRequestBody(
35+
content = contactRequest.asContentString()
36+
))
37+
.retrieve()
38+
.toBodilessEntity()
39+
.subscribe { }
40+
}
41+
42+
}

core/src/main/resources/frontend

0 commit comments

Comments
 (0)