refactor self service panel
This commit is contained in:
parent
52c4971762
commit
5be2aaa289
|
@ -1,72 +1,18 @@
|
|||
"use client";
|
||||
|
||||
import { aliasAvailable, changeOwnPassword, createAliasSelf, deleteAlias, fetchOwnAliases } from "@/lib/actions";
|
||||
import GhostMessage from "@/lib/components/ui/GhostMessage";
|
||||
import LoadingSpinner from "@/lib/components/ui/LoadingSpinner";
|
||||
import { ALIAS_DOMAINS, EMAIL_HOST, IMAP_PORT, IMAP_SECURITY, SMTP_PORT, SMTP_SECURITY, WEBMAIL_URL } from "@/lib/constants";
|
||||
import { AliasEntry } from "@/lib/db";
|
||||
import ConnectionDetailsCard from "@/lib/components/ui/user/ConnectionDetailsCard";
|
||||
import OwnAliasesCard from "@/lib/components/ui/user/OwnAliasesCard";
|
||||
import OwnCredentialsCard from "@/lib/components/ui/user/OwnCredentialsCard";
|
||||
import useWindowDimensions from "@/lib/hooks/useWindowDimensions";
|
||||
import { aliasesNeedApproval } from "@/lib/util";
|
||||
import { Card, Text, Heading, Flex, TextField, Button, IconButton, Popover, Link, Tabs, Box, Grid, Dialog, Callout, Tooltip, Table, ScrollArea, DropdownMenu, Code } from "@radix-ui/themes";
|
||||
import { AlertCircleIcon, CheckIcon, ChevronDownIcon, CopyIcon, ExternalLinkIcon, InfoIcon, LockIcon, UserIcon, UsersRound } from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { Callout, Grid, Heading } from "@radix-ui/themes";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function SelfService() {
|
||||
const session = useSession().data;
|
||||
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const validPassword = newPassword.length >= 8;
|
||||
const [passwordChanged, setPasswordChanged] = useState(false);
|
||||
const [aliases, setAliases] = useState<AliasEntry[] | null>(null);
|
||||
const [newAliasUsername, setNewAliasUsername] = useState("");
|
||||
const [newAliasDomain, setNewAliasDomain] = useState(ALIAS_DOMAINS[0]);
|
||||
const [allowAlias, setAllowAlias] = useState(false);
|
||||
const dimensions = useWindowDimensions();
|
||||
const mobileUi = dimensions.width < 1200;
|
||||
|
||||
useEffect(() => {
|
||||
fetchOwnAliases().then(setAliases);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!newAliasUsername || !newAliasDomain) return setAllowAlias(false);
|
||||
|
||||
aliasAvailable(`${newAliasUsername}@${newAliasDomain}`)
|
||||
.then((res) => {
|
||||
setAllowAlias(res);
|
||||
});
|
||||
}, [newAliasUsername, newAliasDomain]);
|
||||
|
||||
function StaticValueField({ label, value }: { label: string, value: string }) {
|
||||
const [checked, setChecked] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text weight="light" size="2">{label}</Text>
|
||||
<TextField.Root>
|
||||
<TextField.Input disabled value={value} />
|
||||
<TextField.Slot>
|
||||
<IconButton
|
||||
size="1"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(value);
|
||||
setChecked(true);
|
||||
setTimeout(() => setChecked(false), 1000);
|
||||
}}
|
||||
>
|
||||
{checked
|
||||
? <CheckIcon width={16} height={16} />
|
||||
: <CopyIcon width={16} height={16} />
|
||||
}
|
||||
</IconButton>
|
||||
</TextField.Slot>
|
||||
</TextField.Root>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function PasswordChangedAlert() {
|
||||
const [hide, setHide] = useState(false);
|
||||
|
||||
|
@ -95,281 +41,9 @@ export default function SelfService() {
|
|||
<Heading className="pb-4">Account settings</Heading>
|
||||
|
||||
<Grid display="inline-grid" columns={mobileUi ? "1" : "3"} gap="4" width={mobileUi ? "100%" : "auto"}>
|
||||
<Card className="h-fit">
|
||||
<Heading size="3">Credentials</Heading>
|
||||
<Text weight="light" size="2">These are the details you can use to authenticate via SMTP and IMAP.</Text>
|
||||
|
||||
<Flex direction="column" gap="3" className="pt-2">
|
||||
<TextField.Root>
|
||||
<TextField.Slot>
|
||||
<UserIcon height="16" width="16" />
|
||||
</TextField.Slot>
|
||||
<TextField.Input disabled value={session?.user?.email ?? 'n/a'} />
|
||||
<TextField.Slot>
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<IconButton asChild size="1" variant="ghost">
|
||||
<InfoIcon height="16" width="16" />
|
||||
</IconButton>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content className="w-96">
|
||||
<Text>
|
||||
Your username uniquely identifies your account and therefore cannot be changed. If you need a different email address, you can <Link onClick={() => alert("todo")}>request an alias</Link> instead.
|
||||
</Text>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</TextField.Slot>
|
||||
</TextField.Root>
|
||||
|
||||
<Flex direction="row" gap="3">
|
||||
<TextField.Root className="flex-1">
|
||||
<TextField.Slot>
|
||||
<LockIcon height="16" width="16" />
|
||||
</TextField.Slot>
|
||||
<TextField.Input disabled value={"\u25CF".repeat(10)} />
|
||||
</TextField.Root>
|
||||
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button variant="outline">
|
||||
Change
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Content>
|
||||
<Dialog.Title>Update password</Dialog.Title>
|
||||
|
||||
<Box pb="4">
|
||||
<Callout.Root>
|
||||
<Callout.Icon>
|
||||
<AlertCircleIcon />
|
||||
</Callout.Icon>
|
||||
<Callout.Text>You will be logged out of every configured mail client!</Callout.Text>
|
||||
</Callout.Root>
|
||||
</Box>
|
||||
|
||||
<TextField.Root>
|
||||
<TextField.Slot>
|
||||
<LockIcon height="16" width="16" />
|
||||
</TextField.Slot>
|
||||
<TextField.Input
|
||||
type="password"
|
||||
placeholder="New password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.currentTarget.value)}
|
||||
/>
|
||||
</TextField.Root>
|
||||
|
||||
<Flex gap="3" mt="4" justify="end">
|
||||
<Dialog.Close>
|
||||
<Button variant="outline" onClick={() => setNewPassword("")}>Cancel</Button>
|
||||
</Dialog.Close>
|
||||
<Dialog.Close>
|
||||
{validPassword
|
||||
? (
|
||||
<Button
|
||||
variant="soft"
|
||||
onClick={async () => {
|
||||
setNewPassword("");
|
||||
|
||||
try {
|
||||
setPasswordChanged(false);
|
||||
await changeOwnPassword(newPassword);
|
||||
setPasswordChanged(true);
|
||||
} catch (e) {
|
||||
alert(e);
|
||||
}
|
||||
}}
|
||||
>Update</Button>
|
||||
)
|
||||
: (
|
||||
<Tooltip content="The password must have a minimum length of 8 characters">
|
||||
<Button variant="soft" disabled>Update</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
</Dialog.Close>
|
||||
</Flex>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
|
||||
<Card className="h-fit">
|
||||
<Heading size="3">Server configuration</Heading>
|
||||
<Text weight="light" size="2">You can use these details to log in using any mail client.</Text>
|
||||
|
||||
<Box pt="2">
|
||||
<Tabs.Root defaultValue="smtp">
|
||||
<Tabs.List size="2">
|
||||
<Tabs.Trigger value="smtp">Outgoing</Tabs.Trigger>
|
||||
<Tabs.Trigger value="imap">Incoming</Tabs.Trigger>
|
||||
<Flex align="center" justify="end" grow="1" pr="3">
|
||||
<Link href={WEBMAIL_URL} target="_blank">
|
||||
<Button variant="ghost">Webmail <ExternalLinkIcon size="14" /></Button>
|
||||
</Link>
|
||||
</Flex>
|
||||
</Tabs.List>
|
||||
|
||||
<Box pt="3" pb="2">
|
||||
<Tabs.Content value="smtp">
|
||||
<StaticValueField label="SMTP Server" value={EMAIL_HOST} />
|
||||
<StaticValueField label="SMTP Port" value={SMTP_PORT} />
|
||||
<StaticValueField label="Connection security" value={SMTP_SECURITY} />
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="imap">
|
||||
<StaticValueField label="IMAP Server" value={EMAIL_HOST} />
|
||||
<StaticValueField label="IMAP Port" value={IMAP_PORT} />
|
||||
<StaticValueField label="Connection security" value={IMAP_SECURITY} />
|
||||
</Tabs.Content>
|
||||
</Box>
|
||||
</Tabs.Root>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
<Card className="h-fit">
|
||||
<Flex direction="row" justify="between" mb="2">
|
||||
<Heading size="3">Email aliases</Heading>
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button size="1" variant="outline">New alias</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Content>
|
||||
<Dialog.Title>Create alias</Dialog.Title>
|
||||
{(aliasesNeedApproval(session)) && (
|
||||
<Box pt="2" pb="5">
|
||||
<Callout.Root>
|
||||
<Callout.Icon>
|
||||
<InfoIcon />
|
||||
</Callout.Icon>
|
||||
<Callout.Text>Aliases you create will have to be approved by an administrator first</Callout.Text>
|
||||
</Callout.Root>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Flex gap="2">
|
||||
<TextField.Input
|
||||
placeholder={session?.user?.email?.split('@')[0] || "username"}
|
||||
value={newAliasUsername}
|
||||
onChange={(e) => setNewAliasUsername(e.currentTarget.value.toLowerCase())}
|
||||
/>
|
||||
<Text>@</Text>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="outline" color="gray">{newAliasDomain} <ChevronDownIcon /></Button>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content>
|
||||
{ALIAS_DOMAINS.map((alias) => (
|
||||
<DropdownMenu.Item
|
||||
onClick={() => setNewAliasDomain(alias)}
|
||||
key={alias}
|
||||
>
|
||||
{alias}
|
||||
</DropdownMenu.Item>
|
||||
))}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="3" mt="4" justify="end">
|
||||
<Dialog.Close>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setNewAliasUsername("");
|
||||
setNewAliasDomain(ALIAS_DOMAINS[0]);
|
||||
}}>Cancel</Button>
|
||||
</Dialog.Close>
|
||||
<Dialog.Close>
|
||||
<Button
|
||||
variant="soft"
|
||||
disabled={!allowAlias}
|
||||
onClick={async () => {
|
||||
setNewAliasUsername("");
|
||||
setNewAliasDomain(ALIAS_DOMAINS[0]);
|
||||
|
||||
try {
|
||||
const alias = await createAliasSelf(`${newAliasUsername}@${newAliasDomain}`);
|
||||
setAliases([...(aliases ?? []), alias]);
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
alert(e);
|
||||
}
|
||||
}}>Create</Button>
|
||||
</Dialog.Close>
|
||||
</Flex>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
</Flex>
|
||||
{
|
||||
aliases == null
|
||||
? <GhostMessage icon={<LoadingSpinner />} header="Loading..." />
|
||||
: aliases.length
|
||||
? <Box className="h-60">
|
||||
<ScrollArea>
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.ColumnHeaderCell justify='start'>Alias</Table.ColumnHeaderCell>
|
||||
<Table.ColumnHeaderCell justify='end'>Active</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'>{alias.pending ? "Pending" : "Active"}</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>{alias.alias}</Code>?
|
||||
It will become available for other users to claim immediately.
|
||||
</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));
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
alert(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Dialog.Close>
|
||||
</Flex>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
</Table.Cell>
|
||||
</Table.Row>))
|
||||
}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</ScrollArea>
|
||||
</Box>
|
||||
: <GhostMessage icon={<UsersRound />} header="No aliases" message="Create an alias to begin" />
|
||||
}
|
||||
</Card>
|
||||
<OwnCredentialsCard onPasswordChange={() => setPasswordChanged(true)} />
|
||||
<ConnectionDetailsCard />
|
||||
<OwnAliasesCard />
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
|
|
71
src/lib/components/ui/user/ConnectionDetailsCard.tsx
Normal file
71
src/lib/components/ui/user/ConnectionDetailsCard.tsx
Normal file
|
@ -0,0 +1,71 @@
|
|||
"use client";
|
||||
|
||||
import { EMAIL_HOST, IMAP_PORT, IMAP_SECURITY, SMTP_PORT, SMTP_SECURITY, WEBMAIL_URL } from "@/lib/constants";
|
||||
import { Box, Button, Card, Flex, Heading, IconButton, Link, Tabs, Text, TextField } from "@radix-ui/themes";
|
||||
import { CheckIcon, CopyIcon, ExternalLinkIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
function StaticValueField({ label, value }: { label: string, value: string }) {
|
||||
const [checked, setChecked] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text weight="light" size="2">{label}</Text>
|
||||
<TextField.Root>
|
||||
<TextField.Input disabled value={value} />
|
||||
<TextField.Slot>
|
||||
<IconButton
|
||||
size="1"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(value);
|
||||
setChecked(true);
|
||||
setTimeout(() => setChecked(false), 1000);
|
||||
}}
|
||||
>
|
||||
{checked
|
||||
? <CheckIcon width={16} height={16} />
|
||||
: <CopyIcon width={16} height={16} />
|
||||
}
|
||||
</IconButton>
|
||||
</TextField.Slot>
|
||||
</TextField.Root>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ConnectionDetailsCard() {
|
||||
return (
|
||||
<Card className="h-fit">
|
||||
<Heading size="3">Server configuration</Heading>
|
||||
<Text weight="light" size="2">You can use these details to log in using any mail client.</Text>
|
||||
|
||||
<Box pt="2">
|
||||
<Tabs.Root defaultValue="smtp">
|
||||
<Tabs.List size="2">
|
||||
<Tabs.Trigger value="smtp">Outgoing</Tabs.Trigger>
|
||||
<Tabs.Trigger value="imap">Incoming</Tabs.Trigger>
|
||||
<Flex align="center" justify="end" grow="1" pr="3">
|
||||
<Link href={WEBMAIL_URL} target="_blank">
|
||||
<Button variant="ghost">Webmail <ExternalLinkIcon size="14" /></Button>
|
||||
</Link>
|
||||
</Flex>
|
||||
</Tabs.List>
|
||||
|
||||
<Box pt="3" pb="2">
|
||||
<Tabs.Content value="smtp">
|
||||
<StaticValueField label="SMTP Server" value={EMAIL_HOST} />
|
||||
<StaticValueField label="SMTP Port" value={SMTP_PORT} />
|
||||
<StaticValueField label="Connection security" value={SMTP_SECURITY} />
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="imap">
|
||||
<StaticValueField label="IMAP Server" value={EMAIL_HOST} />
|
||||
<StaticValueField label="IMAP Port" value={IMAP_PORT} />
|
||||
<StaticValueField label="Connection security" value={IMAP_SECURITY} />
|
||||
</Tabs.Content>
|
||||
</Box>
|
||||
</Tabs.Root>
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
}
|
178
src/lib/components/ui/user/OwnAliasesCard.tsx
Normal file
178
src/lib/components/ui/user/OwnAliasesCard.tsx
Normal file
|
@ -0,0 +1,178 @@
|
|||
"use client";
|
||||
|
||||
import { aliasAvailable, createAliasSelf, deleteAlias, fetchOwnAliases } from "@/lib/actions";
|
||||
import { ALIAS_DOMAINS } from "@/lib/constants";
|
||||
import { AliasEntry } from "@/lib/db";
|
||||
import { aliasesNeedApproval } from "@/lib/util";
|
||||
import { Box, Button, Callout, Card, Code, Dialog, DropdownMenu, Flex, Heading, ScrollArea, Table, Text, TextField } from "@radix-ui/themes";
|
||||
import { ChevronDownIcon, InfoIcon, UsersRoundIcon } from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import LoadingSpinner from "../LoadingSpinner";
|
||||
import GhostMessage from "../GhostMessage";
|
||||
|
||||
export default function OwnAliasesCard() {
|
||||
const session = useSession().data;
|
||||
const [aliases, setAliases] = useState<AliasEntry[] | null>(null);
|
||||
const [newAliasUsername, setNewAliasUsername] = useState("");
|
||||
const [newAliasDomain, setNewAliasDomain] = useState(ALIAS_DOMAINS[0]);
|
||||
const [allowAlias, setAllowAlias] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchOwnAliases().then(setAliases);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!newAliasUsername || !newAliasDomain) return setAllowAlias(false);
|
||||
|
||||
aliasAvailable(`${newAliasUsername}@${newAliasDomain}`)
|
||||
.then((res) => {
|
||||
setAllowAlias(res);
|
||||
});
|
||||
}, [newAliasUsername, newAliasDomain]);
|
||||
|
||||
return (
|
||||
<Card className="h-fit">
|
||||
<Flex direction="row" justify="between" mb="2">
|
||||
<Heading size="3">Email aliases</Heading>
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button size="1" variant="outline">New alias</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Content>
|
||||
<Dialog.Title>Create alias</Dialog.Title>
|
||||
{(aliasesNeedApproval(session)) && (
|
||||
<Box pt="2" pb="5">
|
||||
<Callout.Root>
|
||||
<Callout.Icon>
|
||||
<InfoIcon />
|
||||
</Callout.Icon>
|
||||
<Callout.Text>Aliases you create will have to be approved by an administrator first</Callout.Text>
|
||||
</Callout.Root>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Flex gap="2">
|
||||
<TextField.Input
|
||||
placeholder={session?.user?.email?.split('@')[0] || "username"}
|
||||
value={newAliasUsername}
|
||||
onChange={(e) => setNewAliasUsername(e.currentTarget.value.toLowerCase())}
|
||||
/>
|
||||
<Text>@</Text>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="outline" color="gray">{newAliasDomain} <ChevronDownIcon /></Button>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content>
|
||||
{ALIAS_DOMAINS.map((alias) => (
|
||||
<DropdownMenu.Item
|
||||
onClick={() => setNewAliasDomain(alias)}
|
||||
key={alias}
|
||||
>
|
||||
{alias}
|
||||
</DropdownMenu.Item>
|
||||
))}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="3" mt="4" justify="end">
|
||||
<Dialog.Close>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setNewAliasUsername("");
|
||||
setNewAliasDomain(ALIAS_DOMAINS[0]);
|
||||
}}>Cancel</Button>
|
||||
</Dialog.Close>
|
||||
<Dialog.Close>
|
||||
<Button
|
||||
variant="soft"
|
||||
disabled={!allowAlias}
|
||||
onClick={async () => {
|
||||
setNewAliasUsername("");
|
||||
setNewAliasDomain(ALIAS_DOMAINS[0]);
|
||||
|
||||
try {
|
||||
const alias = await createAliasSelf(`${newAliasUsername}@${newAliasDomain}`);
|
||||
setAliases([...(aliases ?? []), alias]);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(e);
|
||||
}
|
||||
}}>Create</Button>
|
||||
</Dialog.Close>
|
||||
</Flex>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
</Flex>
|
||||
{
|
||||
aliases == null
|
||||
? <GhostMessage icon={<LoadingSpinner />} header="Loading..." />
|
||||
: aliases.length
|
||||
? <Box className="h-60">
|
||||
<ScrollArea>
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.ColumnHeaderCell justify='start'>Alias</Table.ColumnHeaderCell>
|
||||
<Table.ColumnHeaderCell justify='end'>Active</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'>{alias.pending ? "Pending" : "Active"}</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>{alias.alias}</Code>?
|
||||
It will become available for other users to claim immediately.
|
||||
</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));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
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="Create an alias to begin" />
|
||||
}
|
||||
</Card>
|
||||
);
|
||||
}
|
115
src/lib/components/ui/user/OwnCredentialsCard.tsx
Normal file
115
src/lib/components/ui/user/OwnCredentialsCard.tsx
Normal file
|
@ -0,0 +1,115 @@
|
|||
"use client";
|
||||
|
||||
import { changeOwnPassword } from "@/lib/actions";
|
||||
import { Box, Button, Callout, Card, Dialog, Flex, Heading, IconButton, Link, Popover, Text, TextField, Tooltip } from "@radix-ui/themes";
|
||||
import { AlertCircleIcon, InfoIcon, LockIcon, UserIcon } from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function OwnCredentialsCard({ onPasswordChange }: { onPasswordChange?: () => any }) {
|
||||
const session = useSession().data;
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const validPassword = newPassword.length >= 8;
|
||||
|
||||
return (
|
||||
<Card className="h-fit">
|
||||
<Heading size="3">Credentials</Heading>
|
||||
<Text weight="light" size="2">These are the details you can use to authenticate via SMTP and IMAP.</Text>
|
||||
|
||||
<Flex direction="column" gap="3" className="pt-2">
|
||||
<TextField.Root>
|
||||
<TextField.Slot>
|
||||
<UserIcon height="16" width="16" />
|
||||
</TextField.Slot>
|
||||
<TextField.Input disabled value={session?.user?.email ?? 'n/a'} />
|
||||
<TextField.Slot>
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<IconButton asChild size="1" variant="ghost">
|
||||
<InfoIcon height="16" width="16" />
|
||||
</IconButton>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content className="w-96">
|
||||
<Text>
|
||||
Your username uniquely identifies your account and therefore cannot be changed. If you need a different email address, you can <Link onClick={() => alert("todo")}>request an alias</Link> instead.
|
||||
</Text>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</TextField.Slot>
|
||||
</TextField.Root>
|
||||
|
||||
<Flex direction="row" gap="3">
|
||||
<TextField.Root className="flex-1">
|
||||
<TextField.Slot>
|
||||
<LockIcon height="16" width="16" />
|
||||
</TextField.Slot>
|
||||
<TextField.Input disabled value={"\u25CF".repeat(10)} />
|
||||
</TextField.Root>
|
||||
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button variant="outline">
|
||||
Change
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Content>
|
||||
<Dialog.Title>Update password</Dialog.Title>
|
||||
|
||||
<Box pb="4">
|
||||
<Callout.Root>
|
||||
<Callout.Icon>
|
||||
<AlertCircleIcon />
|
||||
</Callout.Icon>
|
||||
<Callout.Text>You will be logged out of every configured mail client!</Callout.Text>
|
||||
</Callout.Root>
|
||||
</Box>
|
||||
|
||||
<TextField.Root>
|
||||
<TextField.Slot>
|
||||
<LockIcon height="16" width="16" />
|
||||
</TextField.Slot>
|
||||
<TextField.Input
|
||||
type="password"
|
||||
placeholder="New password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.currentTarget.value)}
|
||||
/>
|
||||
</TextField.Root>
|
||||
|
||||
<Flex gap="3" mt="4" justify="end">
|
||||
<Dialog.Close>
|
||||
<Button variant="outline" onClick={() => setNewPassword("")}>Cancel</Button>
|
||||
</Dialog.Close>
|
||||
<Dialog.Close>
|
||||
{validPassword
|
||||
? (
|
||||
<Button
|
||||
variant="soft"
|
||||
onClick={async () => {
|
||||
setNewPassword("");
|
||||
|
||||
try {
|
||||
await changeOwnPassword(newPassword);
|
||||
onPasswordChange?.();
|
||||
} catch (e) {
|
||||
alert(e);
|
||||
}
|
||||
}}
|
||||
>Update</Button>
|
||||
)
|
||||
: (
|
||||
<Tooltip content="The password must have a minimum length of 8 characters">
|
||||
<Button variant="soft" disabled>Update</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
</Dialog.Close>
|
||||
</Flex>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
);
|
||||
}
|
Loading…
Reference in a new issue