/** * Challonge Client Credentials Flow Composable * * Manages client credentials OAuth flow for server-to-server authentication * Used for APPLICATION scope access (application:manage) * * Features: * - Client credentials token exchange * - Automatic token refresh * - Secure credential storage * - Token expiration handling * * Usage: * ```javascript * import { useChallongeClientCredentials } from '@/composables/useChallongeClientCredentials' * * const { * isAuthenticated, * accessToken, * authenticate, * logout, * saveCredentials * } = useChallongeClientCredentials() * * // Save client credentials (one time) * saveCredentials('your_client_id', 'your_client_secret') * * // Get access token (will auto-refresh if expired) * await authenticate('application:manage tournaments:read tournaments:write') * const token = accessToken.value * ``` */ import { ref, computed } from 'vue'; const CREDENTIALS_KEY = 'challonge_client_credentials'; const TOKEN_KEY = 'challonge_client_token'; // Shared state across all instances const credentials = ref(null); const tokenData = ref(null); const loading = ref(false); const error = ref(null); // Load credentials and token from localStorage on module initialization try { const storedCreds = localStorage.getItem(CREDENTIALS_KEY); if (storedCreds) { credentials.value = JSON.parse(storedCreds); } const storedToken = localStorage.getItem(TOKEN_KEY); if (storedToken) { tokenData.value = JSON.parse(storedToken); // Check if token is expired if ( tokenData.value.expires_at && Date.now() >= tokenData.value.expires_at ) { console.log('🔄 Client credentials token expired, will need to refresh'); } } } catch (err) { console.error('Failed to load client credentials:', err); } export function useChallongeClientCredentials() { const isAuthenticated = computed(() => { return !!tokenData.value?.access_token && !isExpired.value; }); const isExpired = computed(() => { if (!tokenData.value?.expires_at) return true; return Date.now() >= tokenData.value.expires_at; }); const accessToken = computed(() => { if (isExpired.value) return null; return tokenData.value?.access_token || null; }); const hasCredentials = computed(() => { return !!(credentials.value?.client_id && credentials.value?.client_secret); }); const maskedClientId = computed(() => { if (!credentials.value?.client_id) return null; const id = credentials.value.client_id; if (id.length < 12) return id.slice(0, 4) + '••••'; return id.slice(0, 6) + '•••••••' + id.slice(-4); }); /** * Save client credentials to localStorage * @param {string} clientId - OAuth client ID * @param {string} clientSecret - OAuth client secret * @returns {boolean} Success status */ function saveCredentials(clientId, clientSecret) { try { if (!clientId || !clientSecret) { throw new Error('Client ID and secret are required'); } credentials.value = { client_id: clientId, client_secret: clientSecret, saved_at: new Date().toISOString() }; localStorage.setItem(CREDENTIALS_KEY, JSON.stringify(credentials.value)); console.log('✅ Client credentials saved'); return true; } catch (err) { error.value = err.message; console.error('Failed to save credentials:', err); return false; } } /** * Clear stored credentials and token * @returns {boolean} Success status */ function clearCredentials() { try { credentials.value = null; tokenData.value = null; localStorage.removeItem(CREDENTIALS_KEY); localStorage.removeItem(TOKEN_KEY); console.log('✅ Client credentials cleared'); return true; } catch (err) { error.value = err.message; console.error('Failed to clear credentials:', err); return false; } } /** * Authenticate using client credentials flow * @param {string} scope - Requested scope (e.g., 'application:manage') * @returns {Promise} Access token */ async function authenticate(scope = 'application:manage') { if (!hasCredentials.value) { throw new Error( 'Client credentials not configured. Use saveCredentials() first.' ); } // Return existing token if still valid if (isAuthenticated.value && !isExpired.value) { console.log('✅ Using existing valid token'); return accessToken.value; } loading.value = true; error.value = null; try { console.log('🔐 Requesting client credentials token...'); console.log(' Client ID:', maskedClientId.value); console.log(' Scope:', scope); const response = await fetch('https://api.challonge.com/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'client_credentials', client_id: credentials.value.client_id, client_secret: credentials.value.client_secret, scope: scope }) }); if (!response.ok) { const errorData = await response.json(); throw new Error( errorData.error_description || errorData.error || `Token request failed: ${response.status}` ); } const data = await response.json(); // Store token with expiration tokenData.value = { access_token: data.access_token, token_type: data.token_type, scope: data.scope, created_at: Date.now(), expires_in: data.expires_in, expires_at: Date.now() + data.expires_in * 1000 }; // Save to localStorage localStorage.setItem(TOKEN_KEY, JSON.stringify(tokenData.value)); console.log('✅ Client credentials token obtained'); console.log(' Expires in:', data.expires_in, 'seconds'); console.log(' Scope:', data.scope); return tokenData.value.access_token; } catch (err) { error.value = err.message; console.error('❌ Client credentials authentication failed:', err); throw err; } finally { loading.value = false; } } /** * Force token refresh * @param {string} scope - Requested scope * @returns {Promise} New access token */ async function refresh(scope = 'application:manage') { // Clear existing token tokenData.value = null; localStorage.removeItem(TOKEN_KEY); // Get new token return authenticate(scope); } /** * Logout and clear token (keeps credentials) */ function logout() { tokenData.value = null; localStorage.removeItem(TOKEN_KEY); console.log('✅ Logged out (credentials retained)'); } /** * Get token info for debugging */ const tokenInfo = computed(() => { if (!tokenData.value) return null; const now = Date.now(); const expiresAt = tokenData.value.expires_at; const timeUntilExpiry = expiresAt ? expiresAt - now : 0; return { hasToken: !!tokenData.value.access_token, isExpired: isExpired.value, scope: tokenData.value.scope, expiresIn: Math.floor(timeUntilExpiry / 1000), expiresAt: expiresAt ? new Date(expiresAt).toLocaleString() : null }; }); return { // State isAuthenticated, isExpired, accessToken, hasCredentials, maskedClientId, loading, error, tokenInfo, // Actions saveCredentials, clearCredentials, authenticate, refresh, logout }; }