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 // current '/' path). cookie-parser will pick one value, but the browser may
// send both. Accept if the header matches ANY provided cookie value. // send both. Accept if the header matches ANY provided cookie value.
const rawHeader = req.headers?.cookie || ''; const rawHeader = req.headers?.cookie || '';
const rawValues = getCookieValuesFromHeader(rawHeader, cookieName).map(v => { const rawValues = getCookieValuesFromHeader(rawHeader, cookieName).map(
v => {
try { try {
return decodeURIComponent(v); return decodeURIComponent(v);
} catch { } catch {
return v; return v;
} }
}); }
);
const anyMatch = csrfHeader && rawValues.includes(csrfHeader); 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({ return res.status(403).json({
error: 'CSRF validation failed', error: 'CSRF validation failed',
code: 'CSRF_FAILED' code: 'CSRF_FAILED'

View File

@@ -60,7 +60,9 @@ app.use(
); );
// Encrypted per-session provider token store // 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) // Mount API routes (nginx strips /api/ prefix before forwarding)
app.use('/gamemaster', gamemasterRouter); 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' }); 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 // Determine upstream path relative to this router mount
// This router is mounted at /challonge, so req.url starts with /v1/... or /v2.1/... // 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; const app = challongeRecord.client_credentials;
if (!app?.client_id || !app?.client_secret) { if (!app?.client_id || !app?.client_secret) {
return res.status(401).json({ 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' code: 'CHALLONGE_CLIENT_CREDENTIALS_REQUIRED'
}); });
} }
@@ -130,7 +132,11 @@ export function createChallongeProxyRouter({ config, tokenStore }) {
expires_at: computeExpiresAt(exchanged.expires_in) 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; accessToken = challongeRecord.client_credentials.access_token;
} }
@@ -158,7 +164,11 @@ export function createChallongeProxyRouter({ config, tokenStore }) {
} }
let accessToken = user.access_token; 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 { try {
const refreshed = await refreshUserOAuth({ const refreshed = await refreshUserOAuth({
config, config,
@@ -172,7 +182,11 @@ export function createChallongeProxyRouter({ config, tokenStore }) {
scope: refreshed.scope, scope: refreshed.scope,
expires_at: computeExpiresAt(refreshed.expires_in) 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; accessToken = challongeRecord.user_oauth.access_token;
} catch (err) { } catch (err) {
logger.warn('Failed to refresh Challonge user OAuth token', { logger.warn('Failed to refresh Challonge user OAuth token', {
@@ -213,10 +227,13 @@ export function createChallongeProxyRouter({ config, tokenStore }) {
) { ) {
const apiKey = challongeRecord.api_key?.token; const apiKey = challongeRecord.api_key?.token;
if (apiKey) { if (apiKey) {
logger.warn('Challonge v2.1 user OAuth unauthorized; retrying with API key', { logger.warn(
'Challonge v2.1 user OAuth unauthorized; retrying with API key',
{
status: upstreamResponse.status, status: upstreamResponse.status,
path: upstreamPath path: upstreamPath
}); }
);
const retryHeaders = { ...headers }; const retryHeaders = { ...headers };
delete retryHeaders.authorization; delete retryHeaders.authorization;

View File

@@ -119,13 +119,18 @@ export function createOAuthRouter({ config, tokenStore }) {
} }
if (!code) { 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') { if (provider === 'discord') {
const clientId = process.env.VITE_DISCORD_CLIENT_ID; const clientId = process.env.VITE_DISCORD_CLIENT_ID;
const clientSecret = process.env.DISCORD_CLIENT_SECRET; 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) { if (!clientId || !clientSecret || !redirectUri) {
return res.status(503).json({ return res.status(503).json({
@@ -155,7 +160,10 @@ export function createOAuthRouter({ config, tokenStore }) {
} }
if (!response.ok) { 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({ return res.status(response.status).json({
error: 'Discord token exchange failed', error: 'Discord token exchange failed',
code: 'DISCORD_TOKEN_EXCHANGE_FAILED', code: 'DISCORD_TOKEN_EXCHANGE_FAILED',
@@ -197,7 +205,10 @@ export function createOAuthRouter({ config, tokenStore }) {
const payload = await response.json().catch(() => ({})); const payload = await response.json().catch(() => ({}));
if (!response.ok) { 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({ return res.status(response.status).json({
error: 'Challonge token exchange failed', error: 'Challonge token exchange failed',
code: '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 = { const user_oauth = {
access_token: payload.access_token, access_token: payload.access_token,
refresh_token: payload.refresh_token, refresh_token: payload.refresh_token,
@@ -223,7 +235,10 @@ export function createOAuthRouter({ config, tokenStore }) {
return res.json(redactProviderRecord('challonge', record)); 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 // 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' }); return res.status(500).json({ error: 'SID middleware not configured' });
} }
if (!apiKey) { 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(); apiKey = String(apiKey).trim();
@@ -241,10 +258,13 @@ export function createOAuthRouter({ config, tokenStore }) {
apiKey = apiKey.slice('bearer '.length).trim(); apiKey = apiKey.slice('bearer '.length).trim();
} }
if (!apiKey) { 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 = { const record = {
...existing, ...existing,
api_key: { api_key: {
@@ -260,7 +280,8 @@ export function createOAuthRouter({ config, tokenStore }) {
return res.status(500).json({ error: 'SID middleware not configured' }); 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 }; const record = { ...existing };
if (record.api_key) delete record.api_key; if (record.api_key) delete record.api_key;
await tokenStore.setProviderRecord(req.sid, 'challonge', record); 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 clientSecret === 'string') clientSecret = clientSecret.trim();
if (typeof scope === 'string') scope = scope.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 prev = existing.client_credentials || {};
const effectiveClientId = clientId || prev.client_id; const effectiveClientId = clientId || prev.client_id;
const effectiveClientSecret = clientSecret || prev.client_secret; const effectiveClientSecret = clientSecret || prev.client_secret;
@@ -286,7 +308,8 @@ export function createOAuthRouter({ config, tokenStore }) {
if (!effectiveClientId || !effectiveClientSecret) { if (!effectiveClientId || !effectiveClientSecret) {
return res.status(400).json({ 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' code: 'MISSING_CLIENT_CREDENTIALS'
}); });
} }
@@ -304,7 +327,10 @@ export function createOAuthRouter({ config, tokenStore }) {
const payload = await response.json().catch(() => ({})); const payload = await response.json().catch(() => ({}));
if (!response.ok) { 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({ return res.status(response.status).json({
error: 'Challonge client credentials exchange failed', error: 'Challonge client credentials exchange failed',
code: 'CHALLONGE_CLIENT_CREDENTIALS_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' }); 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 }; const record = { ...existing };
if (record.client_credentials) delete record.client_credentials; if (record.client_credentials) delete record.client_credentials;
await tokenStore.setProviderRecord(req.sid, 'challonge', record); 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' }); 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; const creds = existing.client_credentials;
if (!creds) { if (!creds) {
return res.json(redactProviderRecord('challonge', existing)); return res.json(redactProviderRecord('challonge', existing));
@@ -373,19 +401,27 @@ export function createOAuthRouter({ config, tokenStore }) {
const record = await tokenStore.getProviderRecord(req.sid, provider); const record = await tokenStore.getProviderRecord(req.sid, provider);
if (!record) { 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') { if (provider === 'discord') {
const refreshToken = record.refresh_token; const refreshToken = record.refresh_token;
if (!refreshToken) { 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 clientId = process.env.VITE_DISCORD_CLIENT_ID;
const clientSecret = process.env.DISCORD_CLIENT_SECRET; const clientSecret = process.env.DISCORD_CLIENT_SECRET;
if (!clientId || !clientSecret) { 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', { 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.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; return router;

View File

@@ -1,6 +1,10 @@
import express from 'express'; import express from 'express';
import fetch from 'node-fetch'; 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 }) { export function createSessionRouter({ config, tokenStore }) {
const router = express.Router(); const router = express.Router();
@@ -75,7 +79,8 @@ export function createSessionRouter({ config, tokenStore }) {
return res.status(500).json({ error: 'SID middleware not configured' }); 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({ return res.json({
sid: req.sid, sid: req.sid,
@@ -83,9 +88,13 @@ export function createSessionRouter({ config, tokenStore }) {
hasApiKey: !!challonge.api_key?.token, hasApiKey: !!challonge.api_key?.token,
hasUserOAuth: !!challonge.user_oauth?.access_token, hasUserOAuth: !!challonge.user_oauth?.access_token,
userOAuthExpiresAt: challonge.user_oauth?.expires_at || null, 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, 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' }); 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 = { const results = {
sid: req.sid, sid: req.sid,
endpoints: { endpoints: {
userTournamentsSample: base, 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: { 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 }, api_key: { present: !!challonge.api_key?.token, probe: null },
client_credentials: { client_credentials: {
present: !!challonge.client_credentials?.access_token, 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) // 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(); return crypto.createHash('sha256').update(sessionSecret).digest();
} }
@@ -149,7 +151,10 @@ export function createOAuthTokenStore({ sessionSecret }) {
const ts = now(); const ts = now();
if (existing) { if (existing) {
existing.lastSeenAt = ts; 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; return existing;
} }

View File

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

View File

@@ -51,11 +51,14 @@ export function useChallongeClientCredentials() {
loading.value = true; loading.value = true;
error.value = ''; error.value = '';
try { try {
status.value = await apiClient.post('/oauth/challonge/client-credentials', { status.value = await apiClient.post(
'/oauth/challonge/client-credentials',
{
clientId, clientId,
clientSecret, clientSecret,
scope scope
}); }
);
return true; return true;
} catch (err) { } catch (err) {
error.value = err.message || 'Failed to save credentials'; error.value = err.message || 'Failed to save credentials';
@@ -69,7 +72,10 @@ export function useChallongeClientCredentials() {
loading.value = true; loading.value = true;
error.value = ''; error.value = '';
try { try {
status.value = await apiClient.post('/oauth/challonge/client-credentials', { scope }); status.value = await apiClient.post(
'/oauth/challonge/client-credentials',
{ scope }
);
return true; return true;
} finally { } finally {
loading.value = false; loading.value = false;
@@ -84,7 +90,10 @@ export function useChallongeClientCredentials() {
loading.value = true; loading.value = true;
error.value = ''; error.value = '';
try { try {
status.value = await apiClient.post('/oauth/challonge/client-credentials/logout', {}); status.value = await apiClient.post(
'/oauth/challonge/client-credentials/logout',
{}
);
return true; return true;
} catch (err) { } catch (err) {
error.value = err.message || 'Logout failed'; error.value = err.message || 'Logout failed';
@@ -98,7 +107,10 @@ export function useChallongeClientCredentials() {
loading.value = true; loading.value = true;
error.value = ''; error.value = '';
try { try {
status.value = await apiClient.post('/oauth/challonge/client-credentials/clear', {}); status.value = await apiClient.post(
'/oauth/challonge/client-credentials/clear',
{}
);
return true; return true;
} catch (err) { } catch (err) {
error.value = err.message || 'Failed to clear credentials'; error.value = err.message || 'Failed to clear credentials';

View File

@@ -263,9 +263,7 @@ export function useOAuth(provider = 'challonge') {
sessionStorage.removeItem('oauth_provider'); sessionStorage.removeItem('oauth_provider');
sessionStorage.removeItem('oauth_return_to'); sessionStorage.removeItem('oauth_return_to');
console.log( console.log(`${provider} OAuth authentication successful`);
`${provider} OAuth authentication successful`
);
return data; return data;
} catch (err) { } catch (err) {
const backendCode = err?.data?.code; const backendCode = err?.data?.code;

View File

@@ -166,7 +166,9 @@ export function createChallongeV2Client(auth, options = {}) {
if (debug) { if (debug) {
console.error('[Challonge v2.1 JSON Parse Error]', parseError); console.error('[Challonge v2.1 JSON Parse Error]', parseError);
} }
const error = new Error(`HTTP ${response.status}: Failed to parse response`); const error = new Error(
`HTTP ${response.status}: Failed to parse response`
);
error.status = response.status; error.status = response.status;
throw error; throw error;
} }
@@ -207,7 +209,8 @@ export function createChallongeV2Client(auth, options = {}) {
const fallbackMessage = response.statusText || 'Request failed'; const fallbackMessage = response.statusText || 'Request failed';
const finalMessage = const finalMessage =
typeof messageFromBody === 'string' && messageFromBody.trim().length === 0 typeof messageFromBody === 'string' &&
messageFromBody.trim().length === 0
? fallbackMessage ? fallbackMessage
: messageFromBody || fallbackMessage; : messageFromBody || fallbackMessage;

View File

@@ -90,7 +90,8 @@ export function createApiClient(config = {}) {
}; };
// Default JSON content type unless caller overrides / uses FormData // Default JSON content type unless caller overrides / uses FormData
const hasBody = fetchOptions.body !== undefined && fetchOptions.body !== null; const hasBody =
fetchOptions.body !== undefined && fetchOptions.body !== null;
const isFormData = const isFormData =
typeof FormData !== 'undefined' && fetchOptions.body instanceof FormData; typeof FormData !== 'undefined' && fetchOptions.body instanceof FormData;
if (hasBody && !isFormData && !headers['Content-Type']) { if (hasBody && !isFormData && !headers['Content-Type']) {
@@ -160,7 +161,10 @@ export function createApiClient(config = {}) {
} }
// Some endpoints may return 204/304 (no body). Avoid JSON parse errors. // Some endpoints may return 204/304 (no body). Avoid JSON parse errors.
if (processedResponse.status === 204 || processedResponse.status === 304) { if (
processedResponse.status === 204 ||
processedResponse.status === 304
) {
return null; return null;
} }

View File

@@ -94,7 +94,8 @@
<ul> <ul>
<li> <li>
<strong>Secure Storage:</strong> Your API key is stored locally in <strong>Secure Storage:</strong> Your API key is stored locally in
the backend for your current session (linked via an httpOnly cookie). the backend for your current session (linked via an httpOnly
cookie).
</li> </li>
<li> <li>
<strong>Session Scoped:</strong> Each browser session has its own <strong>Session Scoped:</strong> Each browser session has its own

View File

@@ -223,7 +223,9 @@ const {
tournamentScope: clientTournamentScope, tournamentScope: clientTournamentScope,
maskedApiKey: clientMaskedApiKey, maskedApiKey: clientMaskedApiKey,
authType authType
} = useChallongeClient({ debug: localStorage.getItem('DEBUG_CHALLONGE') === 'true' }); } = useChallongeClient({
debug: localStorage.getItem('DEBUG_CHALLONGE') === 'true'
});
// Keep existing local controls bound to the composable state // Keep existing local controls bound to the composable state
apiVersion.value = clientApiVersion.value; apiVersion.value = clientApiVersion.value;