Files
chronus/src/components/settings/ApiTokenManager.vue
Atridad Lahiji 54cac49b70
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m56s
OOOOOPS
2026-01-19 23:39:00 -07:00

258 lines
7.2 KiB
Vue

<script setup lang="ts">
import { ref, onMounted } from "vue";
import { Icon } from "@iconify/vue";
interface ApiToken {
id: string;
name: string;
lastUsedAt: string | null;
createdAt: string;
}
const props = defineProps<{
initialTokens: ApiToken[];
}>();
const tokens = ref<ApiToken[]>(props.initialTokens);
const createModalOpen = ref(false);
const showTokenModalOpen = ref(false);
const newTokenName = ref("");
const newTokenValue = ref("");
const loading = ref(false);
const isMounted = ref(false);
onMounted(() => {
isMounted.value = true;
});
function formatDate(dateString: string | null) {
if (!dateString) return "Never";
return new Date(dateString).toLocaleDateString();
}
async function createToken() {
if (!newTokenName.value) return;
loading.value = true;
try {
const response = await fetch("/api/user/tokens", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name: newTokenName.value }),
});
if (response.ok) {
const data = await response.json();
const { token, ...tokenMeta } = data;
tokens.value.unshift({
id: tokenMeta.id,
name: tokenMeta.name,
lastUsedAt: tokenMeta.lastUsedAt,
createdAt: tokenMeta.createdAt,
});
newTokenValue.value = token;
createModalOpen.value = false;
showTokenModalOpen.value = true;
newTokenName.value = "";
} else {
alert("Failed to create token");
}
} catch (error) {
console.error("Error creating token:", error);
alert("An error occurred");
} finally {
loading.value = false;
}
}
async function deleteToken(id: string) {
if (
!confirm(
"Are you sure you want to revoke this token? Any applications using it will stop working.",
)
) {
return;
}
try {
const response = await fetch(`/api/user/tokens/${id}`, {
method: "DELETE",
});
if (response.ok) {
tokens.value = tokens.value.filter((t) => t.id !== id);
} else {
alert("Failed to delete token");
}
} catch (error) {
console.error("Error deleting token:", error);
alert("An error occurred");
}
}
function copyToken() {
navigator.clipboard.writeText(newTokenValue.value);
}
function closeShowTokenModal() {
showTokenModalOpen.value = false;
newTokenValue.value = "";
}
</script>
<template>
<div>
<div class="card bg-base-100 shadow-xl border border-base-200 mb-6">
<div class="card-body p-4 sm:p-6">
<div class="flex justify-between items-center mb-6">
<h2 class="card-title text-lg sm:text-xl">
<Icon
icon="heroicons:code-bracket-square"
class="w-5 h-5 sm:w-6 sm:h-6"
/>
API Tokens
</h2>
<button
class="btn btn-primary btn-sm"
@click="createModalOpen = true"
>
<Icon icon="heroicons:plus" class="w-4 h-4" />
Create Token
</button>
</div>
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Last Used</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-if="tokens.length === 0">
<td colspan="4" class="text-center text-base-content/60 py-4">
No API tokens found. Create one to access the API.
</td>
</tr>
<tr v-else v-for="token in tokens" :key="token.id">
<td class="font-medium">{{ token.name }}</td>
<td class="text-sm">
<span v-if="isMounted">{{
formatDate(token.lastUsedAt)
}}</span>
<span v-else>{{ token.lastUsedAt || "Never" }}</span>
</td>
<td class="text-sm">
<span v-if="isMounted">{{
formatDate(token.createdAt)
}}</span>
<span v-else>{{ token.createdAt }}</span>
</td>
<td>
<button
class="btn btn-ghost btn-xs text-error"
@click="deleteToken(token.id)"
>
<Icon icon="heroicons:trash" class="w-4 h-4" />
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Create Token Modal -->
<dialog class="modal" :class="{ 'modal-open': createModalOpen }">
<div class="modal-box">
<h3 class="font-bold text-lg">Create API Token</h3>
<p class="py-4 text-sm text-base-content/70">
API tokens allow you to authenticate with the API programmatically.
Give your token a descriptive name.
</p>
<form @submit.prevent="createToken" class="space-y-4">
<div class="form-control">
<label class="label pb-2 font-medium" for="token-name">
Token Name
</label>
<input
type="text"
id="token-name"
v-model="newTokenName"
placeholder="e.g. CI/CD Pipeline"
class="input input-bordered w-full"
required
/>
</div>
<div class="modal-action">
<button type="button" class="btn" @click="createModalOpen = false">
Cancel
</button>
<button type="submit" class="btn btn-primary" :disabled="loading">
<span
v-if="loading"
class="loading loading-spinner loading-sm"
></span>
Generate Token
</button>
</div>
</form>
</div>
<form
method="dialog"
class="modal-backdrop"
@click="createModalOpen = false"
>
<button>close</button>
</form>
</dialog>
<!-- Show Token Modal -->
<dialog class="modal" :class="{ 'modal-open': showTokenModalOpen }">
<div class="modal-box">
<h3 class="font-bold text-lg text-success flex items-center gap-2">
<Icon icon="heroicons:check-circle" class="w-6 h-6" />
Token Created
</h3>
<p class="py-4">
Make sure to copy your personal access token now. You won't be able to
see it again!
</p>
<div
class="bg-base-200 p-4 rounded-lg break-all font-mono text-sm relative group"
>
<span>{{ newTokenValue }}</span>
<button
class="absolute top-2 right-2 btn btn-xs btn-ghost opacity-0 group-hover:opacity-100 transition-opacity"
@click="copyToken"
title="Copy to clipboard"
>
<Icon icon="heroicons:clipboard" class="w-4 h-4" />
</button>
</div>
<div class="modal-action">
<button class="btn btn-primary" @click="closeShowTokenModal">
Done
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop" @click="closeShowTokenModal">
<button>close</button>
</form>
</dialog>
</div>
</template>