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:
File diff suppressed because one or more lines are too long
@@ -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'
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user