feat: add API tokens + force log out button
This commit is contained in:
parent
9c8b6a8f91
commit
2716471fa9
9 changed files with 207 additions and 52 deletions
|
@ -17,11 +17,6 @@
|
|||
$: usernameValid = usernameRegex.test(username);
|
||||
let error: APIError | null = null;
|
||||
|
||||
let deleteOpen = false;
|
||||
const toggleDeleteOpen = () => (deleteOpen = !deleteOpen);
|
||||
let deleteUsername = "";
|
||||
let deleteError: APIError | null = null;
|
||||
|
||||
const changeUsername = async () => {
|
||||
try {
|
||||
const resp = await apiFetchClient<MeUser>("/users/@me", "PATCH", { username });
|
||||
|
@ -35,6 +30,11 @@
|
|||
}
|
||||
};
|
||||
|
||||
let deleteOpen = false;
|
||||
const toggleDeleteOpen = () => (deleteOpen = !deleteOpen);
|
||||
let deleteUsername = "";
|
||||
let deleteError: APIError | null = null;
|
||||
|
||||
const deleteAccount = async () => {
|
||||
try {
|
||||
await fastFetchClient("/users/@me", "DELETE");
|
||||
|
@ -50,6 +50,29 @@
|
|||
deleteError = e as APIError;
|
||||
}
|
||||
};
|
||||
|
||||
let invalidateModalOpen = false;
|
||||
const toggleInvalidateModalOpen = () => (invalidateModalOpen = !invalidateModalOpen);
|
||||
let invalidateError: APIError | null = null;
|
||||
|
||||
const invalidateAllTokens = async () => {
|
||||
try {
|
||||
await fastFetchClient("/auth/tokens", "DELETE");
|
||||
|
||||
invalidateError = null;
|
||||
userStore.set(null);
|
||||
localStorage.removeItem("pronouns-token");
|
||||
localStorage.removeItem("pronouns-user");
|
||||
toggleInvalidateModalOpen();
|
||||
addToast({
|
||||
header: "Invalidated tokens",
|
||||
body: "Invalidated all your tokens, please log in again.",
|
||||
});
|
||||
goto("/");
|
||||
} catch (e) {
|
||||
invalidateError = e as APIError;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<h1>Your profile</h1>
|
||||
|
@ -93,6 +116,31 @@
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h4>Force log out</h4>
|
||||
<p>
|
||||
If you think one of your tokens might have been compromised, you can log out on all devices
|
||||
by clicking this button.
|
||||
</p>
|
||||
<p>
|
||||
<Button color="danger" on:click={toggleInvalidateModalOpen}>Force log out</Button>
|
||||
</p>
|
||||
<Modal isOpen={invalidateModalOpen} toggle={toggleInvalidateModalOpen}>
|
||||
<ModalHeader toggle={toggleInvalidateModalOpen}>Force log out</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>If you want to force log out on all devices, click the button below.</p>
|
||||
{#if invalidateError}
|
||||
<ErrorAlert error={invalidateError} />
|
||||
{/if}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" on:click={invalidateAllTokens}>Force log out</Button>
|
||||
<Button color="secondary" on:click={toggleInvalidateModalOpen}>Cancel</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h4>Account info</h4>
|
||||
|
|
|
@ -1,36 +1,89 @@
|
|||
<script lang="ts">
|
||||
import type { APIError } from "$lib/api/entities";
|
||||
import { apiFetchClient, fastFetchClient } from "$lib/api/fetch";
|
||||
import { DateTime } from "luxon";
|
||||
import { Icon, Table } from "sveltestrap";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from "sveltestrap";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
interface Token {
|
||||
token: string;
|
||||
id: string;
|
||||
api_only: boolean;
|
||||
read_only: boolean;
|
||||
created: string;
|
||||
expires: string;
|
||||
}
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
import { decodeJwt } from "jose";
|
||||
const claims = decodeJwt(localStorage.getItem("pronouns-token")!);
|
||||
let newToken: string | null = null;
|
||||
let error: APIError | null = null;
|
||||
|
||||
let newTokenModalOpen = false;
|
||||
const toggleNewTokenModalOpen = () => (newTokenModalOpen = !newTokenModalOpen);
|
||||
|
||||
const createToken = async (readOnly: boolean) => {
|
||||
try {
|
||||
const token = await apiFetchClient<Token>(`/auth/tokens?read_only=${readOnly}`, "POST");
|
||||
newToken = token.token;
|
||||
error = null;
|
||||
data.tokens = [...data.tokens, token];
|
||||
toggleNewTokenModalOpen();
|
||||
} catch (e) {
|
||||
error = e as APIError;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<h1>Tokens ({data.tokens.length})</h1>
|
||||
<h1>
|
||||
Tokens ({data.tokens.length})
|
||||
<ButtonGroup>
|
||||
<Button color="success" on:click={() => createToken(false)}>New API token</Button>
|
||||
<Button color="success" on:click={() => createToken(true)}>New read-only API token</Button>
|
||||
</ButtonGroup>
|
||||
</h1>
|
||||
|
||||
<Table bordered striped hover>
|
||||
<thead>
|
||||
<th>ID</th>
|
||||
<th>Created at</th>
|
||||
<th>Expires at</th>
|
||||
<th>Current?</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data.tokens as token}
|
||||
<tr>
|
||||
<td><code>{token.id}</code></td>
|
||||
<td>{DateTime.fromISO(token.created).toLocal().toLocaleString(DateTime.DATETIME_MED)}</td>
|
||||
<td>{DateTime.fromISO(token.expires).toLocal().toLocaleString(DateTime.DATETIME_MED)}</td>
|
||||
<td
|
||||
>{#if claims["jti"] === token.id}<Icon name="check-lg" alt="Current token" />{:else}<Icon
|
||||
name="x-lg"
|
||||
alt="Not current token"
|
||||
/>{/if}</td
|
||||
>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
{#each data.tokens as token}
|
||||
<Card class="my-2">
|
||||
<CardHeader>{token.id}</CardHeader>
|
||||
<CardBody>
|
||||
<ul class="list-unstyled">
|
||||
<li>
|
||||
<strong>Created at:</strong>
|
||||
{DateTime.fromISO(token.created).toLocal().toLocaleString(DateTime.DATETIME_MED)}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Expires at:</strong>
|
||||
{DateTime.fromISO(token.expires).toLocal().toLocaleString(DateTime.DATETIME_MED)}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Read-only:</strong>
|
||||
{token.read_only ? "yes" : "no"}
|
||||
</li>
|
||||
</ul>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{:else}
|
||||
You don't have any unexpired API tokens right now.
|
||||
{/each}
|
||||
|
||||
<Modal isOpen={newTokenModalOpen}>
|
||||
<ModalHeader toggle={toggleNewTokenModalOpen}>New token created</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>Created a new API token! Please save it somewhere secure, as it will only be shown once.</p>
|
||||
<p><code>{newToken}</code></p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" on:click={toggleNewTokenModalOpen}>Close</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
|
|
@ -2,11 +2,13 @@ import { apiFetchClient } from "$lib/api/fetch";
|
|||
|
||||
export const load = async () => {
|
||||
const tokens = await apiFetchClient<Token[]>("/auth/tokens");
|
||||
return { tokens };
|
||||
return { tokens: tokens.filter((token) => token.api_only) };
|
||||
};
|
||||
|
||||
interface Token {
|
||||
id: string;
|
||||
api_only: boolean;
|
||||
read_only: boolean;
|
||||
created: string;
|
||||
expires: string;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue