Skip to content
This repository has been archived by the owner on Apr 19, 2023. It is now read-only.

Commit

Permalink
✨ Add group routes
Browse files Browse the repository at this point in the history
  • Loading branch information
AnandChowdhary committed Jan 11, 2021
1 parent dd66f28 commit e426608
Show file tree
Hide file tree
Showing 11 changed files with 585 additions and 5 deletions.
32 changes: 32 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,35 @@ export const loginWithTokenResponse = async (auth: User["auth"]) => {
);
activeUserIndex.set(loggedInUsers.length - 1);
};

export const refresh = async () => {
try {
const res = await fetch(`${BASE_URL}/auth/refresh`, {
method: "POST",
body: JSON.stringify({
token: loggedInUsers[index].auth.refreshToken,
}),
headers: {
"X-Requested-With": "XmlHttpRequest",
Accept: "application/json",
"Content-Type": "application/json",
},
});
const {
accessToken,
refreshToken,
}: { accessToken: string; refreshToken: string } = await res.json();
users.update((val) =>
val.map((user, i) => {
if (index === i) {
return {
details: user.details,
memberships: user.memberships,
auth: { accessToken, refreshToken },
};
}
return user;
})
);
} catch (error) {}
};
2 changes: 1 addition & 1 deletion src/components/Table/GroupRecord.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</script>

<a
href={`/groups/${data.id}/summary`}
href={`/groups/${data.id}/details`}
class="inline-flex items-center transition motion-reduce:transition-none hover:opacity-50">
<div class="flex-shrink-0 h-10 w-10">
<img
Expand Down
2 changes: 1 addition & 1 deletion src/routes/admin/audit-logs.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
class="text-indigo-600"
aria-label={item.group ? item.group.name : undefined}
data-balloon-pos={item.group ? 'up' : undefined}
href={`/groups/${item.groupId}/summary`}>#{item.groupId}</a>
href={`/groups/${item.groupId}/details`}>#{item.groupId}</a>
</div>
{/if}
</div>
Expand Down
31 changes: 31 additions & 0 deletions src/routes/groups/[slug]/_layout.svelte
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>
167 changes: 167 additions & 0 deletions src/routes/groups/[slug]/api-keys.svelte
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}
72 changes: 72 additions & 0 deletions src/routes/groups/[slug]/audit-logs.svelte
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>
65 changes: 65 additions & 0 deletions src/routes/groups/[slug]/details.svelte
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>
Loading

0 comments on commit e426608

Please sign in to comment.