feat: add warnings page, add delete user + acknowledge report options

This commit is contained in:
Sam 2023-03-23 17:13:23 +01:00
parent ab77fab0ea
commit 293f68e88c
No known key found for this signature in database
GPG key ID: B4EF20DDE721CAA1
9 changed files with 249 additions and 9 deletions

View file

@ -21,7 +21,7 @@ type Report struct {
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
ResolvedAt *time.Time `json:"resolved_at"` ResolvedAt *time.Time `json:"resolved_at"`
AdminID *xid.ID `json:"admin_id"` AdminID xid.ID `json:"admin_id"`
AdminComment *string `json:"admin_comment"` AdminComment *string `json:"admin_comment"`
} }

View file

@ -450,6 +450,7 @@ func (db *DB) ResetUser(ctx context.Context, tx pgx.Tx, id xid.ID) error {
Set("username", "deleted-"+hash). Set("username", "deleted-"+hash).
Set("display_name", nil). Set("display_name", nil).
Set("bio", nil). Set("bio", nil).
Set("links", nil).
Set("names", "[]"). Set("names", "[]").
Set("pronouns", "[]"). Set("pronouns", "[]").
Set("avatar", nil). Set("avatar", nil).

View file

@ -96,6 +96,13 @@ export interface Report {
admin_comment: string | null; admin_comment: string | null;
} }
export interface Warning {
id: number;
reason: string;
created_at: string;
read: boolean;
}
export interface APIError { export interface APIError {
code: ErrorCode; code: ErrorCode;
message?: string; message?: string;

View file

@ -194,7 +194,7 @@
<Modal header="Force delete account" isOpen={forceDeleteModalOpen} toggle={toggleForceDeleteModal}> <Modal header="Force delete account" isOpen={forceDeleteModalOpen} toggle={toggleForceDeleteModal}>
<ModalBody> <ModalBody>
<p> <p>
If you want to delete your account, type your username (<code>{user.name}</code>) below: If you want to delete your account, type your username (<code>{user?.name}</code>) below:
<br /> <br />
<b> <b>
This is irreversible! Your account <i>cannot</i> be recovered after you press "Force delete account". This is irreversible! Your account <i>cannot</i> be recovered after you press "Force delete account".

View file

@ -1,8 +1,10 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { browser } from "$app/environment"; import { browser } from "$app/environment";
import { decodeJwt } from "jose";
import { import {
Badge,
Collapse, Collapse,
Icon, Icon,
Nav, Nav,
@ -15,13 +17,24 @@
import Logo from "./Logo.svelte"; import Logo from "./Logo.svelte";
import { userStore, themeStore } from "$lib/store"; import { userStore, themeStore } from "$lib/store";
import { ErrorCode, type APIError, type MeUser } from "$lib/api/entities"; import {
import { apiFetch } from "$lib/api/fetch"; ErrorCode,
type APIError,
type MeUser,
type Report,
type Warning,
} from "$lib/api/entities";
import { apiFetch, apiFetchClient } from "$lib/api/fetch";
import { addToast } from "$lib/toast";
let theme: string; let theme: string;
let currentUser: MeUser | null; let currentUser: MeUser | null;
let showMenu: boolean = false; let showMenu: boolean = false;
let isAdmin = false;
let numReports = 0;
let numWarnings = 0;
$: currentUser = $userStore; $: currentUser = $userStore;
$: theme = $themeStore; $: theme = $themeStore;
@ -47,6 +60,32 @@
localStorage.removeItem("pronouns-user"); localStorage.removeItem("pronouns-user");
} }
}); });
isAdmin = !!decodeJwt(token)["adm"];
if (isAdmin) {
apiFetchClient<Report[]>("/admin/reports")
.then((reports) => {
numReports = reports.length;
})
.catch((e) => {
console.log("getting reports:", e);
});
}
apiFetchClient<Warning[]>("/auth/warnings")
.then((warnings) => {
if (warnings.length !== 0) {
numWarnings = warnings.length;
addToast({
header: "Warnings",
body: "You have unread warnings. Go to your settings to view them.",
duration: -1,
});
}
})
.catch((e) => {
console.log("getting warnings:", e);
});
} }
}); });
@ -83,8 +122,23 @@
<NavLink href="/@{currentUser.name}">@{currentUser.name}</NavLink> <NavLink href="/@{currentUser.name}">@{currentUser.name}</NavLink>
</NavItem> </NavItem>
<NavItem> <NavItem>
<NavLink href="/settings">Settings</NavLink> <NavLink href="/settings">
Settings
{#if numWarnings}
<Badge color="danger">{numWarnings}</Badge>
{/if}
</NavLink>
</NavItem> </NavItem>
{#if isAdmin}
<NavItem>
<NavLink href="/reports">
Reports
{#if numReports !== 0}
<Badge color="danger">{numReports}</Badge>
{/if}
</NavLink>
</NavItem>
{/if}
{:else} {:else}
<NavItem> <NavItem>
<NavLink href="/auth/login">Log in</NavLink> <NavLink href="/auth/login">Log in</NavLink>

View file

@ -12,16 +12,34 @@
let warnModalOpen = false; let warnModalOpen = false;
const toggleWarnModal = () => (warnModalOpen = !warnModalOpen); const toggleWarnModal = () => (warnModalOpen = !warnModalOpen);
let banModalOpen = false;
const toggleBanModal = () => (banModalOpen = !banModalOpen);
let ignoreModalOpen = false;
const toggleIgnoreModal = () => (ignoreModalOpen = !ignoreModalOpen);
let reportIndex = -1; let reportIndex = -1;
let reason = ""; let reason = "";
let deleteUser = false; let deleteUser = false;
let error: APIError | null = null; let error: APIError | null = null;
$: console.log(deleteUser);
const openWarnModalFor = (index: number) => { const openWarnModalFor = (index: number) => {
reportIndex = index; reportIndex = index;
toggleWarnModal(); toggleWarnModal();
}; };
const openBanModalFor = (index: number) => {
reportIndex = index;
toggleBanModal();
};
const openIgnoreModalFor = (index: number) => {
reportIndex = index;
toggleIgnoreModal();
};
const warnUser = async () => { const warnUser = async () => {
try { try {
await apiFetchClient<any>(`/admin/reports/${data.reports[reportIndex].id}`, "PATCH", { await apiFetchClient<any>(`/admin/reports/${data.reports[reportIndex].id}`, "PATCH", {
@ -37,6 +55,39 @@
error = e as APIError; error = e as APIError;
} }
}; };
const deactivateUser = async () => {
try {
await apiFetchClient<any>(`/admin/reports/${data.reports[reportIndex].id}`, "PATCH", {
warn: true,
ban: true,
delete: deleteUser,
reason: reason,
});
error = null;
addToast({ body: "Successfully deactivated user", header: "Deactivated user" });
toggleBanModal();
reportIndex = -1;
} catch (e) {
error = e as APIError;
}
};
const ignoreReport = async () => {
try {
await apiFetchClient<any>(`/admin/reports/${data.reports[reportIndex].id}`, "PATCH", {
reason: reason,
});
error = null;
addToast({ body: "Successfully acknowledged report", header: "Ignored report" });
toggleIgnoreModal();
reportIndex = -1;
} catch (e) {
error = e as APIError;
}
};
</script> </script>
<svelte:head> <svelte:head>
@ -54,10 +105,16 @@
<Button outline color="warning" size="sm" on:click={() => openWarnModalFor(index)} <Button outline color="warning" size="sm" on:click={() => openWarnModalFor(index)}
>Warn user</Button >Warn user</Button
> >
<Button outline color="danger" size="sm">Deactivate user</Button> <Button outline color="danger" size="sm" on:click={() => openBanModalFor(index)}
<Button outline color="secondary" size="sm">Ignore report</Button> >Deactivate user</Button
>
<Button outline color="secondary" size="sm" on:click={() => openIgnoreModalFor(index)}
>Ignore report</Button
>
</ReportCard> </ReportCard>
</div> </div>
{:else}
There are no open reports :)
{/each} {/each}
</div> </div>
@ -76,4 +133,40 @@
<Button color="secondary" on:click={toggleWarnModal}>Cancel</Button> <Button color="secondary" on:click={toggleWarnModal}>Cancel</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>
<Modal header="Deactivate user" isOpen={banModalOpen} toggle={toggleBanModal}>
<ModalBody>
{#if error}
<ErrorAlert {error} />
{/if}
<ReportCard report={data.reports[reportIndex]} />
<FormGroup floating label="Reason" class="my-2">
<textarea style="min-height: 100px;" class="form-control" bind:value={reason} />
</FormGroup>
<div class="form-check">
<input class="form-check-input" type="checkbox" bind:checked={deleteUser} id="deleteUser" />
<label class="form-check-label" for="deleteUser">Delete user?</label>
</div>
</ModalBody>
<ModalFooter>
<Button color="danger" on:click={deactivateUser} disabled={!reason}>Deactivate user</Button>
<Button color="secondary" on:click={toggleBanModal}>Cancel</Button>
</ModalFooter>
</Modal>
<Modal header="Ignore report" isOpen={ignoreModalOpen} toggle={toggleIgnoreModal}>
<ModalBody>
{#if error}
<ErrorAlert {error} />
{/if}
<ReportCard report={data.reports[reportIndex]} />
<FormGroup floating label="Reason" class="my-2">
<textarea style="min-height: 100px;" class="form-control" bind:value={reason} />
</FormGroup>
</ModalBody>
<ModalFooter>
<Button color="warning" on:click={ignoreReport} disabled={!reason}>Ignore report</Button>
<Button color="secondary" on:click={toggleIgnoreModal}>Cancel</Button>
</ModalFooter>
</Modal>
</div> </div>

View file

@ -1,7 +1,15 @@
<script lang="ts"> <script lang="ts">
import { page } from "$app/stores"; import { page } from "$app/stores";
import type { LayoutData } from "./$types"; import type { LayoutData } from "./$types";
import { Button, ListGroup, ListGroupItem, Modal, ModalBody, ModalFooter } from "sveltestrap"; import {
Badge,
Button,
ListGroup,
ListGroupItem,
Modal,
ModalBody,
ModalFooter,
} from "sveltestrap";
import { userStore } from "$lib/store"; import { userStore } from "$lib/store";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { addToast } from "$lib/toast"; import { addToast } from "$lib/toast";
@ -20,6 +28,9 @@
addToast({ header: "Logged out", body: "Successfully logged out!" }); addToast({ header: "Logged out", body: "Successfully logged out!" });
goto("/"); goto("/");
}; };
let unreadWarnings: number;
$: unreadWarnings = data.warnings.filter((w) => !w.read).length;
</script> </script>
<svelte:head> <svelte:head>
@ -58,6 +69,16 @@
> >
Tokens Tokens
</ListGroupItem> </ListGroupItem>
<ListGroupItem
tag="a"
active={$page.url.pathname === "/settings/warnings"}
href="/settings/warnings"
>
Warnings
{#if unreadWarnings !== 0}
<Badge color="danger">{unreadWarnings}</Badge>
{/if}
</ListGroupItem>
<ListGroupItem <ListGroupItem
tag="a" tag="a"
active={$page.url.pathname === "/settings/export"} active={$page.url.pathname === "/settings/export"}

View file

@ -1,4 +1,10 @@
import { ErrorCode, type APIError, type Invite, type MeUser } from "$lib/api/entities"; import {
ErrorCode,
type Warning,
type APIError,
type Invite,
type MeUser,
} from "$lib/api/entities";
import { apiFetchClient } from "$lib/api/fetch"; import { apiFetchClient } from "$lib/api/fetch";
import type { LayoutLoad } from "./$types"; import type { LayoutLoad } from "./$types";
@ -6,6 +12,7 @@ export const ssr = false;
export const load = (async ({ parent }) => { export const load = (async ({ parent }) => {
const user = await apiFetchClient<MeUser>("/users/@me"); const user = await apiFetchClient<MeUser>("/users/@me");
const warnings = await apiFetchClient<Warning[]>("/auth/warnings?all=true");
let invites: Invite[] = []; let invites: Invite[] = [];
let invitesEnabled = true; let invitesEnabled = true;
@ -24,5 +31,6 @@ export const load = (async ({ parent }) => {
user, user,
invites, invites,
invitesEnabled, invitesEnabled,
warnings,
}; };
}) satisfies LayoutLoad; }) satisfies LayoutLoad;

View file

@ -0,0 +1,56 @@
<script lang="ts">
import type { APIError } from "$lib/api/entities";
import { apiFetchClient } from "$lib/api/fetch";
import ErrorAlert from "$lib/components/ErrorAlert.svelte";
import { addToast } from "$lib/toast";
import { DateTime } from "luxon";
import { Button, Card, CardBody, CardFooter, CardHeader } from "sveltestrap";
import type { PageData } from "./$types";
export let data: PageData;
let error: APIError | null = null;
const acknowledgeWarning = async (idx: number) => {
try {
await apiFetchClient<any>(`/auth/warnings/${data.warnings[idx].id}/ack`, "POST");
addToast({
header: "Acknowledged",
body: `Marked warning #${data.warnings[idx].id} as read.`,
});
data.warnings[idx].read = true;
data.warnings = data.warnings;
} catch (e) {
error = e as APIError;
}
};
</script>
<h1>Warnings ({data.warnings.length})</h1>
{#if error}
<ErrorAlert {error} />
{/if}
<div>
{#each data.warnings as warning, index}
<Card class="my-2">
<CardHeader>
<strong>#{warning.id}</strong> ({DateTime.fromISO(warning.created_at)
.toLocal()
.toLocaleString(DateTime.DATETIME_MED)})
</CardHeader>
<CardBody>
<blockquote class="blockquote">{warning.reason}</blockquote>
</CardBody>
{#if !warning.read}
<CardFooter>
<Button color="secondary" outline on:click={() => acknowledgeWarning(index)}
>Mark as read</Button
>
</CardFooter>
{/if}
</Card>
{:else}
You have no warnings!
{/each}
</div>