This repository has been archived by the owner on Apr 19, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 65
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
dd66f28
commit e426608
Showing
11 changed files
with
585 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<script lang="ts"> | ||
import { stores } from "@sapper/app"; | ||
const { page } = stores(); | ||
const { slug } = $page.params; | ||
const nav = [ | ||
{ title: "Details", slug: "details" }, | ||
{ title: "Members", slug: "members" }, | ||
{ title: "Security", slug: "security" }, | ||
{ title: "API keys", slug: "api-keys" }, | ||
{ title: "Audit logs", slug: "audit-logs" }, | ||
]; | ||
</script> | ||
|
||
<div class="max-w-7xl my-10 mx-auto px-4 sm:px-6 lg:px-8"> | ||
<div class="md:grid md:grid-cols-4 md:gap-6"> | ||
<div class="md:col-span-1"> | ||
<div class="shadow bg-white sm:rounded-md overflow-hidden top-5 sticky"> | ||
{#each nav as item} | ||
<a | ||
href={`/groups/${slug}/${item.slug}`} | ||
class={$page.path === `/groups/${slug}/${item.slug}` ? 'block px-4 py-3 text-sm transition motion-reduce:transition-none text-gray-700 bg-gray-200 font-bold focus:outline-none focus:ring-0 focus:text-gray-900 focus:bg-gray-300' : 'block px-4 py-3 text-sm transition motion-reduce:transition-none text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:bg-gray-100 focus:text-gray-900 focus:outline-none focus:ring-0'} | ||
role="menuitem">{item.title}</a> | ||
{/each} | ||
</div> | ||
</div> | ||
<div class="md:mt-0 md:col-span-3 shadow bg-white sm:rounded-md overflow-hidden"> | ||
<slot /> | ||
</div> | ||
</div> | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
<script lang="ts"> | ||
import { stores } from "@sapper/app"; | ||
import type { ApiKey } from "@koj/types"; | ||
import { api, can } from "../../../api"; | ||
import DataTable from "../../../components/DataTable.svelte"; | ||
import DeleteModal from "../../../components/DeleteModal.svelte"; | ||
import { onMount } from "svelte"; | ||
import Form from "../../../components/Form.svelte"; | ||
import DeleteButton from "../../../components/Table/DeleteButton.svelte"; | ||
import EditButton from "../../../components/Table/EditButton.svelte"; | ||
import Modal from "../../../components/Modal.svelte"; | ||
onMount(async () => { | ||
scopes = await api<Record<string, string>>({ | ||
method: "GET", | ||
url: `/groups/${slug}/api-keys/scopes`, | ||
}); | ||
}); | ||
const { page } = stores(); | ||
const { slug } = $page.params; | ||
const primaryKeyType = "id"; | ||
let data: ApiKey[] = []; | ||
let scopes: Record<string, string> = {}; | ||
let editActive: ApiKey | undefined = undefined; | ||
let deleteActiveKey: number | undefined = undefined; | ||
let showActiveKeys: number[] = []; | ||
const updateData = (item: ApiKey) => { | ||
if (data.find((i) => i[primaryKeyType] === item[primaryKeyType])) | ||
data = data.map((i) => { | ||
if (i[primaryKeyType] === item[primaryKeyType]) return item; | ||
return i; | ||
}); | ||
else data = [...data, item]; | ||
}; | ||
const add = async (body: { name: string; scopes: string[] }) => { | ||
const result = await api<ApiKey>({ | ||
method: "POST", | ||
url: `/groups/${slug}/api-keys`, | ||
body, | ||
}); | ||
updateData(result); | ||
}; | ||
const edit = async (body: { name: string; scopes: string[] }) => { | ||
if (!editActive) return; | ||
const result = await api<ApiKey>({ | ||
method: "PATCH", | ||
url: `/groups/${slug}/api-keys/${editActive[primaryKeyType]}`, | ||
body, | ||
}); | ||
updateData(result); | ||
editActive = undefined; | ||
}; | ||
const toggleActive = (id: number) => { | ||
if (showActiveKeys.includes(id)) showActiveKeys = showActiveKeys.filter((i) => i !== id); | ||
else showActiveKeys = [...showActiveKeys, id]; | ||
}; | ||
</script> | ||
|
||
<svelte:head> | ||
<title>API keys</title> | ||
</svelte:head> | ||
|
||
<DataTable | ||
let:item | ||
{data} | ||
title="API keys" | ||
itemName="API keys" | ||
titleKey="apiKey" | ||
text="API keys are used to programmatically access features using the API." | ||
endpoint={`/groups/${slug}/api-keys`} | ||
headers={['API key', 'Scopes', 'Restrictions']} | ||
onData={(val) => (data = val)} | ||
{primaryKeyType} | ||
filters={[{ title: 'ID', name: 'id', type: 'int' }, { title: 'Created at', name: 'createdAt', type: 'datetime' }, { title: 'Updated at', name: 'updatedAt', type: 'datetime' }]}> | ||
{#if !showActiveKeys.includes(item.id)} | ||
<td class="px-7 py-4 whitespace-nowrap text-sm">{item.name}</td> | ||
{:else} | ||
<td class="px-7 py-4 whitespace-nowrap text-sm"> | ||
<code class="rounded border">{item.apiKey}</code> | ||
</td> | ||
{/if} | ||
<td class="px-7 py-4 whitespace-nowrap text-sm text-gray-500">{item.scopes.length}</td> | ||
<td class="px-7 py-4 whitespace-nowrap text-sm text-gray-500"> | ||
{(item.ipRestrictions || []).length + (item.referrerRestrictions || []).length} | ||
</td> | ||
<td class="px-7 py-4 whitespace-nowrap text-right text-sm font-medium"> | ||
<button | ||
aria-label={showActiveKeys.includes(item.id) ? 'Hide API key' : 'Show API key'} | ||
data-balloon-pos="up" | ||
class="text-gray-500 hover:text-indigo-700 transition motion-reduce:transition-none ml-2 align-middle focus:text-indigo-700" | ||
on:click={() => toggleActive(item.id)}> | ||
{#if showActiveKeys.includes(item.id)} | ||
<svg | ||
class="w-5 h-5" | ||
fill="none" | ||
stroke="currentColor" | ||
viewBox="0 0 24 24" | ||
xmlns="http://www.w3.org/2000/svg"><path | ||
stroke-linecap="round" | ||
stroke-linejoin="round" | ||
stroke-width="2" | ||
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> | ||
{:else} | ||
<svg | ||
class="w-5 h-5" | ||
fill="none" | ||
stroke="currentColor" | ||
viewBox="0 0 24 24" | ||
xmlns="http://www.w3.org/2000/svg"><path | ||
stroke-linecap="round" | ||
stroke-linejoin="round" | ||
stroke-width="2" | ||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> | ||
<path | ||
stroke-linecap="round" | ||
stroke-linejoin="round" | ||
stroke-width="2" | ||
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> | ||
{/if} | ||
</button> | ||
{#if can(`api-key:write-info-${item[primaryKeyType]}`)} | ||
<EditButton on:click={() => (editActive = item)} /> | ||
{/if} | ||
{#if can(`api-key:write-info-${item[primaryKeyType]}`)} | ||
<DeleteButton on:click={() => (deleteActiveKey = item[primaryKeyType])} /> | ||
{/if} | ||
</td> | ||
</DataTable> | ||
|
||
<div class="p-7"> | ||
<Form | ||
title="Add API key" | ||
text="You can create another API key with specific permissions. You can add IP address range and referrer restrictions after creating the API key." | ||
items={[{ name: 'name', label: 'Name', required: true }, { name: 'scopes', label: 'Scopes', type: 'multiple', options: scopes, required: true }]} | ||
onSubmit={add} | ||
submitText="Add API key" /> | ||
</div> | ||
|
||
{#if deleteActiveKey} | ||
<DeleteModal | ||
title="Delete API key" | ||
text="Are you sure you want to permanently delete this API key? It will stop working immediately." | ||
onClose={() => (deleteActiveKey = undefined)} | ||
url={`/groups/${slug}/api-keys/${deleteActiveKey}`} | ||
onSuccess={() => { | ||
data = data.filter((i) => i[primaryKeyType] !== deleteActiveKey); | ||
deleteActiveKey = undefined; | ||
}} /> | ||
{/if} | ||
|
||
{#if editActive} | ||
<Modal onClose={() => (editActive = undefined)}> | ||
<Form | ||
title="Edit API key" | ||
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' }]} | ||
values={editActive} | ||
onSubmit={edit} | ||
submitText="Save API key" | ||
onClose={() => (editActive = undefined)} | ||
modal={true} /> | ||
</Modal> | ||
{/if} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
<script lang="ts"> | ||
import { stores } from "@sapper/app"; | ||
import type { AuditLog } from "@koj/types"; | ||
import { saveAs } from "file-saver"; | ||
import DataTable from "../../../components/DataTable.svelte"; | ||
import LocationAgentIcons from "../../../components/LocationAgentIcons.svelte"; | ||
import UserRecord from "../../../components/Table/UserRecord.svelte"; | ||
import DownloadButton from "../../../components/Table/DownloadButton.svelte"; | ||
import TimeAgo from "../../../components/TimeAgo.svelte"; | ||
const { page } = stores(); | ||
const { slug } = $page.params; | ||
const primaryKeyType = "id"; | ||
let data: AuditLog[] = []; | ||
const download = (record: AuditLog) => | ||
saveAs(new Blob([JSON.stringify(record, null, 2)]), `audit-log-${record.id}.json`); | ||
</script> | ||
|
||
<svelte:head> | ||
<title>Audit logs</title> | ||
</svelte:head> | ||
|
||
<DataTable | ||
let:item | ||
{data} | ||
title="Audit logs" | ||
titleKey="event" | ||
itemName="audit logs" | ||
text="These audit logs offer detailed insights into user-made changes. Audit logs are auto-deleted after 90 days." | ||
endpoint={`/groups/${slug}/audit-logs`} | ||
headers={['Date', 'User', 'Event', 'Device']} | ||
onData={(val) => (data = val)} | ||
{primaryKeyType} | ||
filters={[{ name: primaryKeyType, title: 'ID', type: 'int' }, { name: 'event', title: 'Event', type: 'string' }, { name: 'rawEvent', title: 'Event type', type: 'string' }, { name: 'groupId', title: 'Group ID', type: 'int' }, { name: 'userId', title: 'User ID', type: 'int' }, { name: 'ipAddress', title: 'IP address', type: 'string' }, { name: 'userAgent', title: 'User agent', type: 'string' }, { name: 'browser', title: 'Browser', type: 'string' }, { name: 'operatingSystem', title: 'Operating system', type: 'string' }, { name: 'city', title: 'City', type: 'string' }, { name: 'region', title: 'Region', type: 'string' }, { name: 'countryCode', title: 'Country code', type: 'string' }, { name: 'createdAt', title: 'Created at', type: 'datetime' }, { name: 'updatedAt', title: 'Updated at', type: 'datetime' }]}> | ||
<td class="px-7 py-4 whitespace-nowrap text-sm text-gray-500"> | ||
<TimeAgo date={item.createdAt} /> | ||
</td> | ||
<td class="px-7 py-4 whitespace-nowrap text-sm"> | ||
{#if item.user} | ||
<UserRecord item={item.user} iconOnly={true} /> | ||
{/if} | ||
</td> | ||
<td class="px-7 py-4 whitespace-nowrap text-sm"> | ||
<div class="font-medium"> | ||
<a | ||
href={`/admin/audit-logs?q=${encodeURIComponent(`event: ${item.event}`)}`}><code>{item.event}</code></a> | ||
</div> | ||
<div class="text-gray-500 mt-1"> | ||
{#if item.groupId} | ||
<div> | ||
Group | ||
<a | ||
class="text-indigo-600" | ||
aria-label={item.group ? item.group.name : undefined} | ||
data-balloon-pos={item.group ? 'up' : undefined} | ||
href={`/groups/${item.groupId}/details`}>#{item.groupId}</a> | ||
</div> | ||
{/if} | ||
</div> | ||
</td> | ||
<td class="px-7 py-4 whitespace-nowrap text-sm"> | ||
<LocationAgentIcons | ||
browserHref={`/admin/audit-logs?q=${encodeURIComponent(`browser: contains ${(item.browser || '').split(' ')[0]}`)}`} | ||
operatingSystemHref={`/admin/audit-logs?q=${encodeURIComponent(`operatingSystem: contains ${(item.operatingSystem || '').split(' ')[0]}`)}`} | ||
countryCodeHref={`/admin/audit-logs?q=${encodeURIComponent(`countryCode: ${item.countryCode}`)}`} | ||
{item} /> | ||
</td> | ||
<td class="px-7 py-4 whitespace-nowrap text-right"> | ||
<DownloadButton on:click={() => download(item)} /> | ||
</td> | ||
</DataTable> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
<script lang="ts"> | ||
import { stores } from "@sapper/app"; | ||
import { onMount } from "svelte"; | ||
import { api } from "../../../api"; | ||
import Error from "../../../components/Error.svelte"; | ||
import Form from "../../../components/Form.svelte"; | ||
import { activeNotification } from "../../../stores"; | ||
const { page } = stores(); | ||
const { slug } = $page.params; | ||
let state = "ready"; | ||
let error = ""; | ||
let data: any = {}; | ||
onMount(() => { | ||
fetch(); | ||
}); | ||
const fetch = async () => { | ||
state = "fetching"; | ||
try { | ||
data = await api<any>({ | ||
method: "GET", | ||
url: `/groups/${slug}`, | ||
onCachedResponse: (result) => (data = result), | ||
}); | ||
error = ""; | ||
} catch (err) { | ||
error = err.message; | ||
} | ||
state = "ready"; | ||
}; | ||
const edit = async (body: { name: string }) => { | ||
data = await api<any>({ | ||
method: "PATCH", | ||
url: `/groups/${slug}`, | ||
body, | ||
}); | ||
error = ""; | ||
activeNotification.set({ | ||
text: "Group has been updated", | ||
type: "success", | ||
}); | ||
}; | ||
</script> | ||
|
||
<svelte:head> | ||
<title>Details</title> | ||
</svelte:head> | ||
|
||
<div class="p-7"> | ||
<h1 class="text-2xl">Details</h1> | ||
{#if error} | ||
<Error {error} /> | ||
{/if} | ||
{#if state === 'fetching'} | ||
<div class={`loading ${data ? 'loading-has' : 'h-52 bg-gray-50'}`} /> | ||
{/if} | ||
<Form | ||
items={[{ name: 'name', label: 'Name', required: true }]} | ||
values={data} | ||
submitText="Save security settings" | ||
onSubmit={edit} /> | ||
</div> |
Oops, something went wrong.