feat: edit users (admin-only)

This commit is contained in:
diced 2022-10-01 14:04:18 -07:00
parent feb75a8a42
commit ec0e7e5ec7
No known key found for this signature in database
GPG key ID: 370BD1BA142842D1
5 changed files with 302 additions and 70 deletions

View file

@ -0,0 +1,67 @@
import { Modal, TextInput, Switch, Group, Button, Title } from '@mantine/core';
import { useForm } from '@mantine/form';
import { showNotification } from '@mantine/notifications';
import { DeleteIcon, PlusIcon } from 'components/icons';
import useFetch from 'hooks/useFetch';
export function CreateUserModal({ open, setOpen, updateUsers }) {
const form = useForm({
initialValues: {
username: '',
password: '',
administrator: false,
},
});
const onSubmit = async values => {
const cleanUsername = values.username.trim();
const cleanPassword = values.password.trim();
if (cleanUsername === '') return form.setFieldError('username', 'Username can\'t be nothing');
if (cleanPassword === '') return form.setFieldError('password', 'Password can\'t be nothing');
const data = {
username: cleanUsername,
password: cleanPassword,
administrator: values.administrator,
};
setOpen(false);
const res = await useFetch('/api/auth/create', 'POST', data);
if (res.error) {
showNotification({
title: 'Failed to create user',
message: res.error,
icon: <DeleteIcon />,
color: 'red',
});
} else {
showNotification({
title: 'Created user: ' + cleanUsername,
message: '',
icon: <PlusIcon />,
color: 'green',
});
}
updateUsers();
};
return (
<Modal
opened={open}
onClose={() => setOpen(false)}
title={<Title>Create User</Title>}
>
<form onSubmit={form.onSubmit(v => onSubmit(v))}>
<TextInput id='username' label='Username' {...form.getInputProps('username')} />
<TextInput id='password' label='Password' type='password' {...form.getInputProps('password')} />
<Switch mt={12} id='administrator' label='Administrator' {...form.getInputProps('administrator')} />
<Group position='right' mt={22}>
<Button onClick={() => setOpen(false)}>Cancel</Button>
<Button type='submit'>Create</Button>
</Group>
</form>
</Modal>
);
}

View file

@ -0,0 +1,73 @@
import { Modal, TextInput, Switch, Group, Button, Title } from '@mantine/core';
import { useForm } from '@mantine/form';
import { showNotification } from '@mantine/notifications';
import { DeleteIcon, PlusIcon } from 'components/icons';
import useFetch from 'hooks/useFetch';
export function EditUserModal({ open, setOpen, updateUsers, user }) {
let form;
if (user) form = useForm({
initialValues: {
username: user?.username,
password: '',
administrator: user?.administrator,
},
});
const onSubmit = async values => {
const cleanUsername = values.username.trim();
const cleanPassword = values.password.trim();
const data = {
username: null,
password: null,
administrator: values.administrator,
};
if (cleanUsername !== '' && cleanUsername !== user.username) data.username = cleanUsername;
if (cleanPassword !== '') data.password = cleanPassword;
setOpen(false);
const res = await useFetch('/api/user/' + user.id, 'PATCH', data);
if (res.error) {
showNotification({
title: 'Failed to edit user',
message: res.error,
icon: <DeleteIcon />,
color: 'red',
});
} else {
showNotification({
title: 'Edited user: ' + cleanUsername,
message: '',
icon: <PlusIcon />,
color: 'green',
});
}
updateUsers();
};
return (
<Modal
opened={open}
onClose={() => setOpen(false)}
title={<Title>Edit User {user?.username}</Title>}
>
{user && (
<form onSubmit={form.onSubmit(v => onSubmit(v))}>
<TextInput id='username' label='Username' {...form.getInputProps('username')} />
<TextInput id='password' label='Password' type='password' {...form.getInputProps('password')} />
<Switch mt={12} id='administrator' label='Administrator' {...form.getInputProps('administrator')} />
<Group position='right' mt={22}>
<Button onClick={() => setOpen(false)}>Cancel</Button>
<Button type='submit'>Create</Button>
</Group>
</form>
)}
</Modal>
);
}

