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

Add modal to create new API tokens

parent 548d5247
No related branches found
No related tags found
1 merge request!124Resolve "Support API tokens"
......@@ -33,3 +33,6 @@ pre {
.hover-info:hover {
color: var(--bs-info) !important;
}
.hover-danger:hover {
color: var(--bs-danger) !important;
}
......@@ -344,6 +344,10 @@ watch(
</tr>
</tbody>
</table>
<p>
Personal API tokens are encouraged to use when accessing the REST API of
CloWM directly.
</p>
</template>
<template v-slot:footer>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
......
<script setup lang="ts">
import { onMounted, reactive, ref } from "vue";
import { type ApiTokenIn, type ApiTokenPrivateOut, ScopeEnum } from "@/client";
import { Modal } from "bootstrap";
import BootstrapModal from "@/components/modals/BootstrapModal.vue";
import dayjs from "dayjs";
import { useUserStore } from "@/stores/users";
import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
const props = defineProps<{
modalId: string;
}>();
const tokenIn = reactive<ApiTokenIn>({
name: "",
expires_at: undefined,
scopes: [],
});
const formState = reactive<{
validated: boolean;
loading: boolean;
}>({
validated: false,
loading: false,
});
const userRepository = useUserStore();
const tokenCreateForm = ref<HTMLFormElement | undefined>(undefined);
let createTokenModal: Modal | null = null;
const scopeDescription: Record<ScopeEnum, string> = {
token: "Some description about token scope",
user: "Some desc for user",
x: "Placeholder for x scope",
};
const emit = defineEmits<{
(e: "token-created", token: ApiTokenPrivateOut): void;
}>();
function modalClosed() {
formState.validated = false;
}
function expiresAtTimestampChanged(target?: HTMLInputElement | null) {
tokenIn.expires_at = target?.value ? dayjs(target?.value).unix() : undefined;
}
function createToken() {
formState.validated = true;
if (tokenCreateForm.value?.checkValidity()) {
formState.loading = true;
userRepository
.createApiToken(tokenIn)
.then((token) => {
emit("token-created", token);
createTokenModal?.hide();
tokenIn.expires_at = undefined;
tokenIn.name = "";
tokenIn.scopes = [];
})
.finally(() => {
formState.loading = false;
});
}
}
onMounted(() => {
createTokenModal = new Modal("#" + props.modalId);
});
</script>
<template>
<bootstrap-modal
:modalId="modalId"
static-backdrop
modal-label="Create API Token Modal"
v-on="{ 'hidden.bs.modal': modalClosed }"
size-modifier-modal="lg"
>
<template #header> Create personal API token</template>
<template #body>
<form
id="api-token-create-form"
:class="{ 'was-validated': formState.validated }"
ref="tokenCreateForm"
@submit.prevent="createToken"
>
<div class="mb-3">
<label for="token-name-input" class="form-label">Token name</label>
<input
type="text"
class="form-control"
id="token-name-input"
required
minlength="3"
maxlength="63"
pattern="^[a-z\d\-]+$"
v-model="tokenIn.name"
/>
<div class="invalid-feedback">
<div>
Name must ...
<ul>
<li>be at least 3 Characters long</li>
<li>contains the characters a-z 0-9 and /</li>
</ul>
</div>
</div>
</div>
<div class="mb-3">
<label for="expires-at-input" class="form-label"
>Expiration date</label
>
<div :class="{ 'input-group': tokenIn.expires_at != undefined }">
<input
class="form-control"
id="expires-at-input"
type="date"
:min="dayjs().add(1, 'day').format('YYYY-MM-DD')"
:value="
tokenIn.expires_at
? dayjs.unix(tokenIn.expires_at).format('YYYY-MM-DD')
: undefined
"
@input="
(event) =>
expiresAtTimestampChanged(event.target as HTMLInputElement)
"
/>
<div
v-if="tokenIn.expires_at"
class="input-group-text cursor-pointer hover-danger"
@click="tokenIn.expires_at = undefined"
>
<font-awesome-icon icon="fa-solid fa-xmark" class="fs-5" />
</div>
</div>
</div>
<div class="mb-3">
<div>Select scopes</div>
<div
v-for="scope in Object.values(ScopeEnum)"
:key="scope"
class="form-check mb-2"
>
<input
class="form-check-input"
type="checkbox"
:value="scope"
:id="`scope-input-${scope}`"
v-model="tokenIn.scopes"
/>
<label
class="form-check-label pt-1"
style="line-height: 0.8rem"
:for="`scope-input-${scope}`"
>
<span>{{ scope.toLowerCase() }}</span>
<br />
<span class="text-secondary" style="font-size: 0.8rem">
{{ scopeDescription[scope] }}
</span>
</label>
</div>
</div>
</form>
</template>
<template v-slot:footer>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Close
</button>
<button
type="submit"
form="api-token-create-form"
class="btn btn-primary"
:disabled="formState.loading || tokenIn.scopes.length === 0"
>
<span
v-if="formState.loading"
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
></span>
Save
</button>
</template>
</bootstrap-modal>
</template>
<style scoped></style>
import { defineStore } from "pinia";
import type { UserOutExtended, UserOut, UserIn, ApiTokenOut } from "@/client";
import type {
UserOutExtended,
UserOut,
UserIn,
ApiTokenOut,
ApiTokenIn,
ApiTokenPrivateOut,
} from "@/client";
import { UserService, RoleEnum, ApiTokenService } from "@/client";
import { useWorkflowExecutionStore } from "@/stores/workflowExecutions";
import { useBucketStore } from "@/stores/buckets";
......@@ -137,6 +144,12 @@ export const useUserStore = defineStore({
})
.finally(onFinally);
},
createApiToken(tokenIn: ApiTokenIn): Promise<ApiTokenPrivateOut> {
return ApiTokenService.apiTokenCreateToken(tokenIn).then((token) => {
this.apiTokensMapping[token.token_id] = token as ApiTokenOut;
return token;
});
},
deleteApiToken(tokenId: string): Promise<void> {
return ApiTokenService.apiTokenDeleteToken(tokenId).then(() => {
delete this.apiTokensMapping[tokenId];
......
......@@ -3,15 +3,23 @@ import { onMounted, reactive } from "vue";
import { useUserStore } from "@/stores/users";
import dayjs from "dayjs";
import DeleteModal from "@/components/modals/DeleteModal.vue";
import CreateApiTokenModal from "@/components/user/CreateApiTokenModal.vue";
import type { ApiTokenPrivateOut } from "@/client";
import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
import CopyToClipboardIcon from "@/components/CopyToClipboardIcon.vue";
const userRepository = useUserStore();
const state = reactive<{
loading: boolean;
deleteTokenId: string;
createdToken?: ApiTokenPrivateOut;
showCreatedToken: boolean;
}>({
loading: false,
deleteTokenId: "",
createdToken: undefined,
showCreatedToken: false,
});
function fetchTokens() {
......@@ -31,12 +39,25 @@ function confirmedDeleteApiToken() {
});
}
function apiTokenCreated(token: ApiTokenPrivateOut) {
state.createdToken = token;
}
function closeSuccessAlert() {
state.createdToken = undefined;
state.showCreatedToken = false;
}
onMounted(() => {
fetchTokens();
});
</script>
<template>
<create-api-token-modal
modal-id="create-personal-api-token-modal"
@token-created="apiTokenCreated"
/>
<delete-modal
modal-id="delete-personal-api-token-modal"
@confirm-delete="confirmedDeleteApiToken"
......@@ -49,11 +70,51 @@ onMounted(() => {
>
<h2 class="w-fit">Personal API tokens</h2>
<span class="w-fit" tabindex="0">
<button type="button" class="btn btn-primary" disabled>
<button
type="button"
class="btn btn-primary"
data-bs-toggle="modal"
data-bs-target="#create-personal-api-token-modal"
>
Create token
</button>
</span>
</div>
<div
v-if="state.createdToken"
class="alert alert-success alert-dismissible"
role="alert"
style="min-width: 50%"
>
<strong>Your new API token</strong>
<button
type="button"
class="btn-close"
aria-label="Close"
@click="closeSuccessAlert"
></button>
<br />
<div class="input-group my-2 w-fit">
<input
:type="state.showCreatedToken ? 'text' : 'password'"
class="form-control"
readonly
:value="state.createdToken.token"
/>
<div
class="input-group-text cursor-pointer hover-info"
@click="state.showCreatedToken = !state.showCreatedToken"
>
<font-awesome-icon
:icon="`fa-solid fa-eye${state.showCreatedToken ? '-slash' : ''}`"
/>
</div>
<div class="input-group-text">
<copy-to-clipboard-icon :text="state.createdToken.token" />
</div>
</div>
<div>Make sure you save it - you won't be able to access it again.</div>
</div>
<table class="table table-striped align-middle caption-top">
<caption>
Displaying
......
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