🔒 Add functionality to manage Challonge client credentials securely
This commit is contained in:
@@ -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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user