alias creation
This commit is contained in:
parent
5be5e328ae
commit
e10dbc9a62
|
@ -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
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue