🔒 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