Skip to content
Snippets Groups Projects
Verified Commit 548d5247 authored by Daniel Göbel's avatar Daniel Göbel
Browse files

Add page to list personal API tokens

parent 60259be1
No related branches found
No related tags found
1 merge request!124Resolve "Support API tokens"
......@@ -28,5 +28,9 @@ export type ApiTokenOut = {
* The UNIX timestamp when this token was created
*/
created_at: number;
/**
* The UNIX timestamp when this token was used the last time
*/
last_used?: (number | null);
};
......@@ -28,6 +28,10 @@ export type ApiTokenPrivateOut = {
* The UNIX timestamp when this token was created
*/
created_at: number;
/**
* The UNIX timestamp when this token was used the last time
*/
last_used?: (number | null);
/**
* The actual token used for authentication
*/
......
......@@ -287,6 +287,11 @@ watch(
>Advanced Usage</a
>
</li>
<li>
<router-link class="dropdown-item" :to="{ name: 'api-tokens' }"
>API tokens
</router-link>
</li>
<li>
<hr class="dropdown-divider" />
</li>
......
......@@ -5,6 +5,7 @@ import { workflowRoutes } from "@/router/workflowRoutes";
import { s3Routes } from "@/router/s3Routes";
import { resourceRoutes } from "@/router/resourceRoutes";
import { adminRoutes } from "@/router/adminRoutes";
import { userRoutes } from "@/router/userRoutes";
import ImprintView from "@/views/ImprintView.vue";
import PrivacyPolicyView from "@/views/PrivacyPolicyView.vue";
import TermsOfUsageView from "@/views/TermsOfUsageView.vue";
......@@ -21,6 +22,7 @@ const router = createRouter({
...s3Routes,
...workflowRoutes,
...adminRoutes,
...userRoutes,
],
},
{
......
import type { RouteRecordRaw } from "vue-router";
export const userRoutes: RouteRecordRaw[] = [
{
path: "api-tokens",
name: "api-tokens",
component: () => import("../views/user/ListApiTokenView.vue"),
meta: {
title: "API Tokens",
},
},
];
import { defineStore } from "pinia";
import type { UserOutExtended, UserOut, UserIn } from "@/client";
import { UserService, RoleEnum } from "@/client";
import type { UserOutExtended, UserOut, UserIn, ApiTokenOut } from "@/client";
import { UserService, RoleEnum, ApiTokenService } from "@/client";
import { useWorkflowExecutionStore } from "@/stores/workflowExecutions";
import { useBucketStore } from "@/stores/buckets";
import { useWorkflowStore } from "@/stores/workflows";
......@@ -36,12 +36,14 @@ export const useUserStore = defineStore({
state: () =>
({
user: null,
token: null,
jwt: null,
roles: [],
apiTokensMapping: {},
}) as {
user: UserOutExtended | null;
token: DecodedToken | null;
jwt: DecodedToken | null;
roles: RoleEnum[];
apiTokensMapping: Record<string, ApiTokenOut>;
},
getters: {
roles(): string[] {
......@@ -50,11 +52,14 @@ export const useUserStore = defineStore({
JSON.parse(localStorage.getItem("roles") ?? "[]")
);
},
apiTokens(): ApiTokenOut[] {
return Object.values(this.apiTokensMapping);
},
authenticated(): boolean {
return this.token != null || this.user != null;
return this.jwt != null || this.user != null;
},
currentUID(): string {
return this.user?.uid ?? this.token?.sub ?? "";
return this.user?.uid ?? this.jwt?.sub ?? "";
},
foreignUser(): boolean {
return this.roles.length === 0;
......@@ -83,7 +88,7 @@ export const useUserStore = defineStore({
});
},
updateJWT(jwt: string | null) {
this.token = jwt != null ? parseJwt(jwt) : null;
this.jwt = jwt != null ? parseJwt(jwt) : null;
},
updateUser(user: UserOutExtended) {
this.user = user;
......@@ -116,6 +121,27 @@ export const useUserStore = defineStore({
},
);
},
fetchApiTokens(
uid?: string,
onFinally?: () => void,
): Promise<ApiTokenOut[]> {
if (this.apiTokens.length > 0) {
onFinally?.();
}
return ApiTokenService.apiTokenListToken(uid)
.then((tokens) => {
for (const token of tokens) {
this.apiTokensMapping[token.token_id] = token;
}
return tokens;
})
.finally(onFinally);
},
deleteApiToken(tokenId: string): Promise<void> {
return ApiTokenService.apiTokenDeleteToken(tokenId).then(() => {
delete this.apiTokensMapping[tokenId];
});
},
updateUserRoles(uid: string, roles: RoleEnum[]): Promise<UserOutExtended> {
return UserService.userUpdateRoles(uid, {
roles: roles,
......
......@@ -130,7 +130,7 @@ onMounted(() => {
>
<button
type="button"
class="btn btn-primary btn-light me-2 shadow-sm border w-fit"
class="btn btn-light me-2 shadow-sm border w-fit"
:disabled="resourceState.loading"
@click="clickRefreshResources"
>
......
<script setup lang="ts">
import { onMounted, reactive } from "vue";
import { useUserStore } from "@/stores/users";
import dayjs from "dayjs";
import DeleteModal from "@/components/modals/DeleteModal.vue";
const userRepository = useUserStore();
const state = reactive<{
loading: boolean;
deleteTokenId: string;
}>({
loading: false,
deleteTokenId: "",
});
function fetchTokens() {
state.loading = true;
userRepository.fetchApiTokens(userRepository.currentUID, () => {
state.loading = false;
});
}
function deleteApiToken(tokenId: string) {
state.deleteTokenId = tokenId;
}
function confirmedDeleteApiToken() {
userRepository.deleteApiToken(state.deleteTokenId).finally(() => {
state.deleteTokenId = "";
});
}
onMounted(() => {
fetchTokens();
});
</script>
<template>
<delete-modal
modal-id="delete-personal-api-token-modal"
@confirm-delete="confirmedDeleteApiToken"
:object-name-delete="
userRepository.apiTokensMapping[state.deleteTokenId]?.name ?? ''
"
/>
<div
class="row border-bottom mb-4 justify-content-between align-items-center"
>
<h2 class="w-fit">Personal API tokens</h2>
<span class="w-fit" tabindex="0">
<button type="button" class="btn btn-primary" disabled>
Create token
</button>
</span>
</div>
<table class="table table-striped align-middle caption-top">
<caption>
Displaying
{{
userRepository.apiTokens.length
}}
API tokens
</caption>
<thead>
<tr>
<th scope="col"><b>Name</b></th>
<th scope="col">Scopes</th>
<th scope="col">Created</th>
<th scope="col">Last used</th>
<th scope="col">Expires</th>
<th scope="col"></th>
</tr>
</thead>
<tbody v-if="state.loading">
<tr>
<td colspan="6" class="text-center fst-italic fw-light">Loading ...</td>
</tr>
</tbody>
<tbody v-else-if="userRepository.apiTokens.length === 0">
<tr>
<td colspan="6" class="text-center fst-italic fw-light">
No personal API tokens
</td>
</tr>
</tbody>
<tbody v-else>
<tr v-for="token in userRepository.apiTokens" :key="token.token_id">
<th scope="row">{{ token.name }}</th>
<td>{{ token.scopes.join(", ") }}</td>
<td>
{{ dayjs.unix(token.created_at).format("MMM DD, YYYY") }}
</td>
<td v-if="token.last_used">
{{ dayjs.unix(token.last_used).format("MMM DD, YYYY HH:mm") }}
</td>
<td v-else>Never</td>
<td v-if="token.expires_at">
{{
dayjs
.duration(token.expires_at - dayjs().unix(), "seconds")
.humanize(true)
}}
</td>
<td v-else>Never</td>
<td class="text-end">
<button
type="button"
class="btn btn-danger btn-sm"
data-bs-toggle="modal"
data-bs-target="#delete-personal-api-token-modal"
@click="deleteApiToken(token.token_id)"
>
Delete
</button>
</td>
</tr>
</tbody>
</table>
</template>
<style scoped></style>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment