Initial release: DictIA v0.8.14-alpha (fork de Speakr, AGPL-3.0)
This commit is contained in:
286
static/js/modules/composables/tokens.js
Normal file
286
static/js/modules/composables/tokens.js
Normal file
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user