View file

@ -1,77 +1,15 @@
import { ActionIcon, Avatar, Button, Card, Group, Modal, SimpleGrid, Skeleton, Stack, Switch, Text, TextInput, Title } from '@mantine/core';
import { useForm } from '@mantine/form';
import { ActionIcon, Avatar, Card, Group, SimpleGrid, Skeleton, Stack, Title } from '@mantine/core';
import { useModals } from '@mantine/modals';
import { showNotification } from '@mantine/notifications';
import { CrossIcon, DeleteIcon, PlusIcon } from 'components/icons';
import { CrossIcon, DeleteIcon, PencilIcon, PlusIcon } from 'components/icons';
import MutedText from 'components/MutedText';
import useFetch from 'hooks/useFetch';
import { userSelector } from 'lib/recoil/user';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
function CreateUserModal({ open, setOpen, updateUsers }) {
const form = useForm({
initialValues: {
username: '',
password: '',
administrator: false,
},
});
const onSubmit = async values => {
const cleanUsername = values.username.trim();
const cleanPassword = values.password.trim();
if (cleanUsername === '') return form.setFieldError('username', 'Username can\'t be nothing');
if (cleanPassword === '') return form.setFieldError('password', 'Password can\'t be nothing');
const data = {
username: cleanUsername,
password: cleanPassword,
administrator: values.administrator,
};
setOpen(false);
const res = await useFetch('/api/auth/create', 'POST', data);
if (res.error) {
showNotification({
title: 'Failed to create user',
message: res.error,
icon: <DeleteIcon />,
color: 'red',
});
} else {
showNotification({
title: 'Created user: ' + cleanUsername,
message: '',
icon: <PlusIcon />,
color: 'green',
});
}
updateUsers();
};
return (
<Modal
opened={open}
onClose={() => setOpen(false)}
title={<Title>Create User</Title>}
>
<form onSubmit={form.onSubmit(v => onSubmit(v))}>
<TextInput id='username' label='Username' {...form.getInputProps('username')} />
<TextInput id='password' label='Password' type='password' {...form.getInputProps('password')} />
<Switch mt={12} id='administrator' label='Administrator' {...form.getInputProps('administrator')} />
<Group position='right' mt={22}>
<Button onClick={() => setOpen(false)}>Cancel</Button>
<Button type='submit'>Create</Button>
</Group>
</form>
</Modal>
);
}
import { CreateUserModal } from './CreateUserModal';
import { EditUserModal } from './EditUserModal';
export default function Users() {
const user = useRecoilValue(userSelector);
@ -79,7 +17,9 @@ export default function Users() {
const modals = useModals();
const [users, setUsers] = useState([]);
const [open, setOpen] = useState(false);
const [createOpen, setCreateOpen] = useState(false);
const [editOpen, setEditOpen] = useState(false);
const [selectedUser, setSelectedUser] = useState(null);
const handleDelete = async (user, delete_images) => {
const res = await useFetch('/api/users', 'DELETE', {
@ -142,10 +82,12 @@ export default function Users() {
return (
<>
<CreateUserModal open={open} setOpen={setOpen} updateUsers={updateUsers} />
<CreateUserModal open={createOpen} setOpen={setCreateOpen} updateUsers={updateUsers} />
<EditUserModal open={editOpen} setOpen={setEditOpen} updateUsers={updateUsers} user={selectedUser} />
<Group mb='md'>
<Title>Users</Title>
<ActionIcon variant='filled' color='primary' onClick={() => setOpen(true)}><PlusIcon /></ActionIcon>
<ActionIcon variant='filled' color='primary' onClick={() => setCreateOpen(true)}><PlusIcon /></ActionIcon>
</Group>
<SimpleGrid
cols={3}
@ -166,6 +108,11 @@ export default function Users() {
</Stack>
</Group>
<Group position='right'>
{user.administrator ? null : (
<ActionIcon aria-label='delete' onClick={() => {setEditOpen(true); setSelectedUser(user);}}>
<PencilIcon />
</ActionIcon>
)}
<ActionIcon aria-label='delete' onClick={() => openDeleteModal(user)}>
<DeleteIcon />
</ActionIcon>

146
src/pages/api/user/[id].ts Normal file
View file

@ -0,0 +1,146 @@
import prisma from 'lib/prisma';
import { hashPassword } from 'lib/util';
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
import Logger from 'lib/logger';
async function handler(req: NextApiReq, res: NextApiRes) {
const user = await req.user();
if (!user) return res.forbid('not logged in');
if (!user.administrator) return res.forbid('not an administrator');
const { id } = req.query as { id: string };
const target = await prisma.user.findFirst({
where: {
id: Number(id),
},
});
if (!target) return res.error('user not found');
if (req.method === 'GET') {
delete target.password;
return res.json(target);
} else if (req.method === 'DELETE') {
const newTarget = await prisma.user.delete({
where: { id: target.id },
});
delete newTarget.password;
return res.json(newTarget);
} else if (req.method === 'PATCH') {
if (target.administrator) return res.forbid('cannot modify administrator');
if (req.body.password) {
const hashed = await hashPassword(req.body.password);
await prisma.user.update({
where: { id: target.id },
data: { password: hashed },
});
}
if (req.body.administrator) {
await prisma.user.update({
where: { id: target.id },
data: { administrator: req.body.administrator },
});
}
if (req.body.username) {
const existing = await prisma.user.findFirst({
where: {
username: req.body.username,
},
});
if (existing && user.username !== req.body.username) {
return res.forbid('username is already taken');
}
await prisma.user.update({
where: { id: target.id },
data: { username: req.body.username },
});
}
if (req.body.avatar) await prisma.user.update({
where: { id: target.id },
data: { avatar: req.body.avatar },
});
if (req.body.embedTitle) await prisma.user.update({
where: { id: target.id },
data: { embedTitle: req.body.embedTitle },
});
if (req.body.embedColor) await prisma.user.update({
where: { id: target.id },
data: { embedColor: req.body.embedColor },
});
if (req.body.embedSiteName) await prisma.user.update({
where: { id: target.id },
data: { embedSiteName: req.body.embedSiteName },
});
if (req.body.systemTheme) await prisma.user.update({
where: { id: target.id },
data: { systemTheme: req.body.systemTheme },
});
if (req.body.domains) {
if (!req.body.domains) await prisma.user.update({
where: { id: target.id },
data: { domains: [] },
});
const invalidDomains = [];
const domains = [];
for (const domain of req.body.domains) {
try {
const url = new URL(domain);
domains.push(url.origin);
} catch (e) {
invalidDomains.push({ domain, reason: e.message });
}
}
if (invalidDomains.length) return res.forbid('invalid domains', { invalidDomains });
await prisma.user.update({
where: { id: target.id },
data: { domains },
});
return res.json({ domains });
}
const newUser = await prisma.user.findFirst({
where: {
id: target.id,
},
select: {
administrator: true,
embedColor: true,
embedTitle: true,
embedSiteName: true,
id: true,
images: false,
password: false,
systemTheme: true,
token: true,
username: true,
domains: true,
avatar: true,
},
});
Logger.get('user').info(`User ${user.username} (${user.id}) updated ${target.username} (${newUser.username}) (${newUser.id})`);
return res.json(newUser);
}
}
export default withZipline(handler);

View file

@ -2,7 +2,6 @@ import prisma from 'lib/prisma';
import { hashPassword } from 'lib/util';
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
import Logger from 'lib/logger';
import pkg from '../../../../package.json';
async function handler(req: NextApiReq, res: NextApiRes) {
const user = await req.user();