🔒 Add functionality to manage Challonge client credentials securely

This commit is contained in:
2026-01-28 18:30:48 +00:00
parent e55537ff8b
commit 36caa456fb

View File

@@ -0,0 +1,275 @@
/**
* 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<string>} 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<string>} 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
};
}