Skip to content

Commit 32f982d

Browse files
committed
Trying to make it so you don't have to auth in the discord application every time.
1 parent 50b4b7d commit 32f982d

File tree

5 files changed

+117
-7
lines changed

5 files changed

+117
-7
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.51
1+
projectVersion=4.1.52
22
org.gradle.configuration-cache=false

src/frontend/src/hooks/useAuth.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect } from 'react';
1+
import { useState, useEffect, useCallback } from 'react';
22
import {
33
loadAuth,
44
initiateDiscordLogin,
@@ -8,13 +8,71 @@ import {
88
handleOAuthRedirect,
99
refreshToken,
1010
fetchDefaultPermissions,
11+
isTokenExpired,
12+
getTokenExpirationTime,
1113
type DiscordUser
1214
} from '../utils/auth';
15+
import { TOKEN_EXPIRED_EVENT } from '../utils/api';
1316

1417
export function useAuth() {
1518
const [authUser, setAuthUser] = useState<DiscordUser | null>(null);
1619
const [authLoading, setAuthLoading] = useState<boolean>(true);
1720

21+
// Handle token expiration - switch to guest user
22+
const handleTokenExpired = useCallback(async () => {
23+
const guestUser = await fetchDefaultPermissions();
24+
setAuthUser(guestUser);
25+
}, []);
26+
27+
// Listen for token expired events from API calls
28+
useEffect(() => {
29+
const handleExpiredEvent = () => {
30+
handleTokenExpired();
31+
};
32+
33+
window.addEventListener(TOKEN_EXPIRED_EVENT, handleExpiredEvent);
34+
return () => {
35+
window.removeEventListener(TOKEN_EXPIRED_EVENT, handleExpiredEvent);
36+
};
37+
}, [handleTokenExpired]);
38+
39+
// Periodic token expiration check
40+
useEffect(() => {
41+
const checkTokenExpiration = () => {
42+
const storedAuth = loadAuth();
43+
if (storedAuth.accessToken && isTokenExpired(storedAuth.accessToken)) {
44+
clearAuth();
45+
handleTokenExpired();
46+
}
47+
};
48+
49+
// Check every minute
50+
const interval = setInterval(checkTokenExpiration, 60000);
51+
52+
// Also set up a timer for exact expiration time
53+
const storedAuth = loadAuth();
54+
if (storedAuth.accessToken) {
55+
const expirationTime = getTokenExpirationTime(storedAuth.accessToken);
56+
if (expirationTime) {
57+
const timeUntilExpiry = expirationTime - Date.now();
58+
if (timeUntilExpiry > 0) {
59+
const timeout = setTimeout(() => {
60+
clearAuth();
61+
handleTokenExpired();
62+
}, timeUntilExpiry);
63+
return () => {
64+
clearInterval(interval);
65+
clearTimeout(timeout);
66+
};
67+
}
68+
}
69+
}
70+
71+
return () => {
72+
clearInterval(interval);
73+
};
74+
}, [authUser, handleTokenExpired]);
75+
1876
// Handle Discord OAuth callback
1977
useEffect(() => {
2078
const handleCallback = async () => {

src/frontend/src/utils/api.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
1-
import { loadAuth } from './auth';
1+
import { loadAuth, isTokenExpired, clearAuth } from './auth';
22
import Cookies from 'js-cookie';
33

4+
// Custom event for token expiration
5+
export const TOKEN_EXPIRED_EVENT = 'auth:token-expired';
6+
7+
/**
8+
* Dispatch token expired event to notify the app
9+
*/
10+
function dispatchTokenExpiredEvent(): void {
11+
window.dispatchEvent(new CustomEvent(TOKEN_EXPIRED_EVENT));
12+
}
13+
414
/**
515
* Get the CSRF token from cookie
616
*/
@@ -51,6 +61,13 @@ export function getAuthHeadersWithCsrf(): HeadersInit {
5161
export async function fetchWithAuth(url: string, options: RequestInit = {}): Promise<Response> {
5262
const auth = loadAuth();
5363

64+
// Check if token is expired before making request
65+
if (auth.accessToken && isTokenExpired(auth.accessToken)) {
66+
clearAuth();
67+
dispatchTokenExpiredEvent();
68+
throw new Error('Token expired');
69+
}
70+
5471
const headers: HeadersInit = {
5572
...options.headers,
5673
};
@@ -68,11 +85,19 @@ export async function fetchWithAuth(url: string, options: RequestInit = {}): Pro
6885
}
6986
}
7087

71-
return fetch(url, {
88+
const response = await fetch(url, {
7289
...options,
7390
credentials: 'include', // Always include cookies for CSRF token
7491
headers,
7592
});
93+
94+
// If we get a 401, the token is invalid/expired
95+
if (response.status === 401) {
96+
clearAuth();
97+
dispatchTokenExpiredEvent();
98+
}
99+
100+
return response;
76101
}
77102

78103
/**

src/frontend/src/utils/auth.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,40 @@ function decodeJWT(token: string): any {
8686
const payload = parts[1];
8787
const decoded = atob(payload);
8888
return JSON.parse(decoded);
89-
} catch (error) {
90-
console.error('Error decoding JWT:', error);
89+
} catch {
9190
return null;
9291
}
9392
}
9493

94+
/**
95+
* Check if a JWT token is expired
96+
* Returns true if expired or invalid, false if still valid
97+
*/
98+
export function isTokenExpired(token: string | null): boolean {
99+
if (!token) return true;
100+
101+
const payload = decodeJWT(token);
102+
if (!payload || !payload.exp) return true;
103+
104+
// exp is in seconds, Date.now() is in milliseconds
105+
// Add 30 second buffer to handle clock skew
106+
const expirationTime = payload.exp * 1000;
107+
return Date.now() >= expirationTime - 30000;
108+
}
109+
110+
/**
111+
* Get the expiration time of a JWT token in milliseconds
112+
* Returns null if token is invalid
113+
*/
114+
export function getTokenExpirationTime(token: string | null): number | null {
115+
if (!token) return null;
116+
117+
const payload = decodeJWT(token);
118+
if (!payload || !payload.exp) return null;
119+
120+
return payload.exp * 1000;
121+
}
122+
95123
export function saveAuth(authState: AuthState): void {
96124
localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(authState));
97125
}

src/main/resources/application.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ spring:
2323
authorizationGrantType: authorization_code
2424
scope:
2525
- identify
26-
- email
2726
redirectUri: "{baseUrl}/login/oauth2/code/{registrationId}"
2827
clientName: Discord Soundboard
2928
provider:

0 commit comments

Comments
 (0)