diff --git a/src/app/admin/aliases/page.tsx b/src/app/admin/aliases/page.tsx index 4e25ee3..c5ca9d3 100644 --- a/src/app/admin/aliases/page.tsx +++ b/src/app/admin/aliases/page.tsx @@ -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) => ( {alias.address} - {alias.alias} + + {alias.alias} + {alias.temporary && Temporary} + diff --git a/src/app/admin/audit/page.tsx b/src/app/admin/audit/page.tsx index f59b3d4..cc5d9f6 100644 --- a/src/app/admin/audit/page.tsx +++ b/src/app/admin/audit/page.tsx @@ -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: , + createTempAlias: , createAlias: , approveAlias: , deleteAlias: , diff --git a/src/app/self-service/page.tsx b/src/app/self-service/page.tsx index 048529a..78d2f3a 100644 --- a/src/app/self-service/page.tsx +++ b/src/app/self-service/page.tsx @@ -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 ( <> diff --git a/src/lib/actions.ts b/src/lib/actions.ts index 8a0220e..da5292f 100644 --- a/src/lib/actions.ts +++ b/src/lib/actions.ts @@ -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 { temporary: true, }; + auditLog('createTempAlias', alias); + return alias; } diff --git a/src/lib/audit.ts b/src/lib/audit.ts index 4cf893e..7eb1792 100644 --- a/src/lib/audit.ts +++ b/src/lib/audit.ts @@ -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 = { diff --git a/src/lib/components/ui/admin/ManageUserButton.tsx b/src/lib/components/ui/admin/ManageUserButton.tsx index ae250fe..f6c55f6 100644 --- a/src/lib/components/ui/admin/ManageUserButton.tsx +++ b/src/lib/components/ui/admin/ManageUserButton.tsx @@ -68,7 +68,9 @@ export default function ManageUserButton({ email }: { email: string }) { aliases.map((alias) => ( - {alias.alias} {alias.pending && Pending} + {alias.alias} + {alias.pending && Pending} + {alias.temporary && Temporary} diff --git a/src/lib/components/ui/user/TempAliasesCard.tsx b/src/lib/components/ui/user/TempAliasesCard.tsx index ec080b5..0ba5db7 100644 --- a/src/lib/components/ui/user/TempAliasesCard.tsx +++ b/src/lib/components/ui/user/TempAliasesCard.tsx @@ -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'); } }}> @@ -58,96 +62,214 @@ export default function TempAliasesCard() { Generate alias - - 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. - - - - Alias label - - - - - setAliasLabel(e.currentTarget.value.substring(0, 16))} - /> - - + { + state == "input" + ? <> + + 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. + - - Alias style - setAliasStyle(value as any)}> - - - - Alias style - Words - Random - - - - + + + Alias label + + + + + setAliasLabel(e.currentTarget.value.substring(0, 16))} + /> + + - - Label position - setLabelAtEnd(value == "after")}> - - - - Alias style - Label first - Label at end - - - - - + + Alias style + setAliasStyle(value as any)}> + + + + Alias style + Words + Random + + + + - - - - - - Generated alias - {aliasPreview?.alias || "Enter a label first"} + + Label position + setLabelAtEnd(value == "after")}> + + + + Alias style + Label first + Label at end + + + + - - refreshAlias()} disabled={!aliasLabel} variant="surface" size="2"> - - - - - - - - - - + + + + + + Generated alias + {aliasPreview?.alias || "Enter a label first"} + + + refreshAlias()} disabled={!aliasLabel} variant="surface" size="2"> + + + + + + + + + + + + + : state == "loading" + ? <> + } header="Creating your alias..." /> + + : <> + + + + + + Alias created + {aliasPreview!.alias} + + + navigator.clipboard.writeText(aliasPreview!.alias)} variant="surface" size="2"> + + + + + + + + + + + } Freely generate and dispose of randomized aliases. + + { + aliases == null + ? } header="Loading..." /> + : aliases.length + ? + + + + + Alias + Manage + + + + + { + aliases.map((alias => + + {alias.alias} + + + + + + + + + Delete alias + + Are you sure you want to delete the alias {alias.alias}? + You will not be able to recreate it afterwards. + + + + + + + + + + + + + + )) + } + + + + + : } header="No aliases" message="Generate an alias to begin" /> + } ); } diff --git a/src/lib/db.ts b/src/lib/db.ts index 3a44131..bc51921 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -82,7 +82,7 @@ export function getAllAliases() { return new Promise(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(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);