Refactor code for improved readability and consistency

- Updated CSRF middleware to enhance cookie value decoding.
- Reformatted OAuth proxy token store initialization for better clarity.
- Adjusted Challonge proxy router for consistent line breaks and readability.
- Enhanced OAuth router error handling and response formatting.
- Improved session router for better readability and consistency in fetching provider records.
- Refactored OAuth token store to improve key derivation logging.
- Cleaned up cookie options utility for better readability.
- Enhanced Challonge client credentials composable for consistent API calls.
- Streamlined OAuth composable for improved logging.
- Refactored main.js for better readability in session initialization.
- Improved Challonge v2.1 service error handling for better clarity.
- Cleaned up API client utility for improved readability.
- Enhanced ApiKeyManager.vue for better text formatting.
- Refactored ChallongeTest.vue for improved readability in composable usage.
This commit is contained in:
2026-02-03 12:50:25 -05:00
parent 700c1cbbbe
commit 8775f8b1fe
15 changed files with 182 additions and 76 deletions

File diff suppressed because one or more lines are too long

View File

@@ -47,16 +47,22 @@ export function csrfMiddleware(options = {}) {
// current '/' path). cookie-parser will pick one value, but the browser may
// send both. Accept if the header matches ANY provided cookie value.
const rawHeader = req.headers?.cookie || '';
const rawValues = getCookieValuesFromHeader(rawHeader, cookieName).map(v => {
try {
return decodeURIComponent(v);
} catch {
return v;
const rawValues = getCookieValuesFromHeader(rawHeader, cookieName).map(
v => {
try {
return decodeURIComponent(v);
} catch {
return v;
}
}
});
);
const anyMatch = csrfHeader && rawValues.includes(csrfHeader);
if (!csrfHeader || (!csrfCookie && !anyMatch) || (csrfCookie !== csrfHeader && !anyMatch)) {
if (
!csrfHeader ||
(!csrfCookie && !anyMatch) ||
(csrfCookie !== csrfHeader && !anyMatch)
) {
return res.status(403).json({
error: 'CSRF validation failed',
code: 'CSRF_FAILED'

View File

@@ -60,7 +60,9 @@ app.use(
);
// Encrypted per-session provider token store
const tokenStore = createOAuthTokenStore({ sessionSecret: config.session.secret });
const tokenStore = createOAuthTokenStore({
sessionSecret: config.session.secret
});
// Mount API routes (nginx strips /api/ prefix before forwarding)
app.use('/gamemaster', gamemasterRouter);

View File

@@ -69,7 +69,8 @@ export function createChallongeProxyRouter({ config, tokenStore }) {
return res.status(500).json({ error: 'SID middleware not configured' });
}
const challongeRecord = (await tokenStore.getProviderRecord(req.sid, 'challonge')) || {};
const challongeRecord =
(await tokenStore.getProviderRecord(req.sid, 'challonge')) || {};
// Determine upstream path relative to this router mount
// This router is mounted at /challonge, so req.url starts with /v1/... or /v2.1/...
@@ -110,7 +111,8 @@ export function createChallongeProxyRouter({ config, tokenStore }) {
const app = challongeRecord.client_credentials;
if (!app?.client_id || !app?.client_secret) {
return res.status(401).json({
error: 'Challonge client credentials not configured for this session',
error:
'Challonge client credentials not configured for this session',
code: 'CHALLONGE_CLIENT_CREDENTIALS_REQUIRED'
});
}
@@ -130,7 +132,11 @@ export function createChallongeProxyRouter({ config, tokenStore }) {
expires_at: computeExpiresAt(exchanged.expires_in)
};
await tokenStore.setProviderRecord(req.sid, 'challonge', challongeRecord);
await tokenStore.setProviderRecord(
req.sid,
'challonge',
challongeRecord
);
accessToken = challongeRecord.client_credentials.access_token;
}
@@ -158,7 +164,11 @@ export function createChallongeProxyRouter({ config, tokenStore }) {
}
let accessToken = user.access_token;
if (isExpired(user.expires_at) && user.refresh_token && config.challonge.configured) {
if (
isExpired(user.expires_at) &&
user.refresh_token &&
config.challonge.configured
) {
try {
const refreshed = await refreshUserOAuth({
config,
@@ -172,7 +182,11 @@ export function createChallongeProxyRouter({ config, tokenStore }) {
scope: refreshed.scope,
expires_at: computeExpiresAt(refreshed.expires_in)
};
await tokenStore.setProviderRecord(req.sid, 'challonge', challongeRecord);
await tokenStore.setProviderRecord(
req.sid,
'challonge',
challongeRecord
);
accessToken = challongeRecord.user_oauth.access_token;
} catch (err) {
logger.warn('Failed to refresh Challonge user OAuth token', {
@@ -213,10 +227,13 @@ export function createChallongeProxyRouter({ config, tokenStore }) {
) {
const apiKey = challongeRecord.api_key?.token;
if (apiKey) {
logger.warn('Challonge v2.1 user OAuth unauthorized; retrying with API key', {
status: upstreamResponse.status,
path: upstreamPath
});
logger.warn(
'Challonge v2.1 user OAuth unauthorized; retrying with API key',
{
status: upstreamResponse.status,
path: upstreamPath
}
);
const retryHeaders = { ...headers };
delete retryHeaders.authorization;

View File

@@ -119,13 +119,18 @@ export function createOAuthRouter({ config, tokenStore }) {
}
if (!code) {
return res.status(400).json({ error: 'Authorization code is required', code: 'MISSING_CODE' });
return res.status(400).json({
error: 'Authorization code is required',
code: 'MISSING_CODE'
});
}
if (provider === 'discord') {
const clientId = process.env.VITE_DISCORD_CLIENT_ID;
const clientSecret = process.env.DISCORD_CLIENT_SECRET;
const redirectUri = process.env.DISCORD_REDIRECT_URI || process.env.VITE_DISCORD_REDIRECT_URI;
const redirectUri =
process.env.DISCORD_REDIRECT_URI ||
process.env.VITE_DISCORD_REDIRECT_URI;
if (!clientId || !clientSecret || !redirectUri) {
return res.status(503).json({
@@ -155,7 +160,10 @@ export function createOAuthRouter({ config, tokenStore }) {
}
if (!response.ok) {
logger.warn('Discord token exchange failed', { status: response.status, payload });
logger.warn('Discord token exchange failed', {
status: response.status,
payload
});
return res.status(response.status).json({
error: 'Discord token exchange failed',
code: 'DISCORD_TOKEN_EXCHANGE_FAILED',
@@ -197,7 +205,10 @@ export function createOAuthRouter({ config, tokenStore }) {
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
logger.warn('Challonge token exchange failed', { status: response.status, payload });
logger.warn('Challonge token exchange failed', {
status: response.status,
payload
});
return res.status(response.status).json({
error: 'Challonge token exchange failed',
code: 'CHALLONGE_TOKEN_EXCHANGE_FAILED',
@@ -205,7 +216,8 @@ export function createOAuthRouter({ config, tokenStore }) {
});
}
const existing = (await tokenStore.getProviderRecord(req.sid, 'challonge')) || {};
const existing =
(await tokenStore.getProviderRecord(req.sid, 'challonge')) || {};
const user_oauth = {
access_token: payload.access_token,
refresh_token: payload.refresh_token,
@@ -223,7 +235,10 @@ export function createOAuthRouter({ config, tokenStore }) {
return res.json(redactProviderRecord('challonge', record));
}
return res.status(400).json({ error: `Unknown provider: ${provider}`, code: 'UNKNOWN_PROVIDER' });
return res.status(400).json({
error: `Unknown provider: ${provider}`,
code: 'UNKNOWN_PROVIDER'
});
});
// Store Challonge API key (v1 compatibility) per session
@@ -233,7 +248,9 @@ export function createOAuthRouter({ config, tokenStore }) {
return res.status(500).json({ error: 'SID middleware not configured' });
}
if (!apiKey) {
return res.status(400).json({ error: 'apiKey is required', code: 'MISSING_API_KEY' });
return res
.status(400)
.json({ error: 'apiKey is required', code: 'MISSING_API_KEY' });
}
apiKey = String(apiKey).trim();
@@ -241,10 +258,13 @@ export function createOAuthRouter({ config, tokenStore }) {
apiKey = apiKey.slice('bearer '.length).trim();
}
if (!apiKey) {
return res.status(400).json({ error: 'apiKey is required', code: 'MISSING_API_KEY' });
return res
.status(400)
.json({ error: 'apiKey is required', code: 'MISSING_API_KEY' });
}
const existing = (await tokenStore.getProviderRecord(req.sid, 'challonge')) || {};
const existing =
(await tokenStore.getProviderRecord(req.sid, 'challonge')) || {};
const record = {
...existing,
api_key: {
@@ -260,7 +280,8 @@ export function createOAuthRouter({ config, tokenStore }) {
return res.status(500).json({ error: 'SID middleware not configured' });
}
const existing = (await tokenStore.getProviderRecord(req.sid, 'challonge')) || {};
const existing =
(await tokenStore.getProviderRecord(req.sid, 'challonge')) || {};
const record = { ...existing };
if (record.api_key) delete record.api_key;
await tokenStore.setProviderRecord(req.sid, 'challonge', record);
@@ -278,7 +299,8 @@ export function createOAuthRouter({ config, tokenStore }) {
if (typeof clientSecret === 'string') clientSecret = clientSecret.trim();
if (typeof scope === 'string') scope = scope.trim();
const existing = (await tokenStore.getProviderRecord(req.sid, 'challonge')) || {};
const existing =
(await tokenStore.getProviderRecord(req.sid, 'challonge')) || {};
const prev = existing.client_credentials || {};
const effectiveClientId = clientId || prev.client_id;
const effectiveClientSecret = clientSecret || prev.client_secret;
@@ -286,7 +308,8 @@ export function createOAuthRouter({ config, tokenStore }) {
if (!effectiveClientId || !effectiveClientSecret) {
return res.status(400).json({
error: 'clientId and clientSecret are required (or must already be stored for this session)',
error:
'clientId and clientSecret are required (or must already be stored for this session)',
code: 'MISSING_CLIENT_CREDENTIALS'
});
}
@@ -304,7 +327,10 @@ export function createOAuthRouter({ config, tokenStore }) {
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
logger.warn('Challonge client_credentials token exchange failed', { status: response.status, payload });
logger.warn('Challonge client_credentials token exchange failed', {
status: response.status,
payload
});
return res.status(response.status).json({
error: 'Challonge client credentials exchange failed',
code: 'CHALLONGE_CLIENT_CREDENTIALS_FAILED',
@@ -333,7 +359,8 @@ export function createOAuthRouter({ config, tokenStore }) {
return res.status(500).json({ error: 'SID middleware not configured' });
}
const existing = (await tokenStore.getProviderRecord(req.sid, 'challonge')) || {};
const existing =
(await tokenStore.getProviderRecord(req.sid, 'challonge')) || {};
const record = { ...existing };
if (record.client_credentials) delete record.client_credentials;
await tokenStore.setProviderRecord(req.sid, 'challonge', record);
@@ -346,7 +373,8 @@ export function createOAuthRouter({ config, tokenStore }) {
return res.status(500).json({ error: 'SID middleware not configured' });
}
const existing = (await tokenStore.getProviderRecord(req.sid, 'challonge')) || {};
const existing =
(await tokenStore.getProviderRecord(req.sid, 'challonge')) || {};
const creds = existing.client_credentials;
if (!creds) {
return res.json(redactProviderRecord('challonge', existing));
@@ -373,19 +401,27 @@ export function createOAuthRouter({ config, tokenStore }) {
const record = await tokenStore.getProviderRecord(req.sid, provider);
if (!record) {
return res.status(400).json({ error: 'No stored tokens', code: 'NO_TOKENS' });
return res
.status(400)
.json({ error: 'No stored tokens', code: 'NO_TOKENS' });
}
if (provider === 'discord') {
const refreshToken = record.refresh_token;
if (!refreshToken) {
return res.status(400).json({ error: 'No refresh token available', code: 'NO_REFRESH_TOKEN' });
return res.status(400).json({
error: 'No refresh token available',
code: 'NO_REFRESH_TOKEN'
});
}
const clientId = process.env.VITE_DISCORD_CLIENT_ID;
const clientSecret = process.env.DISCORD_CLIENT_SECRET;
if (!clientId || !clientSecret) {
return res.status(503).json({ error: 'Discord OAuth not configured', code: 'DISCORD_NOT_CONFIGURED' });
return res.status(503).json({
error: 'Discord OAuth not configured',
code: 'DISCORD_NOT_CONFIGURED'
});
}
const response = await fetch('https://discord.com/api/oauth2/token', {
@@ -473,7 +509,10 @@ export function createOAuthRouter({ config, tokenStore }) {
return res.json(redactProviderRecord('challonge', updatedRecord));
}
return res.status(400).json({ error: `Unknown provider: ${provider}`, code: 'UNKNOWN_PROVIDER' });
return res.status(400).json({
error: `Unknown provider: ${provider}`,
code: 'UNKNOWN_PROVIDER'
});
});
return router;

View File

@@ -1,6 +1,10 @@
import express from 'express';
import fetch from 'node-fetch';
import { COOKIE_NAMES, getCsrfCookieOptions, generateToken } from '../utils/cookie-options.js';
import {
COOKIE_NAMES,
getCsrfCookieOptions,
generateToken
} from '../utils/cookie-options.js';
export function createSessionRouter({ config, tokenStore }) {
const router = express.Router();
@@ -75,7 +79,8 @@ export function createSessionRouter({ config, tokenStore }) {
return res.status(500).json({ error: 'SID middleware not configured' });
}
const challonge = (await tokenStore.getProviderRecord(req.sid, 'challonge')) || {};
const challonge =
(await tokenStore.getProviderRecord(req.sid, 'challonge')) || {};
return res.json({
sid: req.sid,
@@ -83,9 +88,13 @@ export function createSessionRouter({ config, tokenStore }) {
hasApiKey: !!challonge.api_key?.token,
hasUserOAuth: !!challonge.user_oauth?.access_token,
userOAuthExpiresAt: challonge.user_oauth?.expires_at || null,
hasClientCredentials: !!(challonge.client_credentials?.client_id && challonge.client_credentials?.client_secret),
hasClientCredentials: !!(
challonge.client_credentials?.client_id &&
challonge.client_credentials?.client_secret
),
hasClientCredentialsToken: !!challonge.client_credentials?.access_token,
clientCredentialsExpiresAt: challonge.client_credentials?.expires_at || null
clientCredentialsExpiresAt:
challonge.client_credentials?.expires_at || null
}
});
});
@@ -96,17 +105,23 @@ export function createSessionRouter({ config, tokenStore }) {
return res.status(500).json({ error: 'SID middleware not configured' });
}
const challonge = (await tokenStore.getProviderRecord(req.sid, 'challonge')) || {};
const challonge =
(await tokenStore.getProviderRecord(req.sid, 'challonge')) || {};
const base = 'https://api.challonge.com/v2.1/tournaments.json?page=1&per_page=1&state=pending';
const base =
'https://api.challonge.com/v2.1/tournaments.json?page=1&per_page=1&state=pending';
const results = {
sid: req.sid,
endpoints: {
userTournamentsSample: base,
appTournamentsSample: 'https://api.challonge.com/v2.1/application/tournaments.json?page=1&per_page=1&state=pending'
appTournamentsSample:
'https://api.challonge.com/v2.1/application/tournaments.json?page=1&per_page=1&state=pending'
},
methods: {
user_oauth: { present: !!challonge.user_oauth?.access_token, probe: null },
user_oauth: {
present: !!challonge.user_oauth?.access_token,
probe: null
},
api_key: { present: !!challonge.api_key?.token, probe: null },
client_credentials: {
present: !!challonge.client_credentials?.access_token,

View File

@@ -27,7 +27,9 @@ function getEncryptionKey(sessionSecret) {
}
// Dev fallback: derive from session secret (still better than plaintext)
logger.warn('OAUTH_TOKEN_ENC_KEY not set; deriving key from SESSION_SECRET (dev only).');
logger.warn(
'OAUTH_TOKEN_ENC_KEY not set; deriving key from SESSION_SECRET (dev only).'
);
return crypto.createHash('sha256').update(sessionSecret).digest();
}
@@ -149,7 +151,10 @@ export function createOAuthTokenStore({ sessionSecret }) {
const ts = now();
if (existing) {
existing.lastSeenAt = ts;
existing.expiresAt = Math.min(existing.createdAt + SEVEN_DAYS_MS, ts + ONE_DAY_MS);
existing.expiresAt = Math.min(
existing.createdAt + SEVEN_DAYS_MS,
ts + ONE_DAY_MS
);
return existing;
}

View File

@@ -9,10 +9,12 @@ export const COOKIE_NAMES = {
};
export function getCookieSecurityConfig(config) {
const deploymentTarget = config?.deploymentTarget || process.env.DEPLOYMENT_TARGET;
const deploymentTarget =
config?.deploymentTarget || process.env.DEPLOYMENT_TARGET;
const nodeEnv = config?.nodeEnv || process.env.NODE_ENV;
const isProdTarget = deploymentTarget === 'production' || nodeEnv === 'production';
const isProdTarget =
deploymentTarget === 'production' || nodeEnv === 'production';
return {
secure: isProdTarget,