feat: edit users (admin-only)
This commit is contained in:
parent
feb75a8a42
commit
ec0e7e5ec7
5 changed files with 302 additions and 70 deletions
67
src/components/pages/Users/CreateUserModal.tsx
Normal file
67
src/components/pages/Users/CreateUserModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
73
src/components/pages/Users/EditUserModal.tsx
Normal file
73
src/components/pages/Users/EditUserModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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
146
src/pages/api/user/[id].ts
Normal 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);
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue