|
| 1 | +<script lang="ts"> |
| 2 | + import { stores } from "@sapper/app"; |
| 3 | + import type { ApiKey } from "@koj/types"; |
| 4 | + import { api, can } from "../../../api"; |
| 5 | + import DataTable from "../../../components/DataTable.svelte"; |
| 6 | + import DeleteModal from "../../../components/DeleteModal.svelte"; |
| 7 | + import { onMount } from "svelte"; |
| 8 | + import Form from "../../../components/Form.svelte"; |
| 9 | + import DeleteButton from "../../../components/Table/DeleteButton.svelte"; |
| 10 | + import EditButton from "../../../components/Table/EditButton.svelte"; |
| 11 | + import Modal from "../../../components/Modal.svelte"; |
| 12 | +
|
| 13 | + onMount(async () => { |
| 14 | + scopes = await api<Record<string, string>>({ |
| 15 | + method: "GET", |
| 16 | + url: `/groups/${slug}/api-keys/scopes`, |
| 17 | + }); |
| 18 | + }); |
| 19 | +
|
| 20 | + const { page } = stores(); |
| 21 | + const { slug } = $page.params; |
| 22 | + const primaryKeyType = "id"; |
| 23 | + let data: ApiKey[] = []; |
| 24 | + let scopes: Record<string, string> = {}; |
| 25 | + let editActive: ApiKey | undefined = undefined; |
| 26 | + let deleteActiveKey: number | undefined = undefined; |
| 27 | + let showActiveKeys: number[] = []; |
| 28 | +
|
| 29 | + const updateData = (item: ApiKey) => { |
| 30 | + if (data.find((i) => i[primaryKeyType] === item[primaryKeyType])) |
| 31 | + data = data.map((i) => { |
| 32 | + if (i[primaryKeyType] === item[primaryKeyType]) return item; |
| 33 | + return i; |
| 34 | + }); |
| 35 | + else data = [...data, item]; |
| 36 | + }; |
| 37 | +
|
| 38 | + const add = async (body: { name: string; scopes: string[] }) => { |
| 39 | + const result = await api<ApiKey>({ |
| 40 | + method: "POST", |
| 41 | + url: `/groups/${slug}/api-keys`, |
| 42 | + body, |
| 43 | + }); |
| 44 | + updateData(result); |
| 45 | + }; |
| 46 | +
|
| 47 | + const edit = async (body: { name: string; scopes: string[] }) => { |
| 48 | + if (!editActive) return; |
| 49 | + const result = await api<ApiKey>({ |
| 50 | + method: "PATCH", |
| 51 | + url: `/groups/${slug}/api-keys/${editActive[primaryKeyType]}`, |
| 52 | + body, |
| 53 | + }); |
| 54 | + updateData(result); |
| 55 | + editActive = undefined; |
| 56 | + }; |
| 57 | +
|
| 58 | + const toggleActive = (id: number) => { |
| 59 | + if (showActiveKeys.includes(id)) showActiveKeys = showActiveKeys.filter((i) => i !== id); |
| 60 | + else showActiveKeys = [...showActiveKeys, id]; |
| 61 | + }; |
| 62 | +</script> |
| 63 | + |
| 64 | +<svelte:head> |
| 65 | + <title>API keys</title> |
| 66 | +</svelte:head> |
| 67 | + |
| 68 | +<DataTable |
| 69 | + let:item |
| 70 | + {data} |
| 71 | + title="API keys" |
| 72 | + itemName="API keys" |
| 73 | + titleKey="apiKey" |
| 74 | + text="API keys are used to programmatically access features using the API." |
| 75 | + endpoint={`/groups/${slug}/api-keys`} |
| 76 | + headers={['API key', 'Scopes', 'Restrictions']} |
| 77 | + onData={(val) => (data = val)} |
| 78 | + {primaryKeyType} |
| 79 | + filters={[{ title: 'ID', name: 'id', type: 'int' }, { title: 'Created at', name: 'createdAt', type: 'datetime' }, { title: 'Updated at', name: 'updatedAt', type: 'datetime' }]}> |
| 80 | + {#if !showActiveKeys.includes(item.id)} |
| 81 | + <td class="px-7 py-4 whitespace-nowrap text-sm">{item.name}</td> |
| 82 | + {:else} |
| 83 | + <td class="px-7 py-4 whitespace-nowrap text-sm"> |
| 84 | + <code class="rounded border">{item.apiKey}</code> |
| 85 | + </td> |
| 86 | + {/if} |
| 87 | + <td class="px-7 py-4 whitespace-nowrap text-sm text-gray-500">{item.scopes.length}</td> |
| 88 | + <td class="px-7 py-4 whitespace-nowrap text-sm text-gray-500"> |
| 89 | + {(item.ipRestrictions || []).length + (item.referrerRestrictions || []).length} |
| 90 | + </td> |
| 91 | + <td class="px-7 py-4 whitespace-nowrap text-right text-sm font-medium"> |
| 92 | + <button |
| 93 | + aria-label={showActiveKeys.includes(item.id) ? 'Hide API key' : 'Show API key'} |
| 94 | + data-balloon-pos="up" |
| 95 | + class="text-gray-500 hover:text-indigo-700 transition motion-reduce:transition-none ml-2 align-middle focus:text-indigo-700" |
| 96 | + on:click={() => toggleActive(item.id)}> |
| 97 | + {#if showActiveKeys.includes(item.id)} |
| 98 | + <svg |
| 99 | + class="w-5 h-5" |
| 100 | + fill="none" |
| 101 | + stroke="currentColor" |
| 102 | + viewBox="0 0 24 24" |
| 103 | + xmlns="http://www.w3.org/2000/svg"><path |
| 104 | + stroke-linecap="round" |
| 105 | + stroke-linejoin="round" |
| 106 | + stroke-width="2" |
| 107 | + d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" /></svg> |
| 108 | + {:else} |
| 109 | + <svg |
| 110 | + class="w-5 h-5" |
| 111 | + fill="none" |
| 112 | + stroke="currentColor" |
| 113 | + viewBox="0 0 24 24" |
| 114 | + xmlns="http://www.w3.org/2000/svg"><path |
| 115 | + stroke-linecap="round" |
| 116 | + stroke-linejoin="round" |
| 117 | + stroke-width="2" |
| 118 | + d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> |
| 119 | + <path |
| 120 | + stroke-linecap="round" |
| 121 | + stroke-linejoin="round" |
| 122 | + stroke-width="2" |
| 123 | + d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /></svg> |
| 124 | + {/if} |
| 125 | + </button> |
| 126 | + {#if can(`api-key:write-info-${item[primaryKeyType]}`)} |
| 127 | + <EditButton on:click={() => (editActive = item)} /> |
| 128 | + {/if} |
| 129 | + {#if can(`api-key:write-info-${item[primaryKeyType]}`)} |
| 130 | + <DeleteButton on:click={() => (deleteActiveKey = item[primaryKeyType])} /> |
| 131 | + {/if} |
| 132 | + </td> |
| 133 | +</DataTable> |
| 134 | + |
| 135 | +<div class="p-7"> |
| 136 | + <Form |
| 137 | + title="Add API key" |
| 138 | + text="You can create another API key with specific permissions. You can add IP address range and referrer restrictions after creating the API key." |
| 139 | + items={[{ name: 'name', label: 'Name', required: true }, { name: 'scopes', label: 'Scopes', type: 'multiple', options: scopes, required: true }]} |
| 140 | + onSubmit={add} |
| 141 | + submitText="Add API key" /> |
| 142 | +</div> |
| 143 | + |
| 144 | +{#if deleteActiveKey} |
| 145 | + <DeleteModal |
| 146 | + title="Delete API key" |
| 147 | + text="Are you sure you want to permanently delete this API key? It will stop working immediately." |
| 148 | + onClose={() => (deleteActiveKey = undefined)} |
| 149 | + url={`/groups/${slug}/api-keys/${deleteActiveKey}`} |
| 150 | + onSuccess={() => { |
| 151 | + data = data.filter((i) => i[primaryKeyType] !== deleteActiveKey); |
| 152 | + deleteActiveKey = undefined; |
| 153 | + }} /> |
| 154 | +{/if} |
| 155 | + |
| 156 | +{#if editActive} |
| 157 | + <Modal onClose={() => (editActive = undefined)}> |
| 158 | + <Form |
| 159 | + title="Edit API key" |
| 160 | + items={[{ name: 'name', label: 'Name', required: true }, { name: 'description', label: 'Description', type: 'textarea' }, { name: 'scopes', label: 'Scopes', type: 'multiple', options: scopes, required: true }, { name: 'ipRestrictions', label: 'IP address restrictions', type: 'array', hint: 'Enter a comma-separated list of IP CIDRs' }, { name: 'referrerRestrictions', label: 'Referrer restrictions', type: 'array', hint: 'Enter a comma-separated list of hostnames' }]} |
| 161 | + values={editActive} |
| 162 | + onSubmit={edit} |
| 163 | + submitText="Save API key" |
| 164 | + onClose={() => (editActive = undefined)} |
| 165 | + modal={true} /> |
| 166 | + </Modal> |
| 167 | +{/if} |
0 commit comments