diff --git a/src/components/pages/Users/CreateUserModal.tsx b/src/components/pages/Users/CreateUserModal.tsx
new file mode 100644
index 0000000..9857c61
--- /dev/null
+++ b/src/components/pages/Users/CreateUserModal.tsx
@@ -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: ,
+ color: 'red',
+ });
+ } else {
+ showNotification({
+ title: 'Created user: ' + cleanUsername,
+ message: '',
+ icon: ,
+ color: 'green',
+ });
+ }
+
+ updateUsers();
+ };
+
+ return (
+ setOpen(false)}
+ title={Create User}
+ >
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/pages/Users/EditUserModal.tsx b/src/components/pages/Users/EditUserModal.tsx
new file mode 100644
index 0000000..96faa08
--- /dev/null
+++ b/src/components/pages/Users/EditUserModal.tsx
@@ -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: ,
+ color: 'red',
+ });
+ } else {
+ showNotification({
+ title: 'Edited user: ' + cleanUsername,
+ message: '',
+ icon: ,
+ color: 'green',
+ });
+ }
+
+ updateUsers();
+ };
+
+ return (
+ setOpen(false)}
+ title={Edit User {user?.username}}
+ >
+ {user && (
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/pages/Users.tsx b/src/components/pages/Users/index.tsx
similarity index 59%
rename from src/components/pages/Users.tsx
rename to src/components/pages/Users/index.tsx
index e080c02..88e8f92 100644
--- a/src/components/pages/Users.tsx
+++ b/src/components/pages/Users/index.tsx
@@ -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: ,
- color: 'red',
- });
- } else {
- showNotification({
- title: 'Created user: ' + cleanUsername,
- message: '',
- icon: ,
- color: 'green',
- });
- }
-
- updateUsers();
- };
-
- return (
- setOpen(false)}
- title={Create User}
- >
-
-
- );
-}
+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 (
<>
-
+
+
+
Users
- setOpen(true)}>
+ setCreateOpen(true)}>
+ {user.administrator ? null : (
+ {setEditOpen(true); setSelectedUser(user);}}>
+
+
+ )}
openDeleteModal(user)}>
diff --git a/src/pages/api/user/[id].ts b/src/pages/api/user/[id].ts
new file mode 100644
index 0000000..a55b2f1
--- /dev/null
+++ b/src/pages/api/user/[id].ts
@@ -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);
\ No newline at end of file
diff --git a/src/pages/api/user/index.ts b/src/pages/api/user/index.ts
index 9ee2179..3b290aa 100644
--- a/src/pages/api/user/index.ts
+++ b/src/pages/api/user/index.ts
@@ -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();