forked from mirrors/pronouns.cc
feat: add warnings page, add delete user + acknowledge report options
This commit is contained in:
parent
ab77fab0ea
commit
293f68e88c
9 changed files with 249 additions and 9 deletions
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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).
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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".
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
56
frontend/src/routes/settings/warnings/+page.svelte
Normal file
56
frontend/src/routes/settings/warnings/+page.svelte
Normal 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>
|
Loading…
Reference in a new issue