287 lines
8.8 KiB
JavaScript
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
|
|
};
|
|
}
|