Files

287 lines
8.8 KiB
JavaScript

/**
* API Tokens Management Composable
* Handles API token operations for user authentication
*/
const { ref, computed } = Vue;
export function useTokens({ showToast, setGlobalError }) {
// State
const tokens = ref([]);
const isLoadingTokens = ref(false);
const showCreateTokenModal = ref(false);
const showTokenSecretModal = ref(false);
const newTokenSecret = ref('');
const newTokenData = ref(null);
const tokenForm = ref({
name: '',
expires_in_days: 0 // 0 = no expiration
});
// Computed
const hasTokens = computed(() => tokens.value.length > 0);
const activeTokens = computed(() => {
return tokens.value.filter(token => !token.revoked && !isTokenExpired(token));
});
const expiredOrRevokedTokens = computed(() => {
return tokens.value.filter(token => token.revoked || isTokenExpired(token));
});
// Helper methods
const isTokenExpired = (token) => {
if (!token.expires_at) return false;
const expiryDate = new Date(token.expires_at);
return expiryDate < new Date();
};
const formatTokenDate = (dateString) => {
if (!dateString) return 'Never';
const date = new Date(dateString);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
};
const getTokenStatus = (token) => {
if (token.revoked) return 'revoked';
if (isTokenExpired(token)) return 'expired';
return 'active';
};
const getTokenStatusClass = (token) => {
const status = getTokenStatus(token);
const baseClasses = 'px-2 py-1 text-xs font-semibold rounded';
switch (status) {
case 'active':
return `${baseClasses} bg-green-100 text-green-800`;
case 'expired':
return `${baseClasses} bg-yellow-100 text-yellow-800`;
case 'revoked':
return `${baseClasses} bg-red-100 text-red-800`;
default:
return `${baseClasses} bg-gray-100 text-gray-800`;
}
};
// API methods
const loadTokens = async () => {
isLoadingTokens.value = true;
try {
const response = await fetch('/api/tokens', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to load tokens');
}
const data = await response.json();
tokens.value = data.tokens || [];
} catch (error) {
console.error('Error loading tokens:', error);
setGlobalError('Failed to load API tokens: ' + error.message);
} finally {
isLoadingTokens.value = false;
}
};
const createToken = async () => {
if (!tokenForm.value.name || tokenForm.value.name.trim() === '') {
showToast('Please enter a token name', 'error');
return;
}
try {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const response = await fetch('/api/tokens', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
name: tokenForm.value.name,
expires_in_days: parseInt(tokenForm.value.expires_in_days) || 0
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to create token');
}
const data = await response.json();
// Store the plaintext token to show to user (only shown once)
newTokenSecret.value = data.token;
newTokenData.value = {
id: data.id,
name: data.name,
created_at: data.created_at,
expires_at: data.expires_at
};
// Add to tokens list (without the plaintext token)
tokens.value.unshift({
id: data.id,
name: data.name,
created_at: data.created_at,
last_used_at: data.last_used_at,
expires_at: data.expires_at,
revoked: data.revoked
});
// Reset form
tokenForm.value = {
name: '',
expires_in_days: 0
};
// Close create modal and show secret modal
showCreateTokenModal.value = false;
showTokenSecretModal.value = true;
showToast('API token created successfully', 'success');
} catch (error) {
console.error('Error creating token:', error);
showToast('Failed to create token: ' + error.message, 'error');
}
};
const revokeToken = async (tokenId, tokenName) => {
if (!confirm(`Are you sure you want to revoke the token "${tokenName}"? This action cannot be undone and any applications using this token will lose access.`)) {
return;
}
try {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const response = await fetch(`/api/tokens/${tokenId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to revoke token');
}
// Remove from local list
tokens.value = tokens.value.filter(t => t.id !== tokenId);
showToast('Token revoked successfully', 'success');
} catch (error) {
console.error('Error revoking token:', error);
showToast('Failed to revoke token: ' + error.message, 'error');
}
};
const updateTokenName = async (tokenId, newName) => {
if (!newName || newName.trim() === '') {
showToast('Token name cannot be empty', 'error');
return;
}
try {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const response = await fetch(`/api/tokens/${tokenId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({ name: newName })
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to update token');
}
const data = await response.json();
// Update local token
const token = tokens.value.find(t => t.id === tokenId);
if (token) {
token.name = data.name;
}
showToast('Token name updated', 'success');
} catch (error) {
console.error('Error updating token:', error);
showToast('Failed to update token: ' + error.message, 'error');
}
};
const copyTokenToClipboard = async (token) => {
try {
await navigator.clipboard.writeText(token);
showToast('Token copied to clipboard', 'success');
} catch (error) {
console.error('Error copying token:', error);
showToast('Failed to copy token to clipboard', 'error');
}
};
const openCreateTokenModal = () => {
tokenForm.value = {
name: '',
expires_in_days: 0
};
showCreateTokenModal.value = true;
};
const closeCreateTokenModal = () => {
showCreateTokenModal.value = false;
tokenForm.value = {
name: '',
expires_in_days: 0
};
};
const closeTokenSecretModal = () => {
showTokenSecretModal.value = false;
newTokenSecret.value = '';
newTokenData.value = null;
};
return {
// State
tokens,
isLoadingTokens,
showCreateTokenModal,
showTokenSecretModal,
newTokenSecret,
newTokenData,
tokenForm,
// Computed
hasTokens,
activeTokens,
expiredOrRevokedTokens,
// Methods
isTokenExpired,
formatTokenDate,
getTokenStatus,
getTokenStatusClass,
loadTokens,
createToken,
revokeToken,
updateTokenName,
copyTokenToClipboard,
openCreateTokenModal,
closeCreateTokenModal,
closeTokenSecretModal
};
}