Feature: API Tokens (#848)

* initial commit

* fix last_used timestamp

---------

Co-authored-by: Christian Beutel <>
This commit is contained in:
Flomp
2026-03-07 10:10:25 +01:00
committed by GitHub
parent ba88d08078
commit 711a592038
26 changed files with 781 additions and 8 deletions

View File

@@ -1,12 +1,14 @@
<script lang="ts">
import { _ } from "svelte-i18n";
import type { ChangeEventHandler } from "svelte/elements";
interface Props {
name?: string;
value?: string | Date;
label?: string;
error?: string | string[] | null;
min?: string | number;
max?: string | number;
onchange?: ChangeEventHandler<HTMLInputElement>;
}
@@ -15,7 +17,9 @@
value = $bindable(),
label = "",
error = "",
onchange
min,
max,
onchange,
}: Props = $props();
</script>
@@ -32,6 +36,8 @@
class:border-red-400={(error?.length ?? 0) > 0}
class:bg-input-background-error={(error?.length ?? 0) > 0}
type="date"
{min}
{max}
bind:value
{onchange}
/>

View File

@@ -0,0 +1,88 @@
<script lang="ts">
import Modal from "$lib/components/base/modal.svelte";
import { APITokenCreateSchema } from "$lib/models/api/api_token_schema";
import type { APIToken } from "$lib/models/api_token";
import { validator } from "@felte/validator-zod";
import { createForm } from "felte";
import { _ } from "svelte-i18n";
import { z } from "zod";
import Datepicker from "../base/datepicker.svelte";
import TextField from "../base/text_field.svelte";
import Toggle from "../base/toggle.svelte";
interface Props {
onsave?: (token: APIToken) => void;
}
let { onsave }: Props = $props();
let modal: Modal;
let expires: boolean = $state(false);
const today = new Date();
today.setDate(today.getDate() + 1);
const tomorrow = today.toISOString().split("T")[0];
export function openModal() {
modal.openModal();
setFields("name", "");
setFields("expiration", undefined);
setErrors("name", []);
setErrors("expiration", []);
}
const { form, errors, setFields, setErrors } = createForm<
z.infer<typeof APITokenCreateSchema>
>({
initialValues: {
name: "",
expiration: "",
},
extend: validator({
schema: APITokenCreateSchema.extend({
user: z.string().optional(),
}),
}),
onSubmit: async (form) => {
onsave?.(form);
modal.closeModal!();
},
});
</script>
<Modal
id="api-token-modal"
size="md:min-w-md"
title={$_("generate-new-token")}
bind:this={modal}
>
{#snippet content()}
<form class="space-y-4" id="api-token-form" use:form>
<TextField label={$_("name")} name="name" error={$errors.name}
></TextField>
<Toggle label={$_("expires")} bind:value={expires}></Toggle>
{#if expires}
<Datepicker
label={$_("expiration")}
name="expiration"
error={$errors.expiration}
min={tomorrow}
></Datepicker>
{/if}
</form>
{/snippet}
{#snippet footer()}
<div class="flex items-center gap-4">
<button class="btn-secondary" onclick={() => modal.closeModal()}
>{$_("cancel")}</button
>
<button
class="btn-primary"
type="submit"
form="api-token-form"
name="save">{$_("save")}</button
>
</div>
{/snippet}</Modal
>

View File

@@ -0,0 +1,64 @@
<script lang="ts">
import Modal from "$lib/components/base/modal.svelte";
import { _ } from "svelte-i18n";
import TextField from "../base/text_field.svelte";
interface Props {
token: string | null;
}
let { token = $bindable() }: Props = $props();
let modal: Modal;
let copied: boolean = $state(false);
export function openModal() {
copied = false;
modal.openModal();
}
export function closeModal() {
token = null;
modal.closeModal();
}
function copyTokenToClipboard() {
if (!token) {
return;
}
navigator.clipboard.writeText(token);
copied = true;
}
</script>
<Modal
id="api-token-success-modal"
size="md:min-w-md"
title={$_("new-token-generated")}
bind:this={modal}
>
{#snippet content()}
<p class="max-w-lg">
Please copy your new API token now. For your security, we cannot
show it to you again. If you lose this token, you will need to
delete it and generate a new one.
</p>
<div class="flex items-center gap-x-4 flex-nowrap">
<div class="flex-1">
<TextField value={token ?? ""} disabled></TextField>
</div>
<button
class="btn-secondary"
onclick={() => copyTokenToClipboard()}
aria-label="copy token to clipboard"
><i class="fa {copied ? 'fa-check' : 'fa-clipboard'}"
></i></button
>
</div>
{/snippet}
{#snippet footer()}
<button class="btn-primary" onclick={() => modal.closeModal()}
>{$_("close")}</button
>
{/snippet}</Modal
>