implement password change

This commit is contained in:
Lea 2024-01-17 12:02:10 +01:00
parent 9ffbdae1b6
commit 55d9e84431
Signed by: Lea
GPG key ID: 1BAFFE8347019C42
4 changed files with 126 additions and 8 deletions

View file

@ -4,7 +4,7 @@ import './globals.css';
import AuthWrapper from '@/lib/components/AuthWrapper';
import { getServerSession } from 'next-auth';
import NavigationPanel from '@/lib/components/ui/NavigationPanel';
import { Flex } from '@radix-ui/themes';
import { Flex, ThemePanel } from '@radix-ui/themes';
export const metadata: Metadata = {
title: 'Create Next App',

View file

@ -1,9 +1,10 @@
"use client";
import { Card, Text, Heading, Flex, TextField, Button, IconButton, Popover, Link, Tabs, Box, Grid } from "@radix-ui/themes";
import { CheckIcon, CopyIcon, InfoIcon, LockIcon, UserIcon } from "lucide-react";
import { changeOwnPassword } from "@/lib/actions";
import { Card, Text, Heading, Flex, TextField, Button, IconButton, Popover, Link, Tabs, Box, Grid, Dialog, Callout, Tooltip } from "@radix-ui/themes";
import { AlertCircleIcon, CheckIcon, CopyIcon, ExternalLinkIcon, InfoIcon, LockIcon, UserIcon } from "lucide-react";
import { useSession } from "next-auth/react";
import { useState } from "react";
import { useEffect, useState } from "react";
// TODO read these from environment
const EMAIL_HOST = "mx1.amogus.cloud";
@ -11,10 +12,15 @@ const SMTP_PORT = "465";
const IMAP_PORT = "993";
const SMTP_SECURITY = "SSL/TLS";
const IMAP_SECURITY = "SSL/TLS";
const WEBMAIL_URL = "https://mail.amogus.cloud";
export default function SelfService() {
const session = useSession().data;
const [newPassword, setNewPassword] = useState("");
const validPassword = newPassword.length >= 8;
const [passwordChanged, setPasswordChanged] = useState(false);
function StaticValueField({ label, value }: { label: string, value: string }) {
const [checked, setChecked] = useState(false);
@ -44,8 +50,31 @@ export default function SelfService() {
);
}
function PasswordChangedAlert() {
const [hide, setHide] = useState(false);
useEffect(() => {
let timeout = setTimeout(() => {
setHide(true);
setTimeout(() => setPasswordChanged(false), 500);
}, 5000);
return () => clearTimeout(timeout);
});
return (
<Callout.Root className={`transition-all ${hide ? "opacity-0 h-0 m-0 !p-0" : " mb-4"}`}>
<Callout.Icon><CheckIcon /></Callout.Icon>
<Callout.Text>Password updated!</Callout.Text>
</Callout.Root>
);
}
return (
<>
{passwordChanged && (
<PasswordChangedAlert />
)}
<Heading className="pb-4">Account settings</Heading>
<Grid display="inline-grid" rows="1" columns="2" gap="4">
@ -83,9 +112,69 @@ export default function SelfService() {
<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="solid"
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="solid" disabled>Update</Button>
</Tooltip>
)
}
</Dialog.Close>
</Flex>
</Dialog.Content>
</Dialog.Root>
</Flex>
</Flex>
</Card>
@ -99,6 +188,11 @@ export default function SelfService() {
<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">

View file

@ -1,6 +1,7 @@
"use server";
import { database } from "./db";
import { getServerSession } from "next-auth";
import { database, setUserPassword } from "./db";
import { isAdmin } from "./util";
export async function fetchAllUsers(): Promise<string[]> {
@ -15,3 +16,10 @@ export async function fetchAllUsers(): Promise<string[]> {
});
});
}
export async function changeOwnPassword(newPass: string) {
if (newPass.length < 8) throw new Error("Invalid password");
const session = await getServerSession();
if (!session?.user?.email) throw new Error("Unauthenticated");
await setUserPassword(session.user.email, newPass);
}

View file

@ -32,3 +32,19 @@ export function validateCredentials(email: string, password: string) {
);
});
}
export function setUserPassword(email: string, newPass: string) {
return new Promise<void>(async (resolve, reject) => {
const db = database();
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();
});
});
}