yeah
This commit is contained in:
parent
be4aaf0dd4
commit
c90bfd4b47
|
@ -5,7 +5,7 @@ import GenericConfirmationDialog from "@/lib/components/ui/GenericConfirmationDi
|
|||
import GhostMessage from "@/lib/components/ui/GhostMessage";
|
||||
import LoadingSpinner from "@/lib/components/ui/LoadingSpinner";
|
||||
import { AliasEntry } from "@/lib/db";
|
||||
import { Button, Card, Code, Dialog, Flex, Heading, Table } from "@radix-ui/themes";
|
||||
import { Badge, Button, Card, Code, Dialog, Flex, Heading, Table } from "@radix-ui/themes";
|
||||
import { ListChecksIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
|
@ -96,7 +96,10 @@ export default function Aliases() {
|
|||
{active.map((alias) => (
|
||||
<Table.Row key={alias.id}>
|
||||
<Table.Cell justify='start'>{alias.address}</Table.Cell>
|
||||
<Table.Cell justify='start'>{alias.alias}</Table.Cell>
|
||||
<Table.Cell justify='start'>
|
||||
{alias.alias}
|
||||
{alias.temporary && <Badge variant="soft" ml="3" color="gray">Temporary</Badge>}
|
||||
</Table.Cell>
|
||||
<Table.Cell justify='end'>
|
||||
<Flex gap='3' justify="end">
|
||||
<Dialog.Root>
|
||||
|
|
|
@ -9,13 +9,14 @@ import { sha256sum } from "@/lib/util";
|
|||
import { Avatar, Card, Code, Flex, Grid, Heading, IconButton, Text } from "@radix-ui/themes";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { ArrowLeftIcon, ArrowRightIcon, AsteriskSquareIcon, GhostIcon, LogInIcon, UserCheckIcon, UserPlusIcon, UsersIcon, XIcon } from "lucide-react";
|
||||
import { ArrowLeftIcon, ArrowRightIcon, AsteriskSquareIcon, GhostIcon, LogInIcon, UserCheckIcon, UserPlusIcon, UsersIcon, UsersRoundIcon, XIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
const LOG_ACTION_HUMAN_READABLE: { [key in AuditLogAction]?: string } = {
|
||||
requestAlias: "Requested an alias",
|
||||
createTempAlias: "Generated a temporary alias",
|
||||
createAlias: "Created an alias",
|
||||
approveAlias: "Approved an alias",
|
||||
deleteAlias: "Deleted an alias",
|
||||
|
@ -42,6 +43,7 @@ export default function Audit() {
|
|||
|
||||
const logActionIcons: { [key in AuditLogAction]?: React.ReactNode } = {
|
||||
requestAlias: <UsersIcon />,
|
||||
createTempAlias: <UsersRoundIcon />,
|
||||
createAlias: <UsersIcon />,
|
||||
approveAlias: <UserCheckIcon />,
|
||||
deleteAlias: <XIcon />,
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Grid, Heading } from "@radix-ui/themes";
|
|||
|
||||
export default function SelfService() {
|
||||
const dimensions = useWindowDimensions();
|
||||
const mobileUi = dimensions.width < 1200;
|
||||
const mobileUi = dimensions.width < 1450;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -144,7 +144,7 @@ export async function requestTemporaryAlias(
|
|||
break;
|
||||
case 'random':
|
||||
randomString = crypto
|
||||
.randomBytes(12)
|
||||
.randomBytes(8)
|
||||
.toString('base64')
|
||||
.replace(/\W/, ''); // Delete special characters
|
||||
break;
|
||||
|
@ -195,6 +195,8 @@ export async function claimTemporaryAlias(key: string): Promise<AliasEntry> {
|
|||
temporary: true,
|
||||
};
|
||||
|
||||
auditLog('createTempAlias', alias);
|
||||
|
||||
return alias;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { getServerSession } from "next-auth";
|
||||
import fs from "fs/promises";
|
||||
|
||||
export type AuditLogAction = 'login' | 'changeOwnPassword' | 'requestAlias' | 'deleteAlias' // Unprivileged
|
||||
export type AuditLogAction = 'login' | 'changeOwnPassword' | 'requestAlias' | 'deleteAlias' | 'createTempAlias' // Unprivileged
|
||||
| 'createUser' | 'createAlias' | 'approveAlias' | 'deleteAlias'; // Privileged
|
||||
|
||||
export type AuditLog = {
|
||||
|
|
|
@ -68,7 +68,9 @@ export default function ManageUserButton({ email }: { email: string }) {
|
|||
aliases.map((alias) => (
|
||||
<Table.Row key={alias.id}>
|
||||
<Table.Cell justify="start">
|
||||
{alias.alias} {alias.pending && <Badge variant="soft" ml="3">Pending</Badge>}
|
||||
{alias.alias}
|
||||
{alias.pending && <Badge variant="soft" ml="3">Pending</Badge>}
|
||||
{alias.temporary && <Badge variant="soft" ml="3" color="gray">Temporary</Badge>}
|
||||
</Table.Cell>
|
||||
<Table.Cell justify="end">
|
||||
<Flex gap="3" justify="end">
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import { disposeTempAliasRequest, fetchOwnAliases, requestTemporaryAlias, claimTemporaryAlias } from "@/lib/actions";
|
||||
import { disposeTempAliasRequest, fetchOwnAliases, requestTemporaryAlias, claimTemporaryAlias, deleteAlias } from "@/lib/actions";
|
||||
import { GRAVATAR_DEFAULT } from "@/lib/constants";
|
||||
import { AliasEntry, AliasRequestEntry } from "@/lib/db";
|
||||
import { ToastContext } from "@/lib/providers/ToastProvider";
|
||||
import { sha256sum } from "@/lib/util";
|
||||
import { Avatar, Button, Card, Dialog, Flex, Heading, IconButton, Select, Text, TextField } from "@radix-ui/themes";
|
||||
import { CircleUserIcon, RefreshCcwIcon } from "lucide-react";
|
||||
import { Avatar, Badge, Box, Button, Card, Code, Dialog, Flex, Heading, IconButton, ScrollArea, Select, Table, Text, TextField } from "@radix-ui/themes";
|
||||
import { CircleUserIcon, CopyIcon, RefreshCcwIcon, UsersRoundIcon } from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import GhostMessage from "../GhostMessage";
|
||||
import LoadingSpinner from "../LoadingSpinner";
|
||||
|
||||
export default function TempAliasesCard() {
|
||||
const session = useSession().data;
|
||||
|
@ -15,7 +18,8 @@ export default function TempAliasesCard() {
|
|||
const [labelAtEnd, setLabelAtEnd] = useState(false);
|
||||
const [aliasStyle, setAliasStyle] = useState<"words" | "random">("words");
|
||||
const [open, setOpen] = useState(false);
|
||||
const [created, setCreated] = useState(false);
|
||||
const [state, setState] = useState<'input' | 'loading' | 'created'>('input');
|
||||
const toast = useContext(ToastContext);
|
||||
|
||||
useEffect(() => {
|
||||
fetchOwnAliases(true).then(setAliases);
|
||||
|
@ -50,7 +54,7 @@ export default function TempAliasesCard() {
|
|||
setAliasLabel("");
|
||||
setLabelAtEnd(false);
|
||||
setAliasStyle("words");
|
||||
setCreated(false);
|
||||
setState('input');
|
||||
}
|
||||
}}>
|
||||
<Dialog.Trigger>
|
||||
|
@ -58,96 +62,214 @@ export default function TempAliasesCard() {
|
|||
</Dialog.Trigger>
|
||||
<Dialog.Content>
|
||||
<Dialog.Title>Generate alias</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Temporary aliases can be used to sign up with services without exposing your primary email address.
|
||||
If an alias starts receiving spam or you no longer use the service, you can delete the alias.
|
||||
</Dialog.Description>
|
||||
|
||||
<Flex direction="row" gap="3" mt="4">
|
||||
<Flex direction="column" gap="3" grow="1">
|
||||
<Text ml="1" size="1" color="gray" mb="-1">Alias label</Text>
|
||||
<TextField.Root>
|
||||
<TextField.Slot>
|
||||
<CircleUserIcon size="16" />
|
||||
</TextField.Slot>
|
||||
<TextField.Input
|
||||
placeholder="Alias label"
|
||||
value={aliasLabel}
|
||||
onChange={(e) => setAliasLabel(e.currentTarget.value.substring(0, 16))}
|
||||
/>
|
||||
</TextField.Root>
|
||||
</Flex>
|
||||
{
|
||||
state == "input"
|
||||
? <>
|
||||
<Dialog.Description>
|
||||
Temporary aliases can be used to sign up with services without exposing your primary email address.
|
||||
If an alias starts receiving spam or you no longer use the service, you can delete the alias.
|
||||
</Dialog.Description>
|
||||
|
||||
<Flex direction="column" gap="3">
|
||||
<Text ml="1" size="1" color="gray" mb="-1">Alias style</Text>
|
||||
<Select.Root value={aliasStyle} onValueChange={(value) => setAliasStyle(value as any)}>
|
||||
<Select.Trigger />
|
||||
<Select.Content>
|
||||
<Select.Group>
|
||||
<Select.Label>Alias style</Select.Label>
|
||||
<Select.Item value="words">Words</Select.Item>
|
||||
<Select.Item value="random">Random</Select.Item>
|
||||
</Select.Group>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</Flex>
|
||||
<Flex direction="row" gap="3" mt="4">
|
||||
<Flex direction="column" gap="3" grow="1">
|
||||
<Text ml="1" size="1" color="gray" mb="-1">Alias label</Text>
|
||||
<TextField.Root>
|
||||
<TextField.Slot>
|
||||
<CircleUserIcon size="16" />
|
||||
</TextField.Slot>
|
||||
<TextField.Input
|
||||
placeholder="Alias label"
|
||||
value={aliasLabel}
|
||||
onChange={(e) => setAliasLabel(e.currentTarget.value.substring(0, 16))}
|
||||
/>
|
||||
</TextField.Root>
|
||||
</Flex>
|
||||
|
||||
<Flex direction="column" gap="3">
|
||||
<Text ml="1" size="1" color="gray" mb="-1">Label position</Text>
|
||||
<Select.Root value={labelAtEnd ? "after" : "before"} onValueChange={(value) => setLabelAtEnd(value == "after")}>
|
||||
<Select.Trigger />
|
||||
<Select.Content>
|
||||
<Select.Group>
|
||||
<Select.Label>Alias style</Select.Label>
|
||||
<Select.Item value="before">Label first</Select.Item>
|
||||
<Select.Item value="after">Label at end</Select.Item>
|
||||
</Select.Group>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex direction="column" gap="3">
|
||||
<Text ml="1" size="1" color="gray" mb="-1">Alias style</Text>
|
||||
<Select.Root value={aliasStyle} onValueChange={(value) => setAliasStyle(value as any)}>
|
||||
<Select.Trigger />
|
||||
<Select.Content>
|
||||
<Select.Group>
|
||||
<Select.Label>Alias style</Select.Label>
|
||||
<Select.Item value="words">Words</Select.Item>
|
||||
<Select.Item value="random">Random</Select.Item>
|
||||
</Select.Group>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</Flex>
|
||||
|
||||
<Card mt="4">
|
||||
<Flex direction="row" justify="between" align="center">
|
||||
<Flex direction="row" align="center" gap="3">
|
||||
<Avatar
|
||||
size="3"
|
||||
src={`https://gravatar.com/avatar/${sha256sum(aliasPreview?.alias || "")}?d=${GRAVATAR_DEFAULT}`}
|
||||
fallback={"@"}
|
||||
/>
|
||||
<Flex direction="column" gap="0">
|
||||
<Text size="3" weight="medium">Generated alias</Text>
|
||||
<Text size="2" weight="light">{aliasPreview?.alias || "Enter a label first"}</Text>
|
||||
<Flex direction="column" gap="3">
|
||||
<Text ml="1" size="1" color="gray" mb="-1">Label position</Text>
|
||||
<Select.Root value={labelAtEnd ? "after" : "before"} onValueChange={(value) => setLabelAtEnd(value == "after")}>
|
||||
<Select.Trigger />
|
||||
<Select.Content>
|
||||
<Select.Group>
|
||||
<Select.Label>Alias style</Select.Label>
|
||||
<Select.Item value="before">Label first</Select.Item>
|
||||
<Select.Item value="after">Label at end</Select.Item>
|
||||
</Select.Group>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<IconButton onClick={() => refreshAlias()} disabled={!aliasLabel} variant="surface" size="2">
|
||||
<RefreshCcwIcon size="16" />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
</Card>
|
||||
|
||||
<Flex gap="3" mt="4" justify="end">
|
||||
<Dialog.Close>
|
||||
<Button variant="outline">Close</Button>
|
||||
</Dialog.Close>
|
||||
<Button
|
||||
disabled={!aliasPreview}
|
||||
variant="soft"
|
||||
onClick={async () => {
|
||||
try {
|
||||
//const alias = await claimTemporaryAlias(aliasPreview!.key);
|
||||
} catch(e) {
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</Flex>
|
||||
<Card mt="4">
|
||||
<Flex direction="row" justify="between" align="center">
|
||||
<Flex direction="row" align="center" gap="3">
|
||||
<Avatar
|
||||
size="3"
|
||||
src={`https://gravatar.com/avatar/${sha256sum(aliasPreview?.alias || "")}?d=${GRAVATAR_DEFAULT}`}
|
||||
fallback={"@"}
|
||||
/>
|
||||
<Flex direction="column" gap="0">
|
||||
<Text size="3" weight="medium">Generated alias</Text>
|
||||
<Text size="2" weight="light">{aliasPreview?.alias || "Enter a label first"}</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<IconButton onClick={() => refreshAlias()} disabled={!aliasLabel} variant="surface" size="2">
|
||||
<RefreshCcwIcon size="16" />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
</Card>
|
||||
|
||||
<Flex gap="3" mt="4" justify="end">
|
||||
<Dialog.Close>
|
||||
<Button variant="outline">Close</Button>
|
||||
</Dialog.Close>
|
||||
<Button
|
||||
disabled={!aliasPreview}
|
||||
variant="soft"
|
||||
onClick={async () => {
|
||||
try {
|
||||
setState('loading');
|
||||
const alias = await claimTemporaryAlias(aliasPreview!.key);
|
||||
setAliases([...(aliases ?? []), alias]);
|
||||
setState('created');
|
||||
} catch (e) {
|
||||
setOpen(false);
|
||||
toast({
|
||||
title: "Failed to create alias",
|
||||
description: `${e}`,
|
||||
variant: "error",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</Flex>
|
||||
</>
|
||||
: state == "loading"
|
||||
? <>
|
||||
<GhostMessage icon={<LoadingSpinner />} header="Creating your alias..." />
|
||||
</>
|
||||
: <>
|
||||
<Card mt="4">
|
||||
<Flex direction="row" justify="between" align="center">
|
||||
<Flex direction="row" align="center" gap="3">
|
||||
<Avatar
|
||||
size="3"
|
||||
src={`https://gravatar.com/avatar/${sha256sum(aliasPreview!.alias)}?d=${GRAVATAR_DEFAULT}`}
|
||||
fallback={"@"}
|
||||
/>
|
||||
<Flex direction="column" gap="0">
|
||||
<Text size="3" weight="medium">Alias created</Text>
|
||||
<Text size="2" weight="light">{aliasPreview!.alias}</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<IconButton onClick={() => navigator.clipboard.writeText(aliasPreview!.alias)} variant="surface" size="2">
|
||||
<CopyIcon size="16" />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Flex gap="3" mt="4" justify="end">
|
||||
<Dialog.Close>
|
||||
<Button variant="outline">Close</Button>
|
||||
</Dialog.Close>
|
||||
</Flex>
|
||||
</>
|
||||
}
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
</Flex>
|
||||
<Text weight="light" size="2">Freely generate and dispose of randomized aliases.</Text>
|
||||
|
||||
{
|
||||
aliases == null
|
||||
? <GhostMessage icon={<LoadingSpinner />} header="Loading..." />
|
||||
: aliases.length
|
||||
? <Box className="h-52">
|
||||
<ScrollArea>
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.ColumnHeaderCell justify='start'>Alias</Table.ColumnHeaderCell>
|
||||
<Table.ColumnHeaderCell justify='end'>Manage</Table.ColumnHeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
|
||||
<Table.Body>
|
||||
{
|
||||
aliases.map((alias => <Table.Row key={alias.id}>
|
||||
<Table.Cell justify='start'>
|
||||
{alias.alias}
|
||||
</Table.Cell>
|
||||
<Table.Cell justify='end'>
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button variant="soft" size="1" mr="2">Delete</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Content>
|
||||
<Dialog.Title>Delete alias</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Are you sure you want to delete the alias <Code className="whitespace-nowrap">{alias.alias}</Code>?
|
||||
You will not be able to recreate it afterwards.
|
||||
</Dialog.Description>
|
||||
|
||||
<Flex gap="3" mt="4" justify="end">
|
||||
<Dialog.Close>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</Dialog.Close>
|
||||
<Dialog.Close>
|
||||
<Button
|
||||
variant="soft"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await deleteAlias(alias.alias);
|
||||
setAliases(aliases.filter((a) => a.id != alias.id));
|
||||
toast({
|
||||
title: "Temporary alias deleted",
|
||||
description: alias.alias,
|
||||
variant: "success",
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast({
|
||||
title: "Failed to delete alias",
|
||||
description: `${e}`,
|
||||
variant: "error",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Dialog.Close>
|
||||
</Flex>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
</Table.Cell>
|
||||
</Table.Row>))
|
||||
}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</ScrollArea>
|
||||
</Box>
|
||||
: <GhostMessage icon={<UsersRoundIcon />} header="No aliases" message="Generate an alias to begin" />
|
||||
}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -82,7 +82,7 @@ export function getAllAliases() {
|
|||
return new Promise<AliasEntry[]>(async (resolve, reject) => {
|
||||
const db = database('aliases');
|
||||
|
||||
db.all("SELECT id, address, alias, pending FROM aliases", (err, res: any[]) => {
|
||||
db.all("SELECT id, address, alias, pending, temporary FROM aliases", (err, res: any[]) => {
|
||||
db.close();
|
||||
if (err) return reject(err);
|
||||
resolve(res.map((data) => ({
|
||||
|
@ -99,10 +99,10 @@ export function getUserAliases(email: string, tempAliases?: boolean) {
|
|||
const db = database('aliases');
|
||||
|
||||
db.all(
|
||||
"SELECT id, address, alias, pending FROM aliases WHERE address = ?1 " + (typeof tempAliases != 'undefined' ? "AND temporary = ?2" : ""),
|
||||
"SELECT id, address, alias, pending, temporary FROM aliases WHERE address = ?1 " + (typeof tempAliases != 'undefined' ? "AND temporary = ?2" : ""),
|
||||
{
|
||||
1: email,
|
||||
2: tempAliases ? 1 : 0,
|
||||
2: typeof tempAliases == "boolean" ? (tempAliases ? 1 : 0) : undefined,
|
||||
},
|
||||
(err, res: any[]) => {
|
||||
db.close();
|
||||
|
@ -120,7 +120,7 @@ export function getAlias(alias: string) {
|
|||
return new Promise<AliasEntry | undefined>(async (resolve, reject) => {
|
||||
const db = database('aliases');
|
||||
|
||||
db.get("SELECT id, address, alias, pending FROM aliases WHERE alias = ?", alias, (err, res: AliasEntry) => {
|
||||
db.get("SELECT id, address, alias, pending, temporary FROM aliases WHERE alias = ?", alias, (err, res: AliasEntry) => {
|
||||
db.close();
|
||||
if (err) return reject(err);
|
||||
if (!res) return resolve(undefined);
|
||||
|
|
Loading…
Reference in a new issue