alias creation

This commit is contained in:
Lea 2024-01-17 23:10:47 +01:00
parent 5be5e328ae
commit e10dbc9a62
Signed by: Lea
GPG key ID: 1BAFFE8347019C42
4 changed files with 159 additions and 15 deletions

View file

@ -1,12 +1,13 @@
"use client";
import { changeOwnPassword, fetchOwnAliases } from "@/lib/actions";
import { aliasAvailable, changeOwnPassword, createAliasSelf, fetchOwnAliases } from "@/lib/actions";
import GhostMessage from "@/lib/components/ui/GhostMessage";
import LoadingSpinner from "@/lib/components/ui/LoadingSpinner";
import { AliasEntry } from "@/lib/db";
import useWindowDimensions from "@/lib/hooks/useWindowDimensions";
import { Card, Text, Heading, Flex, TextField, Button, IconButton, Popover, Link, Tabs, Box, Grid, Dialog, Callout, Tooltip, Table, ScrollArea } from "@radix-ui/themes";
import { AlertCircleIcon, CheckIcon, CopyIcon, ExternalLinkIcon, InfoIcon, LockIcon, UserIcon, UsersRound } from "lucide-react";
import { aliasesNeedApproval } from "@/lib/util";
import { Card, Text, Heading, Flex, TextField, Button, IconButton, Popover, Link, Tabs, Box, Grid, Dialog, Callout, Tooltip, Table, ScrollArea, DropdownMenu } from "@radix-ui/themes";
import { AlertCircleIcon, CheckIcon, ChevronDownIcon, CopyIcon, ExternalLinkIcon, InfoIcon, LockIcon, UserIcon, UsersRound } from "lucide-react";
import { useSession } from "next-auth/react";
import { useEffect, useState } from "react";
@ -17,6 +18,7 @@ const IMAP_PORT = "993";
const SMTP_SECURITY = "SSL/TLS";
const IMAP_SECURITY = "SSL/TLS";
export const WEBMAIL_URL = "https://webmail.amogus.cloud";
export const ALIAS_DOMAINS = ["amogus.cloud", "lea.pet", "futacockinside.me"];
export default function SelfService() {
const session = useSession().data;
@ -25,6 +27,9 @@ export default function SelfService() {
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 < 1500;
@ -32,6 +37,15 @@ export default function SelfService() {
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);
@ -225,7 +239,77 @@ export default function SelfService() {
<Card className="h-fit">
<Flex direction="row" justify="between" mb="2">
<Heading size="3">Email aliases</Heading>
<Button size="1" variant="outline">New alias</Button>
<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="solid"
disabled={!allowAlias}
onClick={async () => {
setNewAliasUsername("");
setNewAliasDomain(ALIAS_DOMAINS[0]);
try {
const alias = await createAliasSelf(`${newAliasUsername}@${newAliasDomain}`);
setAliases([...(aliases ?? []), alias]);
} catch(e) {
alert(e);
}
}}>Change</Button>
</Dialog.Close>
</Flex>
</Dialog.Content>
</Dialog.Root>
</Flex>
{
aliases == null

View file

@ -1,8 +1,8 @@
"use server";
import { getServerSession } from "next-auth";
import { database, getUserAliases, setUserPassword } from "./db";
import { isAdmin } from "./util";
import { AliasEntry, createAlias, database, getUserAliases, setUserPassword } from "./db";
import { aliasesNeedApproval, isAdmin } from "./util";
export async function fetchAllUsers(): Promise<string[]> {
return new Promise((resolve, reject) => {
@ -29,3 +29,40 @@ export async function fetchOwnAliases() {
if (!session?.user?.email) throw new Error("Unauthenticated");
return await getUserAliases(session.user.email);
}
export async function aliasAvailable(email: string) {
const session = await getServerSession();
if (!session?.user) throw new Error("Unauthenticated");
return new Promise<boolean>((resolve, reject) => {
const db = database('aliases');
db.get('SELECT id FROM aliases WHERE alias = ?', email.toLowerCase(), (err, res) => {
db.close();
if (err) return reject(err);
if (res != undefined) return resolve(false);
const authDb = database('credentials');
authDb.get('SELECT key FROM passwords WHERE key = ?', email.toLowerCase(), (err, res) => {
authDb.close();
if (err) return reject(err);
resolve(res == undefined);
});
});
});
}
export async function createAliasSelf(alias: string): Promise<AliasEntry> {
const session = await getServerSession();
if (!session?.user?.email) throw new Error("Unauthenticated");
const pending = aliasesNeedApproval(session);
if (!await aliasAvailable(alias)) throw new Error("Alias unavailable");
const id = await createAlias(session.user.email, alias.toLowerCase(), pending);
return {
id: id,
address: session.user.email,
alias: alias,
pending: pending
};
}

View file

@ -2,7 +2,7 @@ import sqlite from "sqlite3";
import bcrypt from "bcryptjs";
import { PHASE_PRODUCTION_BUILD } from "next/dist/shared/lib/constants";
export const database = (type: 'credentials'|'aliases') => {
export const database = (type: 'credentials' | 'aliases') => {
if (process.env.NEXT_PHASE != PHASE_PRODUCTION_BUILD) {
for (const v of ["CREDENTIALS_DB_PATH", "ALIASES_DB_PATH"]) {
if (!process.env[v]) {
@ -37,7 +37,7 @@ export function validateCredentials(email: string, password: string) {
const hash: string = row.value.replace("bcrypt:", "");
const isValid = await bcrypt.compare(password, hash);
resolve(isValid);
} catch(e) {
} catch (e) {
reject(e);
}
}
@ -51,13 +51,13 @@ export function setUserPassword(email: string, newPass: string) {
const hash = 'bcrypt:' + await bcrypt.hash(newPass, 10);
db.run("UPDATE passwords SET value = $1 WHERE key = $2",
{ 1: hash, 2: email },
async (err: any, res: any) => {
db.close();
if (err) return reject(err);
console.log(res);
resolve();
});
{ 1: hash, 2: email },
async (err: any, res: any) => {
db.close();
if (err) return reject(err);
console.log(res);
resolve();
});
});
}
@ -76,3 +76,22 @@ export function getUserAliases(email: string) {
});
});
}
export function createAlias(user: string, alias: string, pending: boolean) {
return new Promise<number>(async (resolve, reject) => {
const db = database('aliases');
db.run(
"INSERT INTO aliases (address, alias, pending) VALUES (?1, ?2, ?3)",
{
1: user,
2: alias,
3: pending ? 1 : 0,
},
function(err: any) {
db.close();
if (err) return reject(err);
resolve(this.lastID);
});
});
}

View file

@ -10,3 +10,7 @@ export function sha256sum(input: any) {
export function isAdmin(session: Session | null) {
return session?.user?.email == "lea@amogus.cloud"; // todo
}
export function aliasesNeedApproval(session: Session | null) {
return !isAdmin(session); // also todo
}