From 6d38048e67e957771c3cd2efe6965a7097f9941c Mon Sep 17 00:00:00 2001 From: z1glr Date: Wed, 29 Jan 2025 19:42:32 +0000 Subject: [PATCH] refactored some database retrievle stuff --- .../pkg/db/availabilities/availabilities.go | 4 +- .../db/availabilities/userAvailabilities.go | 36 ----- backend/pkg/db/events/events.go | 70 ++++++--- backend/pkg/db/tasks/tasks.go | 8 +- backend/pkg/db/users/User.go | 139 +++++++++++++++++- backend/pkg/db/users/main.go | 119 --------------- backend/pkg/router/events.go | 23 ++- client/src/Zustand.ts | 1 + client/src/app/admin/(users)/Users.tsx | 18 +-- .../src/app/assignments/VolunteerSelector.tsx | 47 +++++- client/src/lib.ts | 25 +++- 11 files changed, 281 insertions(+), 209 deletions(-) delete mode 100644 backend/pkg/db/availabilities/userAvailabilities.go delete mode 100644 backend/pkg/db/users/main.go diff --git a/backend/pkg/db/availabilities/availabilities.go b/backend/pkg/db/availabilities/availabilities.go index ce29d59..f23a616 100644 --- a/backend/pkg/db/availabilities/availabilities.go +++ b/backend/pkg/db/availabilities/availabilities.go @@ -1,8 +1,6 @@ package availabilities -import ( - "github.com/johannesbuehl/golunteer/backend/pkg/db" -) +import "github.com/johannesbuehl/golunteer/backend/pkg/db" type AvailabilityID int diff --git a/backend/pkg/db/availabilities/userAvailabilities.go b/backend/pkg/db/availabilities/userAvailabilities.go deleted file mode 100644 index 91cf145..0000000 --- a/backend/pkg/db/availabilities/userAvailabilities.go +++ /dev/null @@ -1,36 +0,0 @@ -package availabilities - -import ( - "github.com/johannesbuehl/golunteer/backend/pkg/db" -) - -type eventAvailabilities struct { - UserName string `db:"userName"` - AvailabilityID int `db:"availabilityID"` -} - -type AvailabilityMap map[int][]string - -func Event(eventID int) (AvailabilityMap, error) { - // get the availabilities for the event - var availabilitiesRows []eventAvailabilities - - if err := db.DB.Select(&availabilitiesRows, "SELECT userName, availabilityID FROM USER_AVAILABILITIES WHERE eventID = ?", eventID); err != nil { - return nil, err - } else { - // transform the result into a map - eventAvailabilities := AvailabilityMap{} - - // get the availabilities - for _, a := range availabilitiesRows { - // if there is no slice for this availability, create it - if _, exists := eventAvailabilities[a.AvailabilityID]; !exists { - eventAvailabilities[a.AvailabilityID] = make([]string, 0) - } - - eventAvailabilities[a.AvailabilityID] = append(eventAvailabilities[a.AvailabilityID], a.UserName) - } - - return eventAvailabilities, nil - } -} diff --git a/backend/pkg/db/events/events.go b/backend/pkg/db/events/events.go index bc7e6dd..388d128 100644 --- a/backend/pkg/db/events/events.go +++ b/backend/pkg/db/events/events.go @@ -4,14 +4,26 @@ import ( "slices" "github.com/johannesbuehl/golunteer/backend/pkg/db" - "github.com/johannesbuehl/golunteer/backend/pkg/db/availabilities" "github.com/johannesbuehl/golunteer/backend/pkg/logger" ) +// empty UserName interface so that the functions of this package can accept UserName variables without a circular import +type UserName interface{} +type TaskID interface{} + +type EventID int + +type eventAvailabilities struct { + UserName string `db:"userName"` + AvailabilityID int `db:"availabilityID"` +} + +type AvailabilityMap map[int][]string + type EventData struct { - EventID int `db:"eventID" json:"eventID" validate:"required"` - Date string `db:"date" json:"date" validate:"required"` - Description string `db:"description" json:"description"` + EventID EventID `db:"eventID" json:"eventID" validate:"required"` + Date string `db:"date" json:"date" validate:"required"` + Description string `db:"description" json:"description"` } type EventPatch struct { @@ -20,7 +32,7 @@ type EventPatch struct { } type EventAssignment struct { - TaskID int `db:"taskID" json:"taskID"` + TaskID TaskID `db:"taskID" json:"taskID"` TaskName string `db:"taskName" json:"taskName"` UserName *string `db:"userName" json:"userName"` } @@ -32,7 +44,7 @@ type EventWithAssignments struct { type EventWithAvailabilities struct { EventWithAssignments - Availabilities availabilities.AvailabilityMap `json:"availabilities"` + Availabilities AvailabilityMap `json:"availabilities"` } type EventWithAssignmentsUserAvailability struct { @@ -49,7 +61,7 @@ type EventCreate struct { // transform the database-entry to an WithAssignments func (e EventData) WithAssignments() (EventWithAssignments, error) { // get the assignments associated with the event - if assignemnts, err := Assignments(e.EventID); err != nil { + if assignemnts, err := e.EventID.Assignments(); err != nil { return EventWithAssignments{}, err } else { return EventWithAssignments{ @@ -72,13 +84,37 @@ func (e EventWithAssignments) WithUserAvailability(userName string) (EventWithAs } } +func (eventID EventID) Availabilities() (AvailabilityMap, error) { + // get the availabilities for the event + var availabilitiesRows []eventAvailabilities + + if err := db.DB.Select(&availabilitiesRows, "SELECT userName, availabilityID FROM USER_AVAILABILITIES WHERE eventID = ?", eventID); err != nil { + return nil, err + } else { + // transform the result into a map + eventAvailabilities := AvailabilityMap{} + + // get the availabilities + for _, a := range availabilitiesRows { + // if there is no slice for this availability, create it + if _, exists := eventAvailabilities[a.AvailabilityID]; !exists { + eventAvailabilities[a.AvailabilityID] = make([]string, 0) + } + + eventAvailabilities[a.AvailabilityID] = append(eventAvailabilities[a.AvailabilityID], a.UserName) + } + + return eventAvailabilities, nil + } +} + func (e EventData) WithAvailabilities() (EventWithAvailabilities, error) { // get the event with assignments if event, err := e.WithAssignments(); err != nil { return EventWithAvailabilities{}, err // get the availabilities - } else if availabilities, err := availabilities.Event(e.EventID); err != nil { + } else if availabilities, err := e.EventID.Availabilities(); err != nil { return EventWithAvailabilities{}, err } else { return EventWithAvailabilities{ @@ -141,7 +177,7 @@ func Update(event EventPatch) error { } else { type Task struct { TaskID - EventID int `db:"eventID"` + EventID EventID `db:"eventID"` } // extract the rows that need to be deleted @@ -235,25 +271,13 @@ func WithAvailabilities() ([]EventWithAvailabilities, error) { } } -func GetUserAvailability(eventID int, userName string) (*availabilities.AvailabilityID, error) { - var availabilityID struct { - AvailabilityID *availabilities.AvailabilityID `db:"availabilityID"` - } - - if err := db.DB.QueryRowx("SELECT availabilityID FROM USER_AVAILABILITIES WHERE eventID = $1 AND userName = $2", eventID, userName).StructScan(&availabilityID); err != nil { - return availabilityID.AvailabilityID, err - } else { - return availabilityID.AvailabilityID, nil - } -} - func Delete(eventId int) error { _, err := db.DB.Exec("DELETE FROM EVENTS WHERE eventID = ?", eventId) return err } -func Assignments(eventID int) ([]EventAssignment, error) { +func (eventID EventID) Assignments() ([]EventAssignment, error) { // get the assignments from the database var assignmentRows []EventAssignment @@ -265,7 +289,7 @@ func Assignments(eventID int) ([]EventAssignment, error) { } // set the assignment of an user to a task for a specific event -func SetAssignment(eventID, taskID int, userName string) error { +func (eventID EventID) SetAssignment(taskID TaskID, userName UserName) error { _, err := db.DB.Exec("UPDATE USER_ASSIGNMENTS SET userName = $1 WHERE eventID = $2 AND taskID = $3", userName, eventID, taskID) return err diff --git a/backend/pkg/db/tasks/tasks.go b/backend/pkg/db/tasks/tasks.go index 5fc1c25..3eef39d 100644 --- a/backend/pkg/db/tasks/tasks.go +++ b/backend/pkg/db/tasks/tasks.go @@ -4,8 +4,10 @@ import ( "github.com/johannesbuehl/golunteer/backend/pkg/db" ) +type TaskID int + type TaskDB struct { - TaskID int `json:"taskID" db:"taskID" validate:"required"` + TaskID TaskID `json:"taskID" db:"taskID" validate:"required"` Task `valdate:"required" ` } @@ -25,12 +27,12 @@ func GetSlice() ([]TaskDB, error) { } -func GetMap() (map[int]Task, error) { +func GetMap() (map[TaskID]Task, error) { if tasksRaw, err := GetSlice(); err != nil { return nil, err } else { // convert the result in a map - tasks := map[int]Task{} + tasks := map[TaskID]Task{} for _, a := range tasksRaw { tasks[a.TaskID] = Task{ diff --git a/backend/pkg/db/users/User.go b/backend/pkg/db/users/User.go index be5c5b3..28919e1 100644 --- a/backend/pkg/db/users/User.go +++ b/backend/pkg/db/users/User.go @@ -3,10 +3,125 @@ package users import ( "github.com/google/uuid" "github.com/johannesbuehl/golunteer/backend/pkg/db" + "github.com/johannesbuehl/golunteer/backend/pkg/db/availabilities" "github.com/johannesbuehl/golunteer/backend/pkg/db/events" + "github.com/johannesbuehl/golunteer/backend/pkg/db/tasks" "github.com/johannesbuehl/golunteer/backend/pkg/logger" + "golang.org/x/crypto/bcrypt" ) +type UserName string + +type UserDB struct { + UserName UserName `db:"userName" json:"userName"` + Admin bool `db:"admin" json:"admin"` +} + +type User struct { + UserDB + PossibleTasks []int `json:"possibleTasks"` +} + +type UserChangePassword struct { + UserName `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) +} + +func Get() ([]User, error) { + // get the usersDB from the database + var usersDB []UserDB + + // get the users + if err := db.DB.Select(&usersDB, "SELECT userName, admin FROM USERS"); err != nil { + return nil, err + } else { + users := make([]User, len(usersDB)) + + // for the individual users, get the possible tasks + for ii, userDB := range usersDB { + if user, err := userDB.ToUser(); err != nil { + users = append(users[:ii], users[ii+1:]...) + } else { + users[ii] = user + } + } + + return users, nil + } +} + +func (userName UserName) TokenID() (string, error) { + var dbResult struct { + TokenID string `db:"tokenID"` + } + + err := db.DB.Get(&dbResult, "SELECT tokenID FROM USERS WHERE userName = ?", userName) + + return dbResult.TokenID, err +} + +type UserAdd struct { + UserName `json:"userName" validate:"required" db:"userName"` + Password string `json:"password" validate:"required,min=12,max=64"` + Admin bool `json:"admin" db:"admin"` + PossibleTasks []tasks.TaskID `json:"possibleTasks" validate:"required"` +} + +func Add(user UserAdd) error { + // try to hash the password + if hash, err := hashPassword(user.Password); err != nil { + return err + } else { + insertUser := struct { + UserAdd + Password []byte `db:"password"` + TokenID string `db:"tokenID"` + }{ + UserAdd: user, + Password: hash, + TokenID: uuid.NewString(), + } + + if _, err := db.DB.NamedExec("INSERT INTO USERS (userName, password, admin, tokenID) VALUES (:userName, :password, :admin, :tokenID)", insertUser); err != nil { + return err + } + + // set the possible Tasks + for _, task := range user.PossibleTasks { + if _, err := db.DB.Exec("INSERT INTO USER_TASKS (userName, taskID) VALUES ($1, $2)", user.UserName, task); err != nil { + return err + } + } + + return err + } +} + +func Delete(userName string) error { + _, err := db.DB.Exec("DELETE FROM USERS WHERE userName = $1", userName) + + return err +} + +func (u *UserDB) ToUser() (User, error) { + // get the possible tasks + tasks := make([]int, 0) + + if err := db.DB.Select(&tasks, "SELECT taskID FROM USER_TASKS WHERE userName = $1", u.UserName); err != nil { + return User{}, err + } else { + return User{ + UserDB: *u, + PossibleTasks: tasks, + }, nil + } +} + func (userName UserName) WithUserAvailability() ([]events.EventWithAssignmentsUserAvailability, error) { var events []events.EventWithAssignmentsUserAvailability @@ -62,7 +177,7 @@ func (userName UserName) ChangePassword(password string) (string, error) { } } -func (userName UserName) SetTasks(tasks []int) error { +func (userName UserName) SetTasks(tasks []tasks.TaskID) error { // remove all current possible tasks if _, err := db.DB.Exec("DELETE FROM USER_TASKS WHERE userName = $1", userName); err != nil { return err @@ -79,6 +194,16 @@ func (userName UserName) SetTasks(tasks []int) error { } } +func (userName UserName) CheckTask(taskID tasks.TaskID) (bool, error) { + var check bool + + if err := db.DB.Select(&check, "SELECT 1 FROM USER_TASKS WHERE userName = $1 AND taskID = $2", userName, taskID); err != nil { + return false, err + } else { + return check, nil + } +} + func (userName UserName) UserPending() ([]events.EventData, error) { var result []events.EventData @@ -137,3 +262,15 @@ func (userName UserName) SetEventAvailability(eventID, availabilityID int) error return err } + +func (userName UserName) GetUserAvailability(eventID events.EventID) (*availabilities.AvailabilityID, error) { + var availabilityID struct { + AvailabilityID *availabilities.AvailabilityID `db:"availabilityID"` + } + + if err := db.DB.QueryRowx("SELECT availabilityID FROM USER_AVAILABILITIES WHERE eventID = $1 AND userName = $2", eventID, userName).StructScan(&availabilityID); err != nil { + return availabilityID.AvailabilityID, err + } else { + return availabilityID.AvailabilityID, nil + } +} diff --git a/backend/pkg/db/users/main.go b/backend/pkg/db/users/main.go deleted file mode 100644 index 9982ad9..0000000 --- a/backend/pkg/db/users/main.go +++ /dev/null @@ -1,119 +0,0 @@ -package users - -import ( - "github.com/google/uuid" - "github.com/johannesbuehl/golunteer/backend/pkg/db" - "golang.org/x/crypto/bcrypt" -) - -type UserName string - -type UserDB struct { - UserName UserName `db:"userName" json:"userName"` - Admin bool `db:"admin" json:"admin"` -} - -type User struct { - UserDB - PossibleTasks []int `json:"possibleTasks"` -} - -type UserChangePassword struct { - UserName `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) -} - -func Get() ([]User, error) { - // get the usersDB from the database - var usersDB []UserDB - - // get the users - if err := db.DB.Select(&usersDB, "SELECT userName, admin FROM USERS"); err != nil { - return nil, err - } else { - users := make([]User, len(usersDB)) - - // for the individual users, get the possible tasks - for ii, userDB := range usersDB { - if user, err := userDB.ToUser(); err != nil { - users = append(users[:ii], users[ii+1:]...) - } else { - users[ii] = user - } - } - - return users, nil - } -} - -func (userName UserName) TokenID() (string, error) { - var dbResult struct { - TokenID string `db:"tokenID"` - } - - err := db.DB.Get(&dbResult, "SELECT tokenID FROM USERS WHERE userName = ?", userName) - - return dbResult.TokenID, err -} - -type UserAdd struct { - UserName `json:"userName" validate:"required" db:"userName"` - Password string `json:"password" validate:"required,min=12,max=64"` - Admin bool `json:"admin" db:"admin"` - PossibleTasks []int `json:"possibleTasks" validate:"required"` -} - -func Add(user UserAdd) error { - // try to hash the password - if hash, err := hashPassword(user.Password); err != nil { - return err - } else { - insertUser := struct { - UserAdd - Password []byte `db:"password"` - TokenID string `db:"tokenID"` - }{ - UserAdd: user, - Password: hash, - TokenID: uuid.NewString(), - } - - if _, err := db.DB.NamedExec("INSERT INTO USERS (userName, password, admin, tokenID) VALUES (:userName, :password, :admin, :tokenID)", insertUser); err != nil { - return err - } - - // set the possible Tasks - for _, task := range user.PossibleTasks { - if _, err := db.DB.Exec("INSERT INTO USER_TASKS (userName, taskID) VALUES ($1, $2)", user.UserName, task); err != nil { - return err - } - } - - return err - } -} - -func Delete(userName string) error { - _, err := db.DB.Exec("DELETE FROM USERS WHERE userName = $1", userName) - - return err -} - -func (u *UserDB) ToUser() (User, error) { - // get the possible tasks - tasks := make([]int, 0) - - if err := db.DB.Select(&tasks, "SELECT taskID FROM USER_TASKS WHERE userName = $1", u.UserName); err != nil { - return User{}, err - } else { - return User{ - UserDB: *u, - PossibleTasks: tasks, - }, nil - } -} diff --git a/backend/pkg/router/events.go b/backend/pkg/router/events.go index f0cbe5d..5421cb6 100644 --- a/backend/pkg/router/events.go +++ b/backend/pkg/router/events.go @@ -7,6 +7,8 @@ import ( "github.com/jmoiron/sqlx" "github.com/johannesbuehl/golunteer/backend/pkg/db" "github.com/johannesbuehl/golunteer/backend/pkg/db/events" + "github.com/johannesbuehl/golunteer/backend/pkg/db/tasks" + "github.com/johannesbuehl/golunteer/backend/pkg/db/users" ) func (a *Handler) postEvent() { @@ -189,24 +191,25 @@ func (a *Handler) putEventAssignment() { logger.Warn().Msg("setting event-assignment failed: user is no admin") // retrieve the eventID from the query - } else if eventID := a.C.QueryInt("eventID", -1); eventID == -1 { + } else if eventID := events.EventID(a.C.QueryInt("eventID", -1)); eventID == -1 { a.Status = fiber.StatusBadRequest logger.Warn().Msg("setting event-assignment failed: query is missing \"eventID\"") // retrieve the taskID from the query - } else if taskID := a.C.QueryInt("taskID", -1); taskID == -1 { + } else if taskID := tasks.TaskID(a.C.QueryInt("taskID", -1)); taskID == -1 { a.Status = fiber.StatusBadRequest logger.Log().Msg("setting event-assignment failed: query is missing \"taskID\"") // parse the body - } else if userName := string(a.C.Body()); userName == "" { + } else if userName := users.UserName(a.C.Body()); userName == "" { a.Status = fiber.StatusBadRequest logger.Log().Msg("setting event-assignment failed: body is missing") + // check wether the user has actually entered an availability for the event - } else if availabilityID, err := events.GetUserAvailability(eventID, userName); err != nil { + } else if availabilityID, err := userName.GetUserAvailability(eventID); err != nil { a.Status = fiber.StatusBadRequest logger.Log().Msgf("setting event-assignment failed: can't check users availability: %v", err) @@ -215,8 +218,18 @@ func (a *Handler) putEventAssignment() { logger.Log().Msgf("setting event-assignment failed: user %q isn't available for event with eventID = %d", userName, eventID) + // check wether the user can be assigned for this task + } else if check, err := userName.CheckTask(taskID); err != nil { + a.Status = fiber.StatusInternalServerError + + logger.Error().Msgf("setting event-assignment failed: can't check wether the task with taskID = %d is possible: %v", taskID, err) + } else if !check { + a.Status = fiber.StatusBadRequest + + logger.Log().Msgf("setting event-assignment failed: task with taskID = %d is not possible for user", taskID) + // set the availability in the database - } else if err := events.SetAssignment(eventID, taskID, userName); err != nil { + } else if err := eventID.SetAssignment(taskID, userName); err != nil { a.Status = fiber.StatusBadRequest logger.Warn().Msgf("setting event-assignment failed: can't write to database: %v", err) diff --git a/client/src/Zustand.ts b/client/src/Zustand.ts index c2e2700..2aefe9e 100644 --- a/client/src/Zustand.ts +++ b/client/src/Zustand.ts @@ -43,6 +43,7 @@ interface Zustand { user: StateUser | null; tasks?: Task[]; availabilities?: Availability[]; + users?: User[]; patch: (zustand?: Partial) => void; reset: (zustand?: Partial) => void; } diff --git a/client/src/app/admin/(users)/Users.tsx b/client/src/app/admin/(users)/Users.tsx index e490af5..fa61ce4 100644 --- a/client/src/app/admin/(users)/Users.tsx +++ b/client/src/app/admin/(users)/Users.tsx @@ -1,4 +1,4 @@ -import { apiCall } from "@/lib"; +import { apiCall, getUsers } from "@/lib"; import zustand, { User } from "@/Zustand"; import { Button, @@ -27,19 +27,9 @@ export default function Users() { const users = useAsyncList({ async load() { - const result = await apiCall("GET", "users"); - - if (result.ok) { - const users = (await result.json()) as User[]; - - return { - items: users, - }; - } else { - return { - items: [], - }; - } + return { + items: await getUsers(), + }; }, async sort({ items, sortDescriptor }) { return { diff --git a/client/src/app/assignments/VolunteerSelector.tsx b/client/src/app/assignments/VolunteerSelector.tsx index 56ee6f9..9cc2581 100644 --- a/client/src/app/assignments/VolunteerSelector.tsx +++ b/client/src/app/assignments/VolunteerSelector.tsx @@ -11,9 +11,10 @@ import { DropdownTrigger, } from "@heroui/react"; import { EventWithAvailabilities } from "./page"; -import { ReactElement } from "react"; +import { ReactElement, useEffect, useState } from "react"; import { Availability } from "../admin/(availabilities)/AvailabilityEditor"; -import { apiCall, classNames } from "@/lib"; +import { apiCall, classNames, getUsers } from "@/lib"; +import { useAsyncList } from "@react-stately/data"; export default function VolunteerSelector({ event, @@ -26,6 +27,46 @@ export default function VolunteerSelector({ getAvailabilityById: (availabilityID: number) => Availability; onReloadRequest: () => void; }) { + const [selectableUsers, setSelectableUsers] = useState<[string, string[]][]>( + [], + ); + + const users = useAsyncList({ + async load() { + return { + items: await getUsers(), + }; + }, + }); + + useEffect(() => { + // create a set with all the users that can be assigned to this task + const validUsers = new Set( + users.items + .filter((user) => user.possibleTasks.includes(task.taskID)) + .map((user) => user.userName), + ); + + setSelectableUsers( + Object.entries(event.availabilities) + .map( + ([availabilityID, availabilityUsers]): + | [string, string[]] + | undefined => { + const thisUsers = availabilityUsers.filter((userName) => + validUsers.has(userName), + ); + + // if there is at least one user over, return it + if (thisUsers.length > 0) { + return [availabilityID, thisUsers]; + } + }, + ) + .filter((i) => !!i), + ); + }, [event.availabilities, users.items, task.taskID]); + async function sendVolunteerAssignment( eventID: number, taskID: number, @@ -77,7 +118,7 @@ export default function VolunteerSelector({ sendVolunteerAssignment(event.eventID, task.taskID, a as string) } > - {Object.entries(event.availabilities).map( + {selectableUsers.map( ([availabilityId, volunteers], iAvailability, aAvailabilities) => ( = { [K in keyof T]: string }; @@ -190,7 +190,7 @@ export async function getAvailabilities(): Promise { if (result.ok) { const availabilities = await result.json(); - state.patch({ availabilities: availabilities }); + state.patch({ availabilities }); return availabilities; } else { @@ -198,3 +198,24 @@ export async function getAvailabilities(): Promise { } } } + +export async function getUsers(): Promise { + // check wether it is cached in zustand + const state = zustand.getState(); + + if (!!state.users) { + return state.users; + } else { + const result = await apiCall("GET", "users"); + + if (result.ok) { + const users = await result.json(); + + state.patch({ users }); + + return users; + } else { + return []; + } + } +}