🎨 Improve code readability by reformatting and updating function definitions and comments

This commit is contained in:
2026-01-28 18:18:55 +00:00
parent 1944b43af8
commit a24f766e37
154 changed files with 7261 additions and 117 deletions

View File

@@ -0,0 +1,95 @@
/**
* useChallongeApiKey Composable
* Manages Challonge API key storage in browser localStorage
* Works on mobile, desktop, and tablets
*/
import { ref, computed } from 'vue';
const STORAGE_KEY = 'challonge_api_key';
const storedKey = ref(getStoredKey());
/**
* Get API key from localStorage
* @returns {string|null} Stored API key or null
*/
function getStoredKey() {
try {
return localStorage.getItem(STORAGE_KEY) || null;
} catch (error) {
console.warn('localStorage not available:', error);
return null;
}
}
/**
* Save API key to localStorage
* @param {string} apiKey - The API key to store
* @returns {boolean} Success status
*/
function saveApiKey(apiKey) {
try {
if (!apiKey || typeof apiKey !== 'string') {
throw new Error('Invalid API key format');
}
localStorage.setItem(STORAGE_KEY, apiKey);
storedKey.value = apiKey;
return true;
} catch (error) {
console.error('Failed to save API key:', error);
return false;
}
}
/**
* Clear API key from localStorage
* @returns {boolean} Success status
*/
function clearApiKey() {
try {
localStorage.removeItem(STORAGE_KEY);
storedKey.value = null;
return true;
} catch (error) {
console.error('Failed to clear API key:', error);
return false;
}
}
/**
* Get masked version of API key for display
* Shows first 4 and last 4 characters
* @returns {string|null} Masked key or null
*/
const maskedKey = computed(() => {
if (!storedKey.value) return null;
const key = storedKey.value;
if (key.length < 8) return '••••••••';
return `${key.slice(0, 4)}•••••••${key.slice(-4)}`;
});
/**
* Check if API key is stored
* @returns {boolean} True if key exists
*/
const isKeyStored = computed(() => !!storedKey.value);
/**
* Get the full API key (use with caution)
* @returns {string|null} Full API key or null
*/
function getApiKey() {
return storedKey.value;
}
export function useChallongeApiKey() {
return {
saveApiKey,
clearApiKey,
getApiKey,
getStoredKey,
storedKey: computed(() => storedKey.value),
maskedKey,
isKeyStored
};
}

View File

