Skip to content

Commit d728e49

Browse files
committed
Updates to add csrf and resolve review messages
1 parent 277e753 commit d728e49

File tree

10 files changed

+102
-25
lines changed

10 files changed

+102
-25
lines changed

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
projectVersion=4.1.16
1+
projectVersion=4.1.17
22
org.gradle.configuration-cache=false

src/frontend/src/App.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
handleOAuthRedirect,
1717
type DiscordUser
1818
} from './utils/auth';
19-
import { getAuthHeaders, fetchWithAuth } from './utils/api';
19+
import { getAuthHeaders, fetchWithAuth, fetchCsrfToken, getAuthHeadersWithCsrf } from './utils/api';
2020
import { toast, Toaster } from 'sonner@2.0.3';
2121

2222
interface Sound {
@@ -147,6 +147,9 @@ export default function App() {
147147
}
148148
setAuthLoading(false);
149149
}
150+
151+
// Fetch CSRF token
152+
await fetchCsrfToken();
150153
};
151154

152155
handleCallback();
@@ -573,7 +576,7 @@ export default function App() {
573576
mode: 'cors',
574577
headers: {
575578
'Content-Type': 'application/json',
576-
...getAuthHeaders()
579+
...getAuthHeadersWithCsrf()
577580
}
578581
}
579582
);
@@ -631,7 +634,7 @@ export default function App() {
631634
mode: 'cors',
632635
headers: {
633636
'Content-Type': 'application/json',
634-
...getAuthHeaders()
637+
...getAuthHeadersWithCsrf()
635638
}
636639
}
637640
);
@@ -716,7 +719,7 @@ export default function App() {
716719
{
717720
method: 'POST',
718721
mode: 'cors',
719-
headers: getAuthHeaders()
722+
headers: getAuthHeadersWithCsrf()
720723
}
721724
);
722725

@@ -799,7 +802,7 @@ export default function App() {
799802
{
800803
method: 'POST',
801804
mode: 'cors',
802-
headers: getAuthHeaders()
805+
headers: getAuthHeadersWithCsrf()
803806
}
804807
);
805808

@@ -832,7 +835,7 @@ export default function App() {
832835
{
833836
method: 'POST',
834837
mode: 'cors',
835-
headers: getAuthHeaders()
838+
headers: getAuthHeadersWithCsrf()
836839
}
837840
);
838841

