1
0
mirror of https://github.com/pocket-id/pocket-id.git synced 2026-02-04 11:36:46 +00:00

feat: add option to renew API key (#1214)

This commit is contained in:
Elias Schneider
2026-01-09 12:08:58 +01:00
committed by GitHub
parent 0a94f0fd64
commit 811e8772b6
16 changed files with 321 additions and 41 deletions

View File

@@ -506,6 +506,10 @@
"issuer_url": "Issuer URL",
"smtp_field_required_when_other_provided": "Required when any SMTP setting is provided",
"smtp_field_required_when_email_enabled": "Required when email notifications are enabled",
"renew": "Renew",
"renew_api_key": "Renew API Key",
"renew_api_key_description": "Renewing the API key will generate a new key. Make sure to update any integrations using this key.",
"api_key_renewed": "API key renewed",
"app_config_home_page": "Home Page",
"app_config_home_page_description": "The page users are redirected to after signing in."
}

View File

@@ -31,19 +31,6 @@
return new CalendarDate(d.getFullYear(), d.getMonth() + 1, d.getDate());
}
$effect(() => {
if (calendarDisplayDate) {
const newExternalDate = calendarDisplayDate.toDate(getLocalTimeZone());
if (!value || value.getTime() !== newExternalDate.getTime()) {
value = newExternalDate;
}
} else {
if (value !== undefined) {
value = undefined;
}
}
});
$effect(() => {
if (value) {
const newInternalCalendarDate = dateToCalendarDate(value);
@@ -59,6 +46,17 @@
function handleCalendarInteraction(newDateValue?: DateValue) {
open = false;
calendarDisplayDate = newDateValue as CalendarDate | undefined;
if (calendarDisplayDate) {
const newExternalDate = calendarDisplayDate.toDate(getLocalTimeZone());
if (!value || value.getTime() !== newExternalDate.getTime()) {
value = newExternalDate;
}
} else {
if (value !== undefined) {
value = undefined;
}
}
}
const df = new DateFormatter(getLocale(), {
@@ -89,8 +87,7 @@
<Popover.Content class="w-auto p-0" align="start">
<Calendar
type="single"
bind:value={calendarDisplayDate}
onValueChange={handleCalendarInteraction}
bind:value={() => calendarDisplayDate, (newValue) => handleCalendarInteraction(newValue)}
initialFocus
/>
</Popover.Content>

View File

@@ -13,6 +13,13 @@ export default class ApiKeyService extends APIService {
return res.data as ApiKeyResponse;
};
renew = async (id: string, expiresAt: Date): Promise<ApiKeyResponse> => {
const res = await this.api.post(`/api-keys/${id}/renew`, {
expiresAt
});
return res.data as ApiKeyResponse;
};
revoke = async (id: string): Promise<void> => {
await this.api.delete(`/api-keys/${id}`);
};

View File

@@ -76,4 +76,4 @@
</Card.Content>
</Card.Root>
<ApiKeyDialog bind:apiKeyResponse />
<ApiKeyDialog title={m.api_key_created()} bind:apiKeyResponse />

View File

@@ -6,8 +6,10 @@
import type { ApiKeyResponse } from '$lib/types/api-key.type';
let {
title,
apiKeyResponse = $bindable()
}: {
title: string;
apiKeyResponse: ApiKeyResponse | null;
} = $props();
@@ -21,7 +23,7 @@
<Dialog.Root open={!!apiKeyResponse} {onOpenChange}>
<Dialog.Content class="max-w-md" onOpenAutoFocus={(e) => e.preventDefault()}>
<Dialog.Header>
<Dialog.Title>{m.api_key_created()}</Dialog.Title>
<Dialog.Title>{title}</Dialog.Title>
<Dialog.Description>
{m.for_security_reasons_this_key_will_only_be_shown_once()}
</Dialog.Description>

View File

@@ -19,6 +19,7 @@
// Set default expiration to 30 days from now
const defaultExpiry = new Date();
defaultExpiry.setDate(defaultExpiry.getDate() + 30);
defaultExpiry.setHours(0, 0, 0, 0);
const apiKey = {
name: '',

View File

@@ -7,13 +7,17 @@
AdvancedTableColumn,
CreateAdvancedTableActions
} from '$lib/types/advanced-table.type';
import type { ApiKey } from '$lib/types/api-key.type';
import type { ApiKey, ApiKeyResponse } from '$lib/types/api-key.type';
import { axiosErrorToast } from '$lib/utils/error-util';
import { LucideBan } from '@lucide/svelte';
import { LucideBan, LucideRefreshCcw, LucideTriangleAlert } from '@lucide/svelte';
import { toast } from 'svelte-sonner';
import ApiKeyDialog from './api-key-dialog.svelte';
import RenewApiKeyModal from './renew-api-key-modal.svelte';
const apiKeyService = new ApiKeyService();
let apiKeyToRenew: ApiKey | null = $state(null);
let renewedApiKey: ApiKeyResponse | null = $state(null);
let tableRef: AdvancedTable<ApiKey>;
export function refresh() {
@@ -35,7 +39,7 @@
label: m.expires_at(),
column: 'expiresAt',
sortable: true,
value: (item) => formatDate(item.expiresAt)
cell: ExpirationCell
},
{
label: m.last_used(),
@@ -53,6 +57,13 @@
];
const actions: CreateAdvancedTableActions<ApiKey> = (apiKey) => [
{
label: m.renew(),
icon: LucideRefreshCcw,
variant: 'primary',
hidden: new Date(apiKey.expiresAt) > new Date(),
onClick: (apiKey) => (apiKeyToRenew = apiKey)
},
{
label: m.revoke(),
icon: LucideBan,
@@ -61,6 +72,21 @@
}
];
async function renewApiKey(expirationDate: Date) {
if (!apiKeyToRenew) return;
await apiKeyService
.renew(apiKeyToRenew.id, expirationDate)
.then(async (response) => {
renewedApiKey = response;
await refresh();
apiKeyToRenew = null;
})
.catch((e) => {
axiosErrorToast(e);
});
}
function revokeApiKey(apiKey: ApiKey) {
openConfirmDialog({
title: m.revoke_api_key(),
@@ -84,6 +110,20 @@
}
</script>
{#snippet ExpirationCell({ item }: { item: ApiKey })}
{@const expired = new Date(item.expiresAt) <= new Date()}
<span
class={{
'flex gap-2 items-center': true,
'text-orange-300': expired
}}
>{formatDate(item.expiresAt)}
{#if expired}
<LucideTriangleAlert class="size-4" />
{/if}
</span>
{/snippet}
<AdvancedTable
id="api-key-list"
bind:this={tableRef}
@@ -93,3 +133,6 @@
{columns}
{actions}
/>
<ApiKeyDialog title={m.api_key_renewed()} bind:apiKeyResponse={renewedApiKey} />
<RenewApiKeyModal bind:apiKey={apiKeyToRenew} onRenew={renewApiKey} />

View File

@@ -0,0 +1,52 @@
<script lang="ts">
import DatePicker from '$lib/components/form/date-picker.svelte';
import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
import * as Field from '$lib/components/ui/field/index.js';
import { m } from '$lib/paraglide/messages';
import type { ApiKey } from '$lib/types/api-key.type';
let {
apiKey = $bindable(null),
onRenew
}: {
apiKey: ApiKey | null;
onRenew: (date: Date) => Promise<void>;
} = $props();
let date = $state(new Date());
$effect(() => {
if (apiKey) {
const lastExpirationDuration =
new Date(apiKey.expiresAt).getTime() - new Date(apiKey.createdAt).getTime();
date = new Date(Date.now() + lastExpirationDuration);
}
});
function onOpenChange(open: boolean) {
if (!open) {
apiKey = null;
}
}
</script>
<Dialog.Root open={!!apiKey} {onOpenChange}>
<Dialog.Content class="max-w-md" onOpenAutoFocus={(e) => e.preventDefault()}>
<Dialog.Header>
<Dialog.Title>{m.renew_api_key()}</Dialog.Title>
<Dialog.Description>
{m.renew_api_key_description()}
</Dialog.Description>
</Dialog.Header>
<Field.Field>
<Field.Label>{m.expiration()}</Field.Label>
<DatePicker bind:value={date} />
</Field.Field>
<Dialog.Footer class="mt-3">
<Button variant="outline" onclick={() => onOpenChange(false)}>{m.cancel()}</Button>
<Button variant="default" usePromiseLoading onclick={() => onRenew(date)}>{m.renew()}</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>