@@ -0,0 +1,301 @@
/**
* Challonge OAuth Composable
*
* Manages OAuth authentication flow and token storage for Challonge API v2.1
*
* Features:
* - Authorization URL generation
* - Token exchange and storage
* - Automatic token refresh
* - Secure token management
*/
import { ref, computed } from 'vue';
const STORAGE_KEY = 'challonge_oauth_tokens';
const CLIENT_ID = import.meta.env.VITE_CHALLONGE_CLIENT_ID;
const REDIRECT_URI =
import.meta.env.VITE_CHALLONGE_REDIRECT_URI ||
`${window.location.origin}/oauth/callback`;
// Shared state across all instances
const tokens = ref(null);
const loading = ref(false);
const error = ref(null);
// Load tokens from localStorage on module initialization
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
tokens.value = JSON.parse(stored);
// Check if token is expired
if (tokens.value.expires_at && Date.now() >= tokens.value.expires_at) {
console.log('🔄 Token expired, will need to refresh');
}
}
} catch (err) {
console.error('Failed to load OAuth tokens:', err);
}
export function useChallongeOAuth() {
const isAuthenticated = computed(() => {
return !!tokens.value?.access_token;
});
const isExpired = computed(() => {
if (!tokens.value?.expires_at) return false;
return Date.now() >= tokens.value.expires_at;
});
const accessToken = computed(() => {
return tokens.value?.access_token || null;
});
/**
* Generate authorization URL for OAuth flow
* @param {string} scope - Requested scope (default: 'tournaments:read tournaments:write')
* @param {string} state - Optional state parameter (will be generated if not provided)
* @returns {Object} Object with authUrl and state
*/
function getAuthorizationUrl(
scope = 'tournaments:read tournaments:write',
state = null
) {
if (!CLIENT_ID) {
throw new Error('VITE_CHALLONGE_CLIENT_ID not configured');
}
// Generate state if not provided
const oauthState = state || generateState();
const params = new URLSearchParams({
response_type: 'code',
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: scope,
state: oauthState
});
return {
authUrl: `https://api.challonge.com/oauth/authorize?${params.toString()}`,
state: oauthState
};
}
/**
* Start OAuth authorization flow
* @param {string} scope - Requested scope
*/
function login(scope) {
try {
// Generate auth URL and state
const { authUrl, state } = getAuthorizationUrl(scope);
// Store state for CSRF protection
sessionStorage.setItem('oauth_state', state);
console.log('🔐 Starting OAuth flow with state:', state);
// Redirect to Challonge authorization page
window.location.href = authUrl;
} catch (err) {
error.value = err.message;
console.error('OAuth login error:', err);
}
}
/**
* Exchange authorization code for access token
* @param {string} code - Authorization code from callback
* @param {string} state - State parameter for CSRF protection
*/
async function exchangeCode(code, state) {
// Verify state parameter
const storedState = sessionStorage.getItem('oauth_state');
console.log('🔐 OAuth callback verification:');
console.log(' Received state:', state);
console.log(' Stored state:', storedState);
console.log(' Match:', state === storedState);
if (state !== storedState) {
console.error(
'❌ State mismatch! Possible CSRF attack or session issue.'
);
throw new Error('Invalid state parameter - possible CSRF attack');
}
loading.value = true;
error.value = null;
try {
const response = await fetch('/api/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ code })
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(
errorData.error_description ||
errorData.error ||
'Token exchange failed'
);
}
const data = await response.json();
// Calculate expiration time
const expiresAt = Date.now() + data.expires_in * 1000;
tokens.value = {
access_token: data.access_token,
refresh_token: data.refresh_token,
token_type: data.token_type,
expires_in: data.expires_in,
expires_at: expiresAt,
scope: data.scope,
created_at: Date.now()
};
// Store tokens
localStorage.setItem(STORAGE_KEY, JSON.stringify(tokens.value));
sessionStorage.removeItem('oauth_state');
console.log('✅ OAuth authentication successful');
return tokens.value;
} catch (err) {
error.value = err.message;
console.error('Token exchange error:', err);
throw err;
} finally {
loading.value = false;
}
}
/**
* Refresh access token using refresh token
*/
async function refreshToken() {
if (!tokens.value?.refresh_token) {
throw new Error('No refresh token available');
}
loading.value = true;
error.value = null;
try {
const response = await fetch('/api/oauth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
refresh_token: tokens.value.refresh_token
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(
errorData.error_description ||
errorData.error ||
'Token refresh failed'
);
}
const data = await response.json();
// Calculate expiration time
const expiresAt = Date.now() + data.expires_in * 1000;
tokens.value = {
access_token: data.access_token,
refresh_token: data.refresh_token || tokens.value.refresh_token, // Keep old if not provided
token_type: data.token_type,
expires_in: data.expires_in,
expires_at: expiresAt,
scope: data.scope,
refreshed_at: Date.now()
};
// Store updated tokens
localStorage.setItem(STORAGE_KEY, JSON.stringify(tokens.value));
console.log('✅ Token refreshed successfully');
return tokens.value;
} catch (err) {
error.value = err.message;
console.error('Token refresh error:', err);
// If refresh fails, clear tokens and force re-authentication
logout();
throw err;
} finally {
loading.value = false;
}
}
/**
* Get valid access token (refreshes if expired)
*/
async function getValidToken() {
if (!tokens.value) {
throw new Error('Not authenticated');
}
// If token is expired or about to expire (within 5 minutes), refresh it
const expiresIn = tokens.value.expires_at - Date.now();
const fiveMinutes = 5 * 60 * 1000;
if (expiresIn < fiveMinutes) {
console.log('🔄 Token expired or expiring soon, refreshing...');
await refreshToken();
}
return tokens.value.access_token;
}
/**
* Logout and clear tokens
*/
function logout() {
tokens.value = null;
localStorage.removeItem(STORAGE_KEY);
sessionStorage.removeItem('oauth_state');
console.log('👋 Logged out');
}
/**
* Generate random state for CSRF protection
*/
function generateState() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join(
''
);
}
return {
// State
tokens: computed(() => tokens.value),
isAuthenticated,
isExpired,
accessToken,
loading: computed(() => loading.value),
error: computed(() => error.value),
// Methods
login,
logout,
exchangeCode,
refreshToken,
getValidToken,
getAuthorizationUrl
};
}