@@ -881,7 +884,7 @@ export default function App() {
881884
console.log('📤 Uploading file:', file.name);
882885

883886
// Get auth headers
884-
const authHeaders = getAuthHeaders();
887+
const authHeaders = getAuthHeadersWithCsrf();
885888
console.log('📋 Auth headers:', authHeaders);
886889

887890
const response = await fetch(API_ENDPOINTS.UPLOAD, {

src/frontend/src/components/UsersOverlay.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useState, useEffect } from 'react';
22
import { X, ChevronLeft, ChevronRight, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react';
33
import { API_ENDPOINTS } from '../config';
4+
import { fetchWithAuth, getAuthHeaders } from '../utils/api';
45

56
interface DiscordUser {
67
id: string;
@@ -50,7 +51,7 @@ export function UsersOverlay({ isOpen, onClose, theme, sounds }: UsersOverlayPro
5051
params.append('sortDir', sortDirection);
5152
}
5253

53-
const response = await fetch(
54+
const response = await fetchWithAuth(
5455
`${API_ENDPOINTS.DISCORD_USERS}?${params.toString()}`,
5556
{ mode: 'cors' }
5657
);
@@ -125,7 +126,7 @@ export function UsersOverlay({ isOpen, onClose, theme, sounds }: UsersOverlayPro
125126
}
126127

127128
// Call the Spring Boot PATCH endpoint with query parameters
128-
const response = await fetch(
129+
const response = await fetchWithAuth(
129130
`${API_ENDPOINTS.DISCORD_USERS}/${userId}?${params.toString()}`,
130131
{
131132
method: 'PATCH',

src/frontend/src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const API_ENDPOINTS = {
2828
AUTH_USER: `${API_BASE_URL}/api/auth/user`,
2929
AUTH_LOGOUT: `${API_BASE_URL}/api/auth/logout`,
3030
OAUTH_LOGIN: `${API_BASE_URL}/oauth2/authorization/discord`,
31+
CSRF_TOKEN: `${API_BASE_URL}/api/auth/csrf-token`,
3132
// Bot version
3233
BOT_VERSION: `${API_BASE_URL}/bot/version`,
3334
};

src/frontend/src/utils/api.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,45 @@
11
import { loadAuth } from './auth';
22

3+
// Store CSRF token in memory
4+
let csrfToken: string | null = null;
5+
6+
/**
7+
* Fetch CSRF token from the backend
8+
*/
9+
export async function fetchCsrfToken(): Promise<string | null> {
10+
try {
11+
const response = await fetch('/api/auth/csrf-token', {
12+
credentials: 'include', // Include cookies
13+
});
14+
15+
if (response.ok) {
16+
const data = await response.json();
17+
csrfToken = data.token;
18+
return csrfToken;
19+
}
20+
} catch (error) {
21+
console.error('Failed to fetch CSRF token:', error);
22+
}
23+
24+
return null;
25+
}
26+
27+
/**
28+
* Get the current CSRF token
29+
*/
30+
export function getCsrfToken(): string | null {
31+
return csrfToken;
32+
}
33+
34+
/**
35+
* Check if the request method requires CSRF token
36+
*/
37+
function requiresCsrfToken(method?: string): boolean {
38+
if (!method) return false;
39+
const upperMethod = method.toUpperCase();
40+
return ['POST', 'PUT', 'DELETE', 'PATCH'].includes(upperMethod);
41+
}
42+
343
/**
444
* Make an authenticated API request with the JWT token
545
*/
@@ -15,6 +55,11 @@ export async function fetchWithAuth(url: string, options: RequestInit = {}): Pro
1555
headers['Authorization'] = `Bearer ${auth.accessToken}`;
1656
}
1757

58+
// Add CSRF token for mutation requests
59+
if (requiresCsrfToken(options.method) && csrfToken) {
60+
headers['X-CSRF-TOKEN'] = csrfToken;
61+
}
62+
1863
return fetch(url, {
1964
...options,
2065
headers,
@@ -47,3 +92,16 @@ export function getAuthHeaders(): HeadersInit {
4792

4893
return headers;
4994
}
95+
96+
/**
97+
* Get auth headers including CSRF token for mutation requests
98+
*/
99+
export function getAuthHeadersWithCsrf(): HeadersInit {
100+
const headers = getAuthHeaders();
101+
102+
if (csrfToken) {
103+
headers['X-CSRF-TOKEN'] = csrfToken;
104+
}
105+
106+
return headers;
107+
}

src/frontend/src/utils/auth.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { API_ENDPOINTS } from '../config';
2+
import { getCsrfToken } from './api';
23

34
export interface DiscordUser {
45
id: string;
@@ -188,14 +189,21 @@ export async function validateToken(accessToken: string): Promise<DiscordUser |
188189

189190
export async function logout(accessToken: string): Promise<void> {
190191
try {
192+
const headers: Record<string, string> = {
193+
'Authorization': `Bearer ${accessToken}`
194+
};
195+
196+
const csrfToken = getCsrfToken();
197+
if (csrfToken) {
198+
headers['X-CSRF-TOKEN'] = csrfToken;
199+
}
200+
191201
await fetch(`${API_ENDPOINTS.AUTH_LOGOUT}`, {
192202
method: 'POST',
193-
headers: {
194-
'Authorization': `Bearer ${accessToken}`
195-
}
203+
headers
196204
});
197205
} catch {
198206
// Ignore errors during logout
199207
}
200208
clearAuth();
201-
}
209+
}

src/main/java/net/dirtydeeds/discordsoundboard/config/SecurityConfig.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
2828
.invalidateHttpSession(true)
2929
.deleteCookies("JSESSIONID")
3030
)
31-
.csrf(AbstractHttpConfigurer::disable)
3231
.authorizeHttpRequests((authorizeHttpRequests) ->
3332
authorizeHttpRequests
3433
.requestMatchers("/**").permitAll().anyRequest().authenticated()

src/main/java/net/dirtydeeds/discordsoundboard/controllers/AuthController.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import net.dirtydeeds.discordsoundboard.util.JwtUtil;
55
import org.springframework.beans.factory.annotation.Autowired;
66
import org.springframework.http.ResponseEntity;
7+
import org.springframework.security.web.csrf.CsrfToken;
78
import org.springframework.stereotype.Controller;
89
import org.springframework.web.bind.annotation.*;
910

@@ -63,6 +64,12 @@ public ResponseEntity<Map<String, Object>> getUser(@RequestHeader("Authorization
6364
}
6465
}
6566

67+
@GetMapping("/csrf-token")
68+
@ResponseBody
69+
public CsrfToken csrfToken(CsrfToken token) {
70+
return token;
71+
}
72+
6673
@PostMapping("/logout")
6774
public ResponseEntity<Void> logout(@RequestHeader("Authorization") String authorization) {
6875
return ResponseEntity.ok().build();

src/main/java/net/dirtydeeds/discordsoundboard/controllers/DiscordUserController.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,13 @@ public Page<DiscordUser> getInvoiceOrSelected(@RequestParam(defaultValue = "0")
7777

7878
@PatchMapping("/{userId}")
7979
public ResponseEntity<DiscordUser> updateUserSounds(
80-
@PathVariable String userId,
81-
@RequestParam(required = false) String entranceSound,
82-
@RequestParam(required = false) String leaveSound,
83-
@RequestHeader(value = "Authorization", required = false) String authorization
84-
) {
80+
@PathVariable String userId,
81+
@RequestParam(required = false) String entranceSound,
82+
@RequestParam(required = false) String leaveSound,
83+
@RequestHeader(value = "Authorization", required = false) String authorization) {
8584

8685
String authId = userRoleConfig.getUserIdFromAuth(authorization);
87-
if (userId == null || !userRoleConfig.hasPermission(userId, "manage-users")) {
86+
if (userId == null || !userRoleConfig.hasPermission(authId, "manage-users")) {
8887
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
8988
}
9089

src/main/java/net/dirtydeeds/discordsoundboard/controllers/SoundController.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.springframework.web.bind.annotation.*;
2323
import org.springframework.web.multipart.MultipartFile;
2424
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
25+
import org.springframework.web.util.HtmlUtils;
2526

2627
import java.io.File;
2728
import java.io.IOException;
@@ -44,15 +45,15 @@
4445
* @author dfurrer.
4546
*/
4647
@Hidden
47-
48-
private static final Logger log = LoggerFactory.getLogger(SoundController.class);
4948
@RestController
5049
@RequestMapping("/api/soundFiles")
5150
@SuppressWarnings("unused")
5251
public class SoundController {
5352

5453
private static final long EMITTER_TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(5);
5554

55+
private static final Logger log = LoggerFactory.getLogger(SoundController.class);
56+
5657
@Autowired
5758
private final UserRoleConfig userRoleConfig;
5859
@Setter
@@ -257,11 +258,11 @@ public ResponseEntity<String> uploadFile(
257258
file.transferTo(new File(filePath));
258259

259260
soundService.save(new SoundFile(originalFilename, filePath, "", 0, ZonedDateTime.now(), false, null, 0));
260-
log.error("Failed to upload file", e);
261+
log.error("Failed to upload file");
261262

262263
broadcastUpdate();
263264

264-
return ResponseEntity.ok("File uploaded successfully: " + originalFilename);
265+
return ResponseEntity.ok("File uploaded successfully: " + HtmlUtils.htmlEscape(originalFilename));
265266

266267
} catch (Exception e) {
267268
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)

0 commit comments

Comments
 (0)