From 7265a4e36a658f3f4f8c7d23765a43b9063df610 Mon Sep 17 00:00:00 2001 From: z1glr Date: Tue, 21 Jan 2025 12:51:59 +0000 Subject: [PATCH] improved admin and events tables --- backend/pkg/db/users/users.go | 16 +- backend/pkg/router/router.go | 1 + backend/pkg/router/user.go | 45 +++++ client/src/Zustand.ts | 4 + client/src/app/account/page.tsx | 2 +- .../admin/(availabilities)/Availabilities.tsx | 13 +- .../(availabilities)/AvailabilityEditor.tsx | 5 +- client/src/app/admin/(tasks)/TaskEditor.tsx | 7 +- client/src/app/admin/(tasks)/Tasks.tsx | 13 +- client/src/app/admin/(users)/AddUser.tsx | 94 +++-------- client/src/app/admin/(users)/EditUser.tsx | 159 +++++------------- client/src/app/admin/(users)/UserEditor.tsx | 127 ++++++++++++++ client/src/app/admin/(users)/Users.tsx | 95 ++++++++--- client/src/app/assignments/page.tsx | 3 +- client/src/components/Colorselector.tsx | 2 + client/src/components/DeleteConfirmation.tsx | 6 +- client/src/lib.ts | 2 +- 17 files changed, 357 insertions(+), 237 deletions(-) create mode 100644 client/src/app/admin/(users)/UserEditor.tsx diff --git a/backend/pkg/db/users/users.go b/backend/pkg/db/users/users.go index 32a55b1..7ce0baf 100644 --- a/backend/pkg/db/users/users.go +++ b/backend/pkg/db/users/users.go @@ -11,6 +11,11 @@ type User struct { Admin bool `db:"admin" json:"admin"` } +type UserChangePassword struct { + UserName string `json:"userName" validate:"required" db:"userName"` + Password string `json:"password" validate:"required,min=12"` +} + // hashes a password func hashPassword(password string) ([]byte, error) { return bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) @@ -64,11 +69,6 @@ func Add(user UserAdd) error { } } -type UserChangePassword struct { - UserName string `json:"userName" validate:"required" db:"userName"` - Password string `json:"password" validate:"required,min=12"` -} - func ChangePassword(user UserChangePassword) (string, error) { // try to hash teh password if hash, err := hashPassword(user.Password); err != nil { @@ -103,3 +103,9 @@ func SetAdmin(userName string, admin bool) error { return err } + +func Delete(userName string) error { + _, err := db.DB.Exec("DELETE FROM USERS WHERE name = $1", userName) + + return err +} diff --git a/backend/pkg/router/router.go b/backend/pkg/router/router.go index 0d733d7..ffd8de2 100644 --- a/backend/pkg/router/router.go +++ b/backend/pkg/router/router.go @@ -102,6 +102,7 @@ func init() { "event": deleteEvent, "tasks": deleteTask, "availabilities": deleteAvailability, + "users": deleteUser, }, } diff --git a/backend/pkg/router/user.go b/backend/pkg/router/user.go index bfe2833..7c620b6 100644 --- a/backend/pkg/router/user.go +++ b/backend/pkg/router/user.go @@ -189,3 +189,48 @@ func patchUser(args HandlerArgs) responseMessage { return response } + +func deleteUser(args HandlerArgs) responseMessage { + // check admin + if !args.User.Admin { + logger.Warn().Msg("user-deletion failed: user is no admin") + + return responseMessage{ + Status: fiber.StatusUnauthorized, + } + + // get the username from the query + } else if userName := args.C.Query("userName"); userName == "" { + logger.Log().Msg("user-deletion failed: query is missing \"userName\"") + + return responseMessage{ + Status: fiber.StatusBadRequest, + } + + // check wether the user tries to delete himself + } else if userName == args.User.UserName { + logger.Log().Msg("user-deletion failed: self-deletion is illegal") + + return responseMessage{ + Status: fiber.StatusBadRequest, + } + + // check wether the user tries to delete the admin + } else if userName == "admin" { + logger.Log().Msg("user-deletion failed: admin-deletion is illegal") + + return responseMessage{ + Status: fiber.StatusBadRequest, + } + + // delete the user + } else if err := users.Delete(userName); err != nil { + logger.Error().Msgf("user-deletion failed: user doesn't exist") + + return responseMessage{ + Status: fiber.StatusNotFound, + } + } else { + return responseMessage{} + } +} diff --git a/client/src/Zustand.ts b/client/src/Zustand.ts index b6aab91..8fdc01f 100644 --- a/client/src/Zustand.ts +++ b/client/src/Zustand.ts @@ -23,6 +23,10 @@ export interface User { admin: boolean; } +export type UserAddModify = User & { + password: string; +}; + interface Zustand { user: User | null; tasks?: Task[]; diff --git a/client/src/app/account/page.tsx b/client/src/app/account/page.tsx index 63c6d9e..2e9397b 100644 --- a/client/src/app/account/page.tsx +++ b/client/src/app/account/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { apiCall, vaidatePassword as validatePassword } from "@/lib"; +import { apiCall, validatePassword as validatePassword } from "@/lib"; import { Button, Card, diff --git a/client/src/app/admin/(availabilities)/Availabilities.tsx b/client/src/app/admin/(availabilities)/Availabilities.tsx index 8658a37..f73aea5 100644 --- a/client/src/app/admin/(availabilities)/Availabilities.tsx +++ b/client/src/app/admin/(availabilities)/Availabilities.tsx @@ -91,7 +91,7 @@ export default function Availabilities() { } const topContent = ( - <> +
- +
); return ( @@ -112,6 +112,13 @@ export default function Availabilities() { topContent={topContent} sortDescriptor={availabilities.sortDescriptor} onSortChange={availabilities.sort} + topContentPlacement="outside" + classNames={{ + wrapper: "bg-accent-4", + tr: "even:bg-accent-5 ", + th: "font-subheadline text-xl text-accent-1 bg-transparent ", + thead: "[&>tr]:first:!shadow-border", + }} > @@ -182,7 +189,7 @@ export default function Availabilities() { onOpenChange={(isOpen) => !isOpen ? setDeleteAvailability(undefined) : null } - header="Delete Availability" + itemName="Availability" onDelete={() => sendDeleteAvailability(deleteAvailability?.id)} > {!!deleteAvailability ? ( diff --git a/client/src/app/admin/(availabilities)/AvailabilityEditor.tsx b/client/src/app/admin/(availabilities)/AvailabilityEditor.tsx index c6610c0..83bdf76 100644 --- a/client/src/app/admin/(availabilities)/AvailabilityEditor.tsx +++ b/client/src/app/admin/(availabilities)/AvailabilityEditor.tsx @@ -58,6 +58,9 @@ export default function AvailabilityEditor(props: { isOpen={props.isOpen} onOpenChange={props.onOpenChange} shadow={"none" as "sm"} + classNames={{ + base: "bg-accent-5", + }} >
@@ -81,6 +83,7 @@ export default function AvailabilityEditor(props: { variant="bordered" /> diff --git a/client/src/app/admin/(tasks)/Tasks.tsx b/client/src/app/admin/(tasks)/Tasks.tsx index 09b8910..3ff7553 100644 --- a/client/src/app/admin/(tasks)/Tasks.tsx +++ b/client/src/app/admin/(tasks)/Tasks.tsx @@ -88,7 +88,7 @@ export default function Tasks() { } const topContent = ( - <> +
- +
); return ( @@ -109,6 +109,13 @@ export default function Tasks() { topContent={topContent} sortDescriptor={tasks.sortDescriptor} onSortChange={tasks.sort} + topContentPlacement="outside" + classNames={{ + wrapper: "bg-accent-4", + tr: "even:bg-accent-5 ", + th: "font-subheadline text-xl text-accent-1 bg-transparent ", + thead: "[&>tr]:first:!shadow-border", + }} > @@ -173,7 +180,7 @@ export default function Tasks() { (!isOpen ? setDeleteTask(undefined) : null)} - header="Delete Task" + itemName="Task" onDelete={() => sendDeleteTask(deleteTask?.id)} > {!!deleteTask ? ( diff --git a/client/src/app/admin/(users)/AddUser.tsx b/client/src/app/admin/(users)/AddUser.tsx index d1995dd..f6efa15 100644 --- a/client/src/app/admin/(users)/AddUser.tsx +++ b/client/src/app/admin/(users)/AddUser.tsx @@ -1,79 +1,35 @@ -import { AddLarge, Copy } from "@carbon/icons-react"; -import { - Button, - Checkbox, - Form, - Input, - Modal, - ModalBody, - ModalContent, - ModalFooter, - ModalHeader, -} from "@heroui/react"; -import { FormEvent, useState } from "react"; +import { apiCall } from "@/lib"; +import { AddLarge } from "@carbon/icons-react"; +import { Button } from "@heroui/react"; +import UserEditor from "./UserEditor"; +import { UserAddModify } from "@/Zustand"; export default function AddUser(props: { - isOpen: boolean; + isOpen?: boolean; onOpenChange?: (isOpen: boolean) => void; - onSubmit?: (e: FormEvent) => void; + onSuccess?: () => void; }) { - const [password, setPassword] = useState(""); + // send an addUser request to the backend then reload the table + async function sendAddUser(user: UserAddModify) { + const result = await apiCall("POST", "users", undefined, user); + + if (result.ok) { + props.onSuccess?.(); + } + } return ( - - - -

Add User

-
- - { - e.preventDefault(); - props.onSubmit?.(e); - }} - > - - - navigator.clipboard.writeText(password)} - > - - - } - value={password} - onValueChange={setPassword} - /> - - Admin - - - - - - -
-
+ header="Add User" + footer={ + + } + onSubmit={sendAddUser} + /> ); } diff --git a/client/src/app/admin/(users)/EditUser.tsx b/client/src/app/admin/(users)/EditUser.tsx index fc2c126..cbeee4a 100644 --- a/client/src/app/admin/(users)/EditUser.tsx +++ b/client/src/app/admin/(users)/EditUser.tsx @@ -1,137 +1,54 @@ -import { - apiCall, - classNames, - vaidatePassword as validatePassword, -} from "@/lib"; -import zustand, { User } from "@/Zustand"; -import { - Button, - Checkbox, - Form, - Input, - Modal, - ModalBody, - ModalContent, - ModalFooter, - ModalHeader, -} from "@heroui/react"; -import { FormEvent, useEffect, useState } from "react"; +import { apiCall } from "@/lib"; +import zustand, { User, UserAddModify } from "@/Zustand"; +import { Button } from "@heroui/react"; +import UserEditor from "./UserEditor"; +import { Renew } from "@carbon/icons-react"; export default function EditUser(props: { - isOpen: boolean; - user?: User; - onOpenChange: (isOpen: boolean) => void; - onSuccess: () => void; + value?: User; + isOpen?: boolean; + onOpenChange?: (isOpen: boolean) => void; + onSuccess?: () => void; }) { - const [name, setName] = useState(props.user?.userName); - const [admin, setAdmin] = useState(props.user?.admin); - const [password, setPassword] = useState(""); - - const pwErrors = validatePassword(password); - - // set the states on value changes - useEffect(() => { - if (props.user !== undefined) { - setName(props.user.userName); - setAdmin(props.user.admin); - - // reset the password - setPassword(""); - } - }, [props.user]); - // update the user in the backend - async function updateUser(e: FormEvent) { - const formData = Object.fromEntries(new FormData(e.currentTarget)); - - const data = { - ...formData, - userName: props.user?.userName, - admin: formData.admin !== undefined, - }; - - // if we modify ourself, set admin to true since it isn't included in the form data because the checkbox is disabled - data.admin ||= props.user?.userName === zustand.getState().user?.userName; - - const result = await apiCall("PATCH", "users", undefined, data); + async function updateUser(user: UserAddModify) { + const result = await apiCall("PATCH", "users", undefined, { + ...user, + userName: props.value?.userName, + newName: user.userName, + }); if (result.ok) { // if we updated ourself - if (props.user?.userName === zustand.getState().user?.userName) { + if (props.value?.userName === zustand.getState().user?.userName) { zustand.setState({ user: null }); } - props.onSuccess(); + props.onSuccess?.(); + props.onOpenChange?.(false); } } return ( - -
{ - e.preventDefault(); - updateUser(e); - }} - > - {props.user !== undefined ? ( - - -

- Edit User{" "} - - {props.user.userName} - -

-
- - - 0 ? "warning" : "default"} - name="password" - value={password} - onValueChange={setPassword} - isInvalid={password.length > 0 && pwErrors.length > 0} - errorMessage={ -
    - {pwErrors.map((e, ii) => ( -
  • {e}
  • - ))} -
- } - /> - - Admin - -
- - - -
- ) : null} -
-
+ + Edit User{" "} + + "{props.value?.userName}" + + + } + footer={ + + } + value={props.value} + isOpen={props.isOpen} + onOpenChange={props.onOpenChange} + onSubmit={updateUser} + /> ); } diff --git a/client/src/app/admin/(users)/UserEditor.tsx b/client/src/app/admin/(users)/UserEditor.tsx new file mode 100644 index 0000000..872dbc3 --- /dev/null +++ b/client/src/app/admin/(users)/UserEditor.tsx @@ -0,0 +1,127 @@ +import { classNames, validatePassword as validatePassword } from "@/lib"; +import zustand, { User, UserAddModify } from "@/Zustand"; +import { + Checkbox, + Form, + Input, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, +} from "@heroui/react"; +import React, { FormEvent, useState } from "react"; + +export default function UserEditor(props: { + header: React.ReactNode; + footer: React.ReactNode; + value?: User; + isPasswordRequired?: boolean; + isOpen?: boolean; + onOpenChange?: (isOpen: boolean) => void; + onSubmit: (user: UserAddModify) => void; +}) { + const [name, setName] = useState(props.value?.userName ?? ""); + const [admin, setAdmin] = useState(props.value?.admin ?? false); + const [password, setPassword] = useState(""); + + const pwErrors = validatePassword(password); + + // update the user in the backend + async function submit(e: FormEvent) { + const formData = Object.fromEntries(new FormData(e.currentTarget)) as { + userName: string; + password: string; + admin: string; + }; + + const data = { + ...formData, + admin: formData.admin !== undefined, + }; + + // if we modify ourself, set admin to true since it isn't included in the form data because the checkbox is disabled + data.admin ||= props.value?.userName === zustand.getState().user?.userName; + + props.onSubmit(data); + } + + return ( + +
{ + e.preventDefault(); + submit(e); + }} + > + + +

{props.header}

+
+ + + 0 ? "warning" : "default"} + name="password" + variant="bordered" + value={password} + onValueChange={setPassword} + isInvalid={password.length > 0 && pwErrors.length > 0} + errorMessage={ +
    + {pwErrors.map((e, ii) => ( +
  • {e}
  • + ))} +
+ } + /> + + Admin + +
+ {props.footer} +
+
+
+ ); +} diff --git a/client/src/app/admin/(users)/Users.tsx b/client/src/app/admin/(users)/Users.tsx index f6d25f5..e490af5 100644 --- a/client/src/app/admin/(users)/Users.tsx +++ b/client/src/app/admin/(users)/Users.tsx @@ -1,7 +1,8 @@ import { apiCall } from "@/lib"; -import { User } from "@/Zustand"; +import zustand, { User } from "@/Zustand"; import { Button, + ButtonGroup, Checkbox, Table, TableBody, @@ -12,14 +13,17 @@ import { Tooltip, } from "@heroui/react"; import { useAsyncList } from "@react-stately/data"; -import { FormEvent, useState } from "react"; +import { useState } from "react"; import AddUser from "./AddUser"; -import { AddLarge, Edit } from "@carbon/icons-react"; +import { AddLarge, Edit, TrashCan } from "@carbon/icons-react"; import EditUser from "./EditUser"; +import DeleteConfirmation from "@/components/DeleteConfirmation"; export default function Users() { const [showAddUser, setShowAddUser] = useState(false); const [editUser, setEditUser] = useState(); + const [deleteUser, setDeleteUser] = useState(); + const loggedInUser = zustand((state) => state.user); const users = useAsyncList({ async load() { @@ -64,23 +68,22 @@ export default function Users() { }, }); - // send an addUser request to the backend then reload the table - async function addUser(e: FormEvent) { - const data = Object.fromEntries(new FormData(e.currentTarget)); + async function sendDeleteUser(userName: User["userName"] | undefined) { + if (!!userName) { + const result = await apiCall("DELETE", "users", { + userName, + }); - const result = await apiCall("POST", "users", undefined, { - ...data, - admin: data.admin === "admin", - }); - - if (result.ok) { - users.reload(); + if (result.ok) { + users.reload(); + setDeleteUser(undefined); + } } } // content above the user-tabel const topContent = ( - <> +
- +
); return ( @@ -101,6 +104,13 @@ export default function Users() { topContent={topContent} sortDescriptor={users.sortDescriptor} onSortChange={users.sort} + topContentPlacement="outside" + classNames={{ + wrapper: "bg-accent-4", + tr: "even:bg-accent-5 ", + th: "font-subheadline text-xl text-accent-1 bg-transparent ", + thead: "[&>tr]:first:!shadow-border", + }} > @@ -119,16 +129,34 @@ export default function Users() { - + + + + )} @@ -138,11 +166,14 @@ export default function Users() { void addUser(e)} + onSuccess={() => { + setShowAddUser(false); + users.reload(); + }} /> !isOpen ? setEditUser(undefined) : undefined } @@ -151,6 +182,16 @@ export default function Users() { setEditUser(undefined); }} /> + + (!isOpen ? setDeleteUser(undefined) : null)} + onDelete={() => sendDeleteUser(deleteUser?.userName)} + itemName="User" + > + {" "} + The user {deleteUser?.userName} will be deleted. + ); } diff --git a/client/src/app/assignments/page.tsx b/client/src/app/assignments/page.tsx index c256292..e92505c 100644 --- a/client/src/app/assignments/page.tsx +++ b/client/src/app/assignments/page.tsx @@ -238,14 +238,15 @@ export default function AdminPanel() { topContent={topContent} topContentPlacement="outside" isHeaderSticky + isStriped sortDescriptor={events.sortDescriptor} - onSortChange={events.sort} classNames={{ wrapper: "bg-accent-4", tr: "even:bg-accent-5 ", th: "font-subheadline text-xl text-accent-1 bg-transparent ", thead: "[&>tr]:first:!shadow-border", }} + onSortChange={events.sort} className="w-fit max-w-full" > diff --git a/client/src/components/Colorselector.tsx b/client/src/components/Colorselector.tsx index 97fe783..1a804f0 100644 --- a/client/src/components/Colorselector.tsx +++ b/client/src/components/Colorselector.tsx @@ -31,12 +31,14 @@ export function color2Tailwind(v: string): string | undefined { } export default function ColorSelector(props: { + isRequired?: boolean; name?: string; value?: string; onValueChange?: (value: string) => void; }) { return ( void; children: React.ReactNode; - header: React.ReactNode; + itemName: string; onDelete?: () => void; }) { return ( @@ -28,7 +28,7 @@ export default function DeleteConfirmation(props: { > -

{props.header}

+

Delete {props.itemName}

{props.children} @@ -40,7 +40,7 @@ export default function DeleteConfirmation(props: { color="danger" onPress={() => props.onDelete?.()} > - Delete event + Delete {props.itemName}
diff --git a/client/src/lib.ts b/client/src/lib.ts index d5be209..3c5bf7b 100644 --- a/client/src/lib.ts +++ b/client/src/lib.ts @@ -83,7 +83,7 @@ export class DateFormatter { } } -export function vaidatePassword(password: string): string[] { +export function validatePassword(password: string): string[] { const errors = []; if (password.length < 12) {