diff --git a/backend/pkg/db/events/events.go b/backend/pkg/db/events/events.go index 7bf53e2..bc7e6dd 100644 --- a/backend/pkg/db/events/events.go +++ b/backend/pkg/db/events/events.go @@ -247,48 +247,6 @@ func GetUserAvailability(eventID int, userName string) (*availabilities.Availabi } } -func WithUserAvailability(userName string) ([]EventWithAssignmentsUserAvailability, error) { - var events []EventWithAssignmentsUserAvailability - - if err := db.DB.Select(&events, "SELECT EVENTS.eventID, EVENTS.description, EVENTS.date, USER_AVAILABILITIES.availabilityID FROM EVENTS LEFT JOIN USER_AVAILABILITIES ON EVENTS.eventID = USER_AVAILABILITIES.eventID AND USER_AVAILABILITIES.userName = $1", userName); err != nil { - return nil, err - } else { - // get the assignments for every event - for ii, event := range events { - if eventWithAssignments, err := event.EventWithAssignments.EventData.WithAssignments(); err != nil { - // remove the current event from the events - events = append(events[:ii], events[ii+1:]...) - } else { - events[ii].EventWithAssignments = eventWithAssignments - } - } - - return events, nil - } -} - -func UserPending(userName string) ([]EventData, error) { - var result []EventData - - if err := db.DB.Select(&result, "SELECT eventID, date, description FROM EVENTS WHERE NOT EXISTS (SELECT 1 FROM USER_AVAILABILITIES WHERE USER_AVAILABILITIES.eventID = EVENTS.eventID AND USER_AVAILABILITIES.userName = ?)", userName); err != nil { - return nil, err - } else { - return result, nil - } -} - -func UserPendingCount(userName string) (int, error) { - var result struct { - Count int `db:"count(*)"` - } - - if err := db.DB.QueryRowx("SELECT count(*) FROM EVENTS WHERE NOT EXISTS (SELECT 1 FROM USER_AVAILABILITIES WHERE USER_AVAILABILITIES.eventID = EVENTS.eventID AND USER_AVAILABILITIES.userName = ?)", userName).StructScan(&result); err != nil { - return 0, err - } else { - return result.Count, nil - } -} - func Delete(eventId int) error { _, err := db.DB.Exec("DELETE FROM EVENTS WHERE eventID = ?", eventId) @@ -306,43 +264,6 @@ func Assignments(eventID int) ([]EventAssignment, error) { } } -func User(userName string) ([]EventWithAssignments, error) { - // get all assignments of the user - - // var eventsDB []EventWithAssignment - var eventsDB []EventData - - // get all the events where the volunteer is assigned a task - if err := db.DB.Select(&eventsDB, "SELECT DISTINCT EVENTS.date, EVENTS.description, EVENTS.eventID FROM USER_ASSIGNMENTS INNER JOIN EVENTS ON USER_ASSIGNMENTS.eventID = EVENTS.eventID WHERE userName = $1", userName); err != nil { - return nil, err - } else { - // for each event create an event with assignments - events := make([]EventWithAssignments, len(eventsDB)) - - for ii, event := range eventsDB { - if eventsWithAssignment, err := event.WithAssignments(); err != nil { - logger.Logger.Error().Msgf("can't get assignments for event with eventID = %d: %v", event.EventID, err) - - // remove the last element from the return-slice, since there is now one element less - if len(events) > 0 { - events = events[:len(events)-1] - } - } else { - events[ii] = eventsWithAssignment - } - } - - return events, nil - } -} - -// set the availability of an user for a specific event -func SetUserAvailability(eventID, availabilityID int, userName string) error { - _, err := db.DB.Exec("INSERT INTO USER_AVAILABILITIES (userName, eventID, availabilityID) VALUES ($1, $2, $3) ON CONFLICT (userName, eventID) DO UPDATE SET availabilityID = $3", userName, eventID, availabilityID) - - return err -} - // set the assignment of an user to a task for a specific event func SetAssignment(eventID, taskID int, userName string) error { _, err := db.DB.Exec("UPDATE USER_ASSIGNMENTS SET userName = $1 WHERE eventID = $2 AND taskID = $3", userName, eventID, taskID) diff --git a/backend/pkg/db/users/User.go b/backend/pkg/db/users/User.go new file mode 100644 index 0000000..be5c5b3 --- /dev/null +++ b/backend/pkg/db/users/User.go @@ -0,0 +1,139 @@ +package users + +import ( + "github.com/google/uuid" + "github.com/johannesbuehl/golunteer/backend/pkg/db" + "github.com/johannesbuehl/golunteer/backend/pkg/db/events" + "github.com/johannesbuehl/golunteer/backend/pkg/logger" +) + +func (userName UserName) WithUserAvailability() ([]events.EventWithAssignmentsUserAvailability, error) { + var events []events.EventWithAssignmentsUserAvailability + + if err := db.DB.Select(&events, "SELECT EVENTS.eventID, EVENTS.description, EVENTS.date, USER_AVAILABILITIES.availabilityID FROM EVENTS LEFT JOIN USER_AVAILABILITIES ON EVENTS.eventID = USER_AVAILABILITIES.eventID AND USER_AVAILABILITIES.userName = $1", userName); err != nil { + return nil, err + } else { + // get the assignments for every event + for ii, event := range events { + if eventWithAssignments, err := event.EventWithAssignments.EventData.WithAssignments(); err != nil { + // remove the current event from the events + events = append(events[:ii], events[ii+1:]...) + } else { + events[ii].EventWithAssignments = eventWithAssignments + } + } + + return events, nil + } +} + +func (userName UserName) ChangeName(newName UserName) error { + _, err := db.DB.Exec("UPDATE USERS SET userName = ? WHERE userName = ?", newName, userName) + + return err +} + +func (userName UserName) SetAdmin(admin bool) error { + _, err := db.DB.Exec("UPDATE USERS SET admin = ? WHERE userName = ?", admin, userName) + + return err +} + +func (userName UserName) ChangePassword(password string) (string, error) { + // try to hash teh password + if hash, err := hashPassword(password); err != nil { + return "", err + } else { + execStruct := struct { + UserName `db:"userName"` + Password []byte `db:"password"` + TokenID string `db:"tokenID"` + }{ + UserName: userName, + Password: hash, + TokenID: uuid.NewString(), + } + + if _, err := db.DB.NamedExec("UPDATE USERS SET tokenID = :tokenID, password = :password WHERE name = :userName", execStruct); err != nil { + return "", err + } else { + return execStruct.TokenID, nil + } + } +} + +func (userName UserName) SetTasks(tasks []int) error { + // remove all current possible tasks + if _, err := db.DB.Exec("DELETE FROM USER_TASKS WHERE userName = $1", userName); err != nil { + return err + + // set the new tasks + } else { + for _, task := range tasks { + if _, err := db.DB.Exec("INSERT INTO USER_TASKS (userName, taskID) VALUES ($1, $2)", userName, task); err != nil { + return err + } + } + + return nil + } +} + +func (userName UserName) UserPending() ([]events.EventData, error) { + var result []events.EventData + + if err := db.DB.Select(&result, "SELECT eventID, date, description FROM EVENTS WHERE NOT EXISTS (SELECT 1 FROM USER_AVAILABILITIES WHERE USER_AVAILABILITIES.eventID = EVENTS.eventID AND USER_AVAILABILITIES.userName = ?)", userName); err != nil { + return nil, err + } else { + return result, nil + } +} + +func (userName UserName) UserPendingCount() (int, error) { + var result struct { + Count int `db:"count(*)"` + } + + if err := db.DB.QueryRowx("SELECT count(*) FROM EVENTS WHERE NOT EXISTS (SELECT 1 FROM USER_AVAILABILITIES WHERE USER_AVAILABILITIES.eventID = EVENTS.eventID AND USER_AVAILABILITIES.userName = ?)", userName).StructScan(&result); err != nil { + return 0, err + } else { + return result.Count, nil + } +} + +func (userName UserName) GetAssignedEvents() ([]events.EventWithAssignments, error) { + // get all assignments of the user + + // var eventsDB []EventWithAssignment + var eventsDB []events.EventData + + // get all the events where the volunteer is assigned a task + if err := db.DB.Select(&eventsDB, "SELECT DISTINCT EVENTS.date, EVENTS.description, EVENTS.eventID FROM USER_ASSIGNMENTS INNER JOIN EVENTS ON USER_ASSIGNMENTS.eventID = EVENTS.eventID WHERE userName = $1", userName); err != nil { + return nil, err + } else { + // for each event create an event with assignments + events := make([]events.EventWithAssignments, len(eventsDB)) + + for ii, event := range eventsDB { + if eventsWithAssignment, err := event.WithAssignments(); err != nil { + logger.Logger.Error().Msgf("can't get assignments for event with eventID = %d: %v", event.EventID, err) + + // remove the last element from the return-slice, since there is now one element less + if len(events) > 0 { + events = events[:len(events)-1] + } + } else { + events[ii] = eventsWithAssignment + } + } + + return events, nil + } +} + +// set the availability of an user for a specific event +func (userName UserName) SetEventAvailability(eventID, availabilityID int) error { + _, err := db.DB.Exec("INSERT INTO USER_AVAILABILITIES (userName, eventID, availabilityID) VALUES ($1, $2, $3) ON CONFLICT (userName, eventID) DO UPDATE SET availabilityID = $3", userName, eventID, availabilityID) + + return err +} diff --git a/backend/pkg/db/users/main.go b/backend/pkg/db/users/main.go new file mode 100644 index 0000000..9982ad9 --- /dev/null +++ b/backend/pkg/db/users/main.go @@ -0,0 +1,119 @@ +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/db/users/users.go b/backend/pkg/db/users/users.go deleted file mode 100644 index f1e493b..0000000 --- a/backend/pkg/db/users/users.go +++ /dev/null @@ -1,111 +0,0 @@ -package users - -import ( - "github.com/google/uuid" - "github.com/johannesbuehl/golunteer/backend/pkg/db" - "golang.org/x/crypto/bcrypt" -) - -type User struct { - UserName string `db:"userName" json:"userName"` - 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) -} - -func Get() ([]User, error) { - // get the users from the database - var users []User - - if err := db.DB.Select(&users, "SELECT userName, admin FROM USERS"); err != nil { - return nil, err - } else { - return users, nil - } -} - -func TokenID(userName string) (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 string `json:"userName" validate:"required" db:"userName"` - Password string `json:"password" validate:"required,min=12,max=64"` - Admin bool `json:"admin" db:"admin"` -} - -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(), - } - - _, err := db.DB.NamedExec("INSERT INTO USERS (userName, password, admin, tokenID) VALUES (:userName, :password, :admin, :tokenID)", insertUser) - - return err - } -} - -func ChangePassword(user UserChangePassword) (string, error) { - // try to hash teh password - if hash, err := hashPassword(user.Password); err != nil { - return "", err - } else { - execStruct := struct { - UserName string `db:"userName"` - Password []byte `db:"password"` - TokenID string `db:"tokenID"` - }{ - UserName: user.UserName, - Password: hash, - TokenID: uuid.NewString(), - } - - if _, err := db.DB.NamedExec("UPDATE USERS SET tokenID = :tokenID, password = :password WHERE name = :userName", execStruct); err != nil { - return "", err - } else { - return execStruct.TokenID, nil - } - } -} - -func ChangeName(userName, newName string) error { - _, err := db.DB.Exec("UPDATE USERS SET userName = ? WHERE userName = ?", newName, userName) - - return err -} - -func SetAdmin(userName string, admin bool) error { - _, err := db.DB.Exec("UPDATE USERS SET admin = ? WHERE userName = ?", admin, userName) - - return err -} - -func Delete(userName string) error { - _, err := db.DB.Exec("DELETE FROM USERS WHERE userName = $1", userName) - - return err -} diff --git a/backend/pkg/router/events.go b/backend/pkg/router/events.go index 39490f8..f0cbe5d 100644 --- a/backend/pkg/router/events.go +++ b/backend/pkg/router/events.go @@ -94,7 +94,7 @@ func (a *Handler) getEventsAvailabilities() { func (a *Handler) getEventUserAssignmentAvailability() { // retrieve the assignments - if events, err := events.WithUserAvailability(a.UserName); err != nil { + if events, err := a.UserName.WithUserAvailability(); err != nil { a.Status = fiber.StatusBadRequest logger.Log().Msgf("getting events with tasks and user-availability failed: %v", err) @@ -104,7 +104,7 @@ func (a *Handler) getEventUserAssignmentAvailability() { } func (a *Handler) getEventsUserPending() { - if events, err := events.UserPending(a.UserName); err != nil { + if events, err := a.UserName.UserPending(); err != nil { a.Status = fiber.StatusInternalServerError logger.Warn().Msgf("can't query database for users %q pending events: %v", a.UserName, err) @@ -114,7 +114,7 @@ func (a *Handler) getEventsUserPending() { } func (a *Handler) getEventsUserPendingCount() { - if count, err := events.UserPendingCount(a.UserName); err != nil { + if count, err := a.UserName.UserPendingCount(); err != nil { a.Status = fiber.StatusInternalServerError logger.Warn().Msgf("can't query database for users %q pending events: %v", a.UserName, err) @@ -125,7 +125,7 @@ func (a *Handler) getEventsUserPendingCount() { func (a *Handler) getEventsUserAssigned() { // retrieve the events from the database - if events, err := events.User(a.UserName); err != nil { + if events, err := a.UserName.GetAssignedEvents(); err != nil { a.Status = fiber.StatusBadRequest logger.Log().Msgf("retrieval of user-assigned-events failed: %v", err) @@ -172,7 +172,7 @@ func (a *Handler) putEventUserAvailability() { } // insert the availability into the database - if err := events.SetUserAvailability(eventID, availabilityID, a.UserName); err != nil { + if err := a.UserName.SetEventAvailability(eventID, availabilityID); err != nil { a.Status = fiber.StatusInternalServerError logger.Error().Msgf("setting user-event-availability failed: can't write availability to database: %v", err) diff --git a/backend/pkg/router/login.go b/backend/pkg/router/login.go index 0b09cd7..640ff76 100644 --- a/backend/pkg/router/login.go +++ b/backend/pkg/router/login.go @@ -3,6 +3,7 @@ package router import ( "github.com/gofiber/fiber/v2" "github.com/johannesbuehl/golunteer/backend/pkg/db" + "github.com/johannesbuehl/golunteer/backend/pkg/db/users" "golang.org/x/crypto/bcrypt" ) @@ -45,8 +46,8 @@ func handleLogin(c *fiber.Ctx) error { // extract username and password from the request requestBody := struct { - Username string `json:"userName" validate:"required"` - Password string `json:"password" validate:"required"` + users.UserName `json:"userName" validate:"required"` + Password string `json:"password" validate:"required"` }{} if err := args.C.BodyParser(&requestBody); err != nil { @@ -60,21 +61,21 @@ func handleLogin(c *fiber.Ctx) error { } else { // query the database for the user var result userDB - if err := db.DB.QueryRowx("SELECT password, admin, tokenID FROM USERS WHERE userName = ?", requestBody.Username).StructScan(&result); err != nil { + if err := db.DB.QueryRowx("SELECT password, admin, tokenID FROM USERS WHERE userName = ?", requestBody.UserName).StructScan(&result); err != nil { args.Status = fiber.StatusForbidden args.Message = messageWrongLogin - logger.Info().Msgf("can't get user with userName = %q from database", requestBody.Username) + logger.Info().Msgf("can't get user with userName = %q from database", requestBody.UserName) } else { // hash the password if bcrypt.CompareHashAndPassword(result.Password, []byte(requestBody.Password)) != nil { args.Status = fiber.StatusForbidden - logger.Info().Msgf("login denied: wrong password for user with userName = %q", requestBody.Username) + logger.Info().Msgf("login denied: wrong password for user with userName = %q", requestBody.UserName) } else { // password is correct -> generate the JWT if jwt, err := config.SignJWT(JWTPayload{ - UserName: requestBody.Username, + UserName: requestBody.UserName, TokenID: result.TokenID, }); err != nil { args.Status = fiber.StatusInternalServerError @@ -83,11 +84,11 @@ func handleLogin(c *fiber.Ctx) error { args.setSessionCookie(&jwt) args.Data = UserChecked{ - UserName: requestBody.Username, + UserName: requestBody.UserName, Admin: true, } - logger.Debug().Msgf("user %q logged in", requestBody.Username) + logger.Debug().Msgf("user %q logged in", requestBody.UserName) } } } diff --git a/backend/pkg/router/router.go b/backend/pkg/router/router.go index 5fbce0d..edcd38c 100644 --- a/backend/pkg/router/router.go +++ b/backend/pkg/router/router.go @@ -9,6 +9,7 @@ import ( "github.com/golang-jwt/jwt/v5" _config "github.com/johannesbuehl/golunteer/backend/pkg/config" "github.com/johannesbuehl/golunteer/backend/pkg/db" + "github.com/johannesbuehl/golunteer/backend/pkg/db/users" _logger "github.com/johannesbuehl/golunteer/backend/pkg/logger" ) @@ -194,8 +195,8 @@ func (args Handler) removeSessionCookie() { // payload of the JSON webtoken type JWTPayload struct { - UserName string `json:"userName"` - TokenID string `json:"tokenID"` + users.UserName `json:"userName"` + TokenID string `json:"tokenID"` } // complete JSON webtoken @@ -207,7 +208,7 @@ type JWT struct { // extracts the json webtoken from the request // // @returns (userName, tokenID, error) -func extractJWT(c *fiber.Ctx) (string, string, error) { +func extractJWT(c *fiber.Ctx) (users.UserName, string, error) { // get the session-cookie cookie := c.Cookies("session") @@ -240,8 +241,8 @@ type userDB struct { } type UserChecked struct { - UserName string `json:"userName" db:"userName"` - Admin bool `json:"admin" db:"admin"` + users.UserName `json:"userName" db:"userName"` + Admin bool `json:"admin" db:"admin"` } // checks wether the request is from a valid user diff --git a/backend/pkg/router/user.go b/backend/pkg/router/user.go index 83c247d..a5a53c5 100644 --- a/backend/pkg/router/user.go +++ b/backend/pkg/router/user.go @@ -69,7 +69,7 @@ func (a *Handler) putPassword() { a.Status = fiber.StatusBadRequest // send the password change to the database and get the new tokenID back - } else if tokenID, err := users.ChangePassword(body); err != nil { + } else if tokenID, err := body.UserName.ChangePassword(body.Password); err != nil { logger.Error().Msgf("can't update password: %v", err) a.Status = fiber.StatusInternalServerError @@ -103,7 +103,7 @@ func (a *Handler) patchUser() { // parse the body var body struct { users.UserAdd - NewName string `json:"newName"` + NewName users.UserName `json:"newName"` } if err := a.C.BodyParser(&body); err != nil { @@ -126,13 +126,7 @@ func (a *Handler) patchUser() { // if the password has length 0 assume the password shouldn't be changed } else { if len(body.Password) > 0 { - // create a password-change-struct and validate it. use the old user-name, since the new isn't stored yet - usePasswordChange := users.UserChangePassword{ - UserName: body.UserName, - Password: body.Password, - } - - if _, err = users.ChangePassword(usePasswordChange); err != nil { + if _, err = body.UserName.ChangePassword(body.Password); err != nil { a.Status = fiber.StatusInternalServerError logger.Error().Msgf("can't change password: %v", err) @@ -143,7 +137,7 @@ func (a *Handler) patchUser() { // only change the name, if it differs if body.NewName != body.UserName { - if err := users.ChangeName(body.UserName, body.NewName); err != nil { + if err := body.UserName.ChangeName(body.NewName); err != nil { a.Status = fiber.StatusInternalServerError logger.Error().Msgf("can't change user-name: %v", err) @@ -153,15 +147,22 @@ func (a *Handler) patchUser() { } // set the admin-status - if err := users.SetAdmin(body.NewName, body.Admin); err != nil { + if err := body.NewName.SetAdmin(body.Admin); err != nil { a.Status = fiber.StatusInternalServerError logger.Error().Msgf("updating admin-status failed: %v", err) + + // update the possible tasks + } else if err := body.NewName.SetTasks(body.PossibleTasks); err != nil { + a.Status = fiber.StatusInternalServerError + + logger.Error().Msgf("updating possible user-tasks failed: %v", err) + } else { // if we modified ourself, update the session-cookie - if body.UserName == a.UserName { + if body.UserName != body.NewName { // get the tokenID - if tokenID, err := users.TokenID(body.NewName); err != nil { + if tokenID, err := body.NewName.TokenID(); err != nil { a.Status = fiber.StatusInternalServerError logger.Error().Msgf("can't get tokenID: %v", err) @@ -200,7 +201,7 @@ func (a *Handler) deleteUser() { a.Status = fiber.StatusBadRequest // check wether the user tries to delete himself - } else if userName == a.UserName { + } else if users.UserName(userName) == a.UserName { logger.Log().Msg("user-deletion failed: self-deletion is illegal") a.Status = fiber.StatusBadRequest diff --git a/backend/setup.sql b/backend/setup.sql index 262c40f..6fbcb0b 100644 --- a/backend/setup.sql +++ b/backend/setup.sql @@ -20,6 +20,14 @@ CREATE TABLE IF NOT EXISTS USERS ( CHECK (length(tokenID) = 36) ); +CREATE TABLE IF NOT EXISTS USER_TASKS ( + userName varchar(64), + taskID INTEGER, + PRIMARY KEY (userName, taskID), + FOREIGN KEY (userName) REFERENCES USERS(userName) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (taskID) REFERENCES TASKS(taskID) ON DELETE CASCADE ON UPDATE CASCADE +); + CREATE TABLE IF NOT EXISTS EVENTS ( eventID INTEGER PRIMARY KEY, date DATETIME NOT NULL, diff --git a/client/src/Zustand.ts b/client/src/Zustand.ts index 31da8fd..c2e2700 100644 --- a/client/src/Zustand.ts +++ b/client/src/Zustand.ts @@ -26,17 +26,21 @@ export interface TaskAssignment { userName: string | null; } -export interface User { +export interface StateUser { userName: string; admin: boolean; } +export type User = StateUser & { + possibleTasks: number[]; +}; + export type UserAddModify = User & { password: string; }; interface Zustand { - user: User | null; + user: StateUser | null; tasks?: Task[]; availabilities?: Availability[]; patch: (zustand?: Partial) => void; diff --git a/client/src/app/Main.tsx b/client/src/app/Main.tsx index d90d7d3..7c4ee16 100644 --- a/client/src/app/Main.tsx +++ b/client/src/app/Main.tsx @@ -1,7 +1,7 @@ "use client"; import { apiCall } from "@/lib"; -import zustand from "@/Zustand"; +import zustand, { StateUser } from "@/Zustand"; import { Spinner } from "@heroui/react"; import { usePathname, useRouter } from "next/navigation"; import React, { useEffect, useState } from "react"; @@ -23,10 +23,7 @@ export default function Main({ children }: { children: React.ReactNode }) { void (async () => { let loggedIn = false; - const welcomeResult = await apiCall<{ - userName: string; - loggedIn: boolean; - }>("GET", "welcome"); + const welcomeResult = await apiCall("GET", "welcome"); if (welcomeResult.ok) { try { diff --git a/client/src/app/admin/(users)/UserEditor.tsx b/client/src/app/admin/(users)/UserEditor.tsx index 118a5c2..d7bda88 100644 --- a/client/src/app/admin/(users)/UserEditor.tsx +++ b/client/src/app/admin/(users)/UserEditor.tsx @@ -1,11 +1,13 @@ import { AllString, classNames, + getTasks, validatePassword as validatePassword, } from "@/lib"; import zustand, { User, UserAddModify } from "@/Zustand"; import { Checkbox, + CheckboxGroup, Form, Input, Modal, @@ -14,6 +16,7 @@ import { ModalFooter, ModalHeader, } from "@heroui/react"; +import { useAsyncList } from "@react-stately/data"; import React, { FormEvent, useState } from "react"; export default function UserEditor(props: { @@ -26,8 +29,19 @@ export default function UserEditor(props: { onSubmit: (user: UserAddModify) => void; }) { const [name, setName] = useState(props.value?.userName ?? ""); - const [admin, setAdmin] = useState(props.value?.admin ?? false); const [password, setPassword] = useState(""); + const [admin, setAdmin] = useState(props.value?.admin ?? false); + const [possibleTasks, setPossibleTasks] = useState( + props.value?.possibleTasks.map((t) => t.toString()) ?? [], + ); + + const tasks = useAsyncList({ + async load() { + return { + items: await getTasks(), + }; + }, + }); const pwErrors = validatePassword(password); @@ -39,6 +53,7 @@ export default function UserEditor(props: { const data = { ...formData, + possibleTasks: possibleTasks.map((t) => parseInt(t)), admin: formData.admin !== undefined, }; @@ -120,6 +135,17 @@ export default function UserEditor(props: { > Admin + + {tasks.items.map((task) => ( + + {task.taskName} + + ))} + {props.footer} diff --git a/client/src/app/login/page.tsx b/client/src/app/login/page.tsx index 641ca93..193eab4 100644 --- a/client/src/app/login/page.tsx +++ b/client/src/app/login/page.tsx @@ -2,7 +2,7 @@ import CheckboxIcon from "@/components/CheckboxIcon"; import { apiCall } from "@/lib"; -import zustand from "@/Zustand"; +import zustand, { StateUser } from "@/Zustand"; import { ViewFilled, ViewOffFilled, @@ -21,7 +21,7 @@ export default function Login() { async function sendLogin(e: FormEvent) { const data = Object.fromEntries(new FormData(e.currentTarget)); - const result = await apiCall("POST", "login", undefined, data); + const result = await apiCall("POST", "login", undefined, data); if (result.ok) { // add the user-info to the zustand