From 21cc7ff8a393769cfbc81dc07fe822f64482cb07 Mon Sep 17 00:00:00 2001 From: dDogge <> Date: Sun, 17 Mar 2024 15:34:48 +0100 Subject: [PATCH 01/11] Added signed_by attribute to weekly_report SQL table --- backend/internal/database/migrations/0035_weekly_report.sql | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/internal/database/migrations/0035_weekly_report.sql b/backend/internal/database/migrations/0035_weekly_report.sql index 0e29b97..47610b5 100644 --- a/backend/internal/database/migrations/0035_weekly_report.sql +++ b/backend/internal/database/migrations/0035_weekly_report.sql @@ -8,7 +8,9 @@ CREATE TABLE weekly_reports ( own_work_time INTEGER, study_time INTEGER, testing_time INTEGER, + signed_by INTEGER, FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (project_id) REFERENCES projects(id) + FOREIGN KEY (project_id) REFERENCES projects(id), + FOREIGN KEY (signed_by) REFERENCES users(id), PRIMARY KEY (user_id, project_id, week) ) \ No newline at end of file From d57fe55074b8f13aee30fca262b76bae1c75b51e Mon Sep 17 00:00:00 2001 From: al8763be Date: Sun, 17 Mar 2024 16:11:03 +0100 Subject: [PATCH 02/11] Changes to SubmitWeeklyReport --- frontend/src/API/API.ts | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index bd6518b..d87594c 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -167,32 +167,29 @@ export const api: API = { token: string, ): Promise> { try { - return fetch("/api/submitWeeklyReport", { + const response = await fetch("/api/submitWeeklyReport", { method: "POST", headers: { "Content-Type": "application/json", Authorization: "Bearer " + token, }, body: JSON.stringify(project), - }) - .then((response) => { - if (!response.ok) { - return { - success: false, - message: "Failed to submit weekly report", - }; - } else { - return response.json(); - } - }) - .then((data: Project) => { - return { success: true, data }; - }); + }); + + if (!response.ok) { + return { + success: false, + message: "Failed to submit weekly report", + }; + } + + const data = (await response.json()) as Project; + return { success: true, data }; } catch (e) { - return Promise.resolve({ + return { success: false, message: "Failed to submit weekly report", - }); + }; } }, From 670ed46d5188d72eb6f67695e342d4542e9a9c72 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Sun, 17 Mar 2024 16:41:29 +0100 Subject: [PATCH 03/11] WeeklyReport type as represented in the db --- backend/internal/types/WeeklyReport.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/backend/internal/types/WeeklyReport.go b/backend/internal/types/WeeklyReport.go index e0ea1ef..43c19c6 100644 --- a/backend/internal/types/WeeklyReport.go +++ b/backend/internal/types/WeeklyReport.go @@ -19,3 +19,26 @@ type NewWeeklyReport struct { // Total time spent on testing TestingTime int `json:"testingTime"` } + +type WeeklyReport struct { + // The ID of the report + ReportId int `json:"reportId" db:"report_id"` + // The user id of the user who submitted the report + UserId int `json:"userId" db:"user_id"` + // The name of the project, as it appears in the database + ProjectName string `json:"projectName"` + // The week number + Week int `json:"week"` + // Total time spent on development + DevelopmentTime int `json:"developmentTime"` + // Total time spent in meetings + MeetingTime int `json:"meetingTime"` + // Total time spent on administrative tasks + AdminTime int `json:"adminTime"` + // Total time spent on personal projects + OwnWorkTime int `json:"ownWorkTime"` + // Total time spent on studying + StudyTime int `json:"studyTime"` + // Total time spent on testing + TestingTime int `json:"testingTime"` +} From d6fc0594a9e24c08b6f2c750c64ba4dcd60be294 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Sun, 17 Mar 2024 16:55:40 +0100 Subject: [PATCH 04/11] Splitting backend endpoints into smaller bits --- backend/internal/handlers/global_state.go | 226 ------------------ .../handlers/handlers_project_related.go | 98 ++++++++ .../handlers/handlers_report_related.go | 34 +++ .../handlers/handlers_user_related.go | 116 +++++++++ 4 files changed, 248 insertions(+), 226 deletions(-) create mode 100644 backend/internal/handlers/handlers_project_related.go create mode 100644 backend/internal/handlers/handlers_report_related.go create mode 100644 backend/internal/handlers/handlers_user_related.go diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index 2378f7b..01c8999 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -1,13 +1,9 @@ package handlers import ( - "strconv" - "time" "ttime/internal/database" - "ttime/internal/types" "github.com/gofiber/fiber/v2" - "github.com/golang-jwt/jwt/v5" ) // The actual interface that we will use @@ -51,50 +47,6 @@ type GState struct { ButtonCount int } -// Register is a simple handler that registers a new user -// -// @Summary Register a new user -// @Description Register a new user -// @Tags User -// @Accept json -// @Produce json -// @Success 200 {string} string "User added" -// @Failure 400 {string} string "Bad request" -// @Failure 500 {string} string "Internal server error" -// @Router /api/register [post] -func (gs *GState) Register(c *fiber.Ctx) error { - u := new(types.NewUser) - if err := c.BodyParser(u); err != nil { - return c.Status(400).SendString(err.Error()) - } - - if err := gs.Db.AddUser(u.Username, u.Password); err != nil { - return c.Status(500).SendString(err.Error()) - } - - return c.Status(200).SendString("User added") -} - -// This path should obviously be protected in the future -// UserDelete deletes a user from the database -func (gs *GState) UserDelete(c *fiber.Ctx) error { - // Read from path parameters - username := c.Params("username") - - // Read username from Locals - auth_username := c.Locals("user").(*jwt.Token).Claims.(jwt.MapClaims)["name"].(string) - - if username != auth_username { - return c.Status(403).SendString("You can only delete yourself") - } - - if err := gs.Db.RemoveUser(username); err != nil { - return c.Status(500).SendString(err.Error()) - } - - return c.Status(200).SendString("User deleted") -} - func (gs *GState) GetButtonCount(c *fiber.Ctx) error { return c.Status(200).JSON(fiber.Map{"pressCount": gs.ButtonCount}) } @@ -103,181 +55,3 @@ func (gs *GState) IncrementButtonCount(c *fiber.Ctx) error { gs.ButtonCount++ return c.Status(200).JSON(fiber.Map{"pressCount": gs.ButtonCount}) } - -// Login is a simple login handler that returns a JWT token -func (gs *GState) Login(c *fiber.Ctx) error { - // The body type is identical to a NewUser - u := new(types.NewUser) - if err := c.BodyParser(u); err != nil { - return c.Status(400).SendString(err.Error()) - } - - if !gs.Db.CheckUser(u.Username, u.Password) { - println("User not found") - return c.SendStatus(fiber.StatusUnauthorized) - } - - // Create the Claims - claims := jwt.MapClaims{ - "name": u.Username, - "admin": false, - "exp": time.Now().Add(time.Hour * 72).Unix(), - } - - // Create token - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - - // Generate encoded token and send it as response. - t, err := token.SignedString([]byte("secret")) - if err != nil { - return c.SendStatus(fiber.StatusInternalServerError) - } - - return c.JSON(fiber.Map{"token": t}) -} - -// LoginRenew is a simple handler that renews the token -func (gs *GState) LoginRenew(c *fiber.Ctx) error { - // For testing: curl localhost:3000/restricted -H "Authorization: Bearer " - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - claims["exp"] = time.Now().Add(time.Hour * 72).Unix() - renewed := jwt.MapClaims{ - "name": claims["name"], - "admin": claims["admin"], - "exp": claims["exp"], - } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, renewed) - t, err := token.SignedString([]byte("secret")) - if err != nil { - return c.SendStatus(fiber.StatusInternalServerError) - } - return c.JSON(fiber.Map{"token": t}) -} - -// CreateProject is a simple handler that creates a new project -func (gs *GState) CreateProject(c *fiber.Ctx) error { - user := c.Locals("user").(*jwt.Token) - - p := new(types.NewProject) - if err := c.BodyParser(p); err != nil { - return c.Status(400).SendString(err.Error()) - } - - // Get the username from the token and set it as the owner of the project - // This is ugly but - claims := user.Claims.(jwt.MapClaims) - owner := claims["name"].(string) - - if err := gs.Db.AddProject(p.Name, p.Description, owner); err != nil { - return c.Status(500).SendString(err.Error()) - } - - return c.Status(200).SendString("Project added") -} - -// GetUserProjects returns all projects that the user is a member of -func (gs *GState) GetUserProjects(c *fiber.Ctx) error { - // First we get the username from the token - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - username := claims["name"].(string) - - // Then dip into the database to get the projects - projects, err := gs.Db.GetProjectsForUser(username) - if err != nil { - return c.Status(500).SendString(err.Error()) - } - - // Return a json serialized list of projects - return c.JSON(projects) -} - -// ListAllUsers is a handler that returns a list of all users in the application database -func (gs *GState) ListAllUsers(c *fiber.Ctx) error { - // Get all users from the database - users, err := gs.Db.GetAllUsersApplication() - if err != nil { - return c.Status(500).SendString(err.Error()) - } - - // Return the list of users as JSON - return c.JSON(users) -} - -func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error { - // Extract the project name from the request parameters or body - projectName := c.Params("projectName") - - // Get all users associated with the project from the database - users, err := gs.Db.GetAllUsersProject(projectName) - if err != nil { - return c.Status(500).SendString(err.Error()) - } - - // Return the list of users as JSON - return c.JSON(users) -} - -// ProjectRoleChange is a handler that changes a user's role within a project -func (gs *GState) ProjectRoleChange(c *fiber.Ctx) error { - // Extract the necessary parameters from the request - username := c.Params("username") - projectName := c.Params("projectName") - role := c.Params("role") - - // Change the user's role within the project in the database - if err := gs.Db.ChangeUserRole(username, projectName, role); err != nil { - return c.Status(500).SendString(err.Error()) - } - - // Return a success message - return c.SendStatus(fiber.StatusOK) -} - -// GetProject retrieves a specific project by its ID -func (gs *GState) GetProject(c *fiber.Ctx) error { - // Extract the project ID from the request parameters or body - projectID := c.Params("projectID") - - // Parse the project ID into an integer - projectIDInt, err := strconv.Atoi(projectID) - if err != nil { - return c.Status(400).SendString("Invalid project ID") - } - - // Get the project from the database by its ID - project, err := gs.Db.GetProject(projectIDInt) - if err != nil { - return c.Status(500).SendString(err.Error()) - } - - // Return the project as JSON - return c.JSON(project) -} - -func (gs *GState) SubmitWeeklyReport(c *fiber.Ctx) error { - // Extract the necessary parameters from the token - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - username := claims["name"].(string) - - report := new(types.NewWeeklyReport) - if err := c.BodyParser(report); err != nil { - return c.Status(400).SendString(err.Error()) - } - - // Make sure all the fields of the report are valid - if report.Week < 1 || report.Week > 52 { - return c.Status(400).SendString("Invalid week number") - } - if report.DevelopmentTime < 0 || report.MeetingTime < 0 || report.AdminTime < 0 || report.OwnWorkTime < 0 || report.StudyTime < 0 || report.TestingTime < 0 { - return c.Status(400).SendString("Invalid time report") - } - - if err := gs.Db.AddWeeklyReport(report.ProjectName, username, report.Week, report.DevelopmentTime, report.MeetingTime, report.AdminTime, report.OwnWorkTime, report.StudyTime, report.TestingTime); err != nil { - return c.Status(500).SendString(err.Error()) - } - - return c.Status(200).SendString("Time report added") -} diff --git a/backend/internal/handlers/handlers_project_related.go b/backend/internal/handlers/handlers_project_related.go new file mode 100644 index 0000000..6a430e9 --- /dev/null +++ b/backend/internal/handlers/handlers_project_related.go @@ -0,0 +1,98 @@ +package handlers + +import ( + "strconv" + "ttime/internal/types" + + "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v5" +) + +// CreateProject is a simple handler that creates a new project +func (gs *GState) CreateProject(c *fiber.Ctx) error { + user := c.Locals("user").(*jwt.Token) + + p := new(types.NewProject) + if err := c.BodyParser(p); err != nil { + return c.Status(400).SendString(err.Error()) + } + + // Get the username from the token and set it as the owner of the project + // This is ugly but + claims := user.Claims.(jwt.MapClaims) + owner := claims["name"].(string) + + if err := gs.Db.AddProject(p.Name, p.Description, owner); err != nil { + return c.Status(500).SendString(err.Error()) + } + + return c.Status(200).SendString("Project added") +} + +// GetUserProjects returns all projects that the user is a member of +func (gs *GState) GetUserProjects(c *fiber.Ctx) error { + // First we get the username from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Then dip into the database to get the projects + projects, err := gs.Db.GetProjectsForUser(username) + if err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return a json serialized list of projects + return c.JSON(projects) +} + +// ProjectRoleChange is a handler that changes a user's role within a project +func (gs *GState) ProjectRoleChange(c *fiber.Ctx) error { + // Extract the necessary parameters from the request + username := c.Params("username") + projectName := c.Params("projectName") + role := c.Params("role") + + // Change the user's role within the project in the database + if err := gs.Db.ChangeUserRole(username, projectName, role); err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return a success message + return c.SendStatus(fiber.StatusOK) +} + +// GetProject retrieves a specific project by its ID +func (gs *GState) GetProject(c *fiber.Ctx) error { + // Extract the project ID from the request parameters or body + projectID := c.Params("projectID") + + // Parse the project ID into an integer + projectIDInt, err := strconv.Atoi(projectID) + if err != nil { + return c.Status(400).SendString("Invalid project ID") + } + + // Get the project from the database by its ID + project, err := gs.Db.GetProject(projectIDInt) + if err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return the project as JSON + return c.JSON(project) +} + +func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error { + // Extract the project name from the request parameters or body + projectName := c.Params("projectName") + + // Get all users associated with the project from the database + users, err := gs.Db.GetAllUsersProject(projectName) + if err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return the list of users as JSON + return c.JSON(users) +} diff --git a/backend/internal/handlers/handlers_report_related.go b/backend/internal/handlers/handlers_report_related.go new file mode 100644 index 0000000..2486091 --- /dev/null +++ b/backend/internal/handlers/handlers_report_related.go @@ -0,0 +1,34 @@ +package handlers + +import ( + "ttime/internal/types" + + "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v5" +) + +func (gs *GState) SubmitWeeklyReport(c *fiber.Ctx) error { + // Extract the necessary parameters from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + report := new(types.NewWeeklyReport) + if err := c.BodyParser(report); err != nil { + return c.Status(400).SendString(err.Error()) + } + + // Make sure all the fields of the report are valid + if report.Week < 1 || report.Week > 52 { + return c.Status(400).SendString("Invalid week number") + } + if report.DevelopmentTime < 0 || report.MeetingTime < 0 || report.AdminTime < 0 || report.OwnWorkTime < 0 || report.StudyTime < 0 || report.TestingTime < 0 { + return c.Status(400).SendString("Invalid time report") + } + + if err := gs.Db.AddWeeklyReport(report.ProjectName, username, report.Week, report.DevelopmentTime, report.MeetingTime, report.AdminTime, report.OwnWorkTime, report.StudyTime, report.TestingTime); err != nil { + return c.Status(500).SendString(err.Error()) + } + + return c.Status(200).SendString("Time report added") +} diff --git a/backend/internal/handlers/handlers_user_related.go b/backend/internal/handlers/handlers_user_related.go new file mode 100644 index 0000000..e90abd0 --- /dev/null +++ b/backend/internal/handlers/handlers_user_related.go @@ -0,0 +1,116 @@ +package handlers + +import ( + "time" + "ttime/internal/types" + + "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v5" +) + +// Register is a simple handler that registers a new user +// +// @Summary Register a new user +// @Description Register a new user +// @Tags User +// @Accept json +// @Produce json +// @Success 200 {string} string "User added" +// @Failure 400 {string} string "Bad request" +// @Failure 500 {string} string "Internal server error" +// @Router /api/register [post] +func (gs *GState) Register(c *fiber.Ctx) error { + u := new(types.NewUser) + if err := c.BodyParser(u); err != nil { + return c.Status(400).SendString(err.Error()) + } + + if err := gs.Db.AddUser(u.Username, u.Password); err != nil { + return c.Status(500).SendString(err.Error()) + } + + return c.Status(200).SendString("User added") +} + +// This path should obviously be protected in the future +// UserDelete deletes a user from the database +func (gs *GState) UserDelete(c *fiber.Ctx) error { + // Read from path parameters + username := c.Params("username") + + // Read username from Locals + auth_username := c.Locals("user").(*jwt.Token).Claims.(jwt.MapClaims)["name"].(string) + + if username != auth_username { + return c.Status(403).SendString("You can only delete yourself") + } + + if err := gs.Db.RemoveUser(username); err != nil { + return c.Status(500).SendString(err.Error()) + } + + return c.Status(200).SendString("User deleted") +} + +// Login is a simple login handler that returns a JWT token +func (gs *GState) Login(c *fiber.Ctx) error { + // The body type is identical to a NewUser + u := new(types.NewUser) + if err := c.BodyParser(u); err != nil { + return c.Status(400).SendString(err.Error()) + } + + if !gs.Db.CheckUser(u.Username, u.Password) { + println("User not found") + return c.SendStatus(fiber.StatusUnauthorized) + } + + // Create the Claims + claims := jwt.MapClaims{ + "name": u.Username, + "admin": false, + "exp": time.Now().Add(time.Hour * 72).Unix(), + } + + // Create token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + // Generate encoded token and send it as response. + t, err := token.SignedString([]byte("secret")) + if err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + + return c.JSON(fiber.Map{"token": t}) +} + +// LoginRenew is a simple handler that renews the token +func (gs *GState) LoginRenew(c *fiber.Ctx) error { + // For testing: curl localhost:3000/restricted -H "Authorization: Bearer " + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + claims["exp"] = time.Now().Add(time.Hour * 72).Unix() + renewed := jwt.MapClaims{ + "name": claims["name"], + "admin": claims["admin"], + "exp": claims["exp"], + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, renewed) + t, err := token.SignedString([]byte("secret")) + if err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + return c.JSON(fiber.Map{"token": t}) +} + +// ListAllUsers is a handler that returns a list of all users in the application database +func (gs *GState) ListAllUsers(c *fiber.Ctx) error { + // Get all users from the database + users, err := gs.Db.GetAllUsersApplication() + if err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return the list of users as JSON + return c.JSON(users) +} From c90d4956363f37bcb1a9d2abebba1486dd0a823d Mon Sep 17 00:00:00 2001 From: dDogge <> Date: Sun, 17 Mar 2024 17:47:31 +0100 Subject: [PATCH 05/11] Added new types and changed SQL since apperently sqlite does use autoincrement --- .../internal/database/migrations/0010_users.sql | 2 +- .../database/migrations/0020_projects.sql | 2 +- .../database/migrations/0035_weekly_report.sql | 12 ++++++------ backend/internal/types/WeeklyReport.go | 16 ++++++++-------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/backend/internal/database/migrations/0010_users.sql b/backend/internal/database/migrations/0010_users.sql index 5c9d329..d2e2dd1 100644 --- a/backend/internal/database/migrations/0010_users.sql +++ b/backend/internal/database/migrations/0010_users.sql @@ -3,7 +3,7 @@ -- username is what is used for login -- password is the hashed password CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY, + id INTEGER PRIMARY KEY AUTOINCREMENT, userId TEXT DEFAULT (HEX(RANDOMBLOB(4))) NOT NULL UNIQUE, username VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL diff --git a/backend/internal/database/migrations/0020_projects.sql b/backend/internal/database/migrations/0020_projects.sql index 58d8e97..99bce86 100644 --- a/backend/internal/database/migrations/0020_projects.sql +++ b/backend/internal/database/migrations/0020_projects.sql @@ -1,5 +1,5 @@ CREATE TABLE IF NOT EXISTS projects ( - id INTEGER PRIMARY KEY, + id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(255) NOT NULL UNIQUE, description TEXT NOT NULL, owner_user_id INTEGER NOT NULL, diff --git a/backend/internal/database/migrations/0035_weekly_report.sql b/backend/internal/database/migrations/0035_weekly_report.sql index 47610b5..366d932 100644 --- a/backend/internal/database/migrations/0035_weekly_report.sql +++ b/backend/internal/database/migrations/0035_weekly_report.sql @@ -1,7 +1,8 @@ CREATE TABLE weekly_reports ( - user_id INTEGER, - project_id INTEGER, - week INTEGER, + report_id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + project_id INTEGER NOT NULL, + week INTEGER NOT NULL, development_time INTEGER, meeting_time INTEGER, admin_time INTEGER, @@ -11,6 +12,5 @@ CREATE TABLE weekly_reports ( signed_by INTEGER, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (project_id) REFERENCES projects(id), - FOREIGN KEY (signed_by) REFERENCES users(id), - PRIMARY KEY (user_id, project_id, week) -) \ No newline at end of file + FOREIGN KEY (signed_by) REFERENCES users(id) +); \ No newline at end of file diff --git a/backend/internal/types/WeeklyReport.go b/backend/internal/types/WeeklyReport.go index 43c19c6..a9a6264 100644 --- a/backend/internal/types/WeeklyReport.go +++ b/backend/internal/types/WeeklyReport.go @@ -26,19 +26,19 @@ type WeeklyReport struct { // The user id of the user who submitted the report UserId int `json:"userId" db:"user_id"` // The name of the project, as it appears in the database - ProjectName string `json:"projectName"` + ProjectId string `json:"projectId" db:"project_id"` // The week number - Week int `json:"week"` + Week int `json:"week" db:"week"` // Total time spent on development - DevelopmentTime int `json:"developmentTime"` + DevelopmentTime int `json:"developmentTime" db:"development_time"` // Total time spent in meetings - MeetingTime int `json:"meetingTime"` + MeetingTime int `json:"meetingTime" db:"meeting_time"` // Total time spent on administrative tasks - AdminTime int `json:"adminTime"` + AdminTime int `json:"adminTime" db:"admin_time"` // Total time spent on personal projects - OwnWorkTime int `json:"ownWorkTime"` + OwnWorkTime int `json:"ownWorkTime" db:"own_work_time"` // Total time spent on studying - StudyTime int `json:"studyTime"` + StudyTime int `json:"studyTime" db:"study_time"` // Total time spent on testing - TestingTime int `json:"testingTime"` + TestingTime int `json:"testingTime" db:"testing_time"` } From a77e57e496a72f2dd9e77cfdc5aa7fb00d442ae9 Mon Sep 17 00:00:00 2001 From: dDogge <> Date: Sun, 17 Mar 2024 17:58:02 +0100 Subject: [PATCH 06/11] Added GetWeeklyReport function and corresponding test --- backend/internal/database/db.go | 26 +++++++++++++++++ backend/internal/database/db_test.go | 39 ++++++++++++++++++++++++++ backend/internal/types/WeeklyReport.go | 2 +- 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 320327a..7f4a89c 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -29,6 +29,7 @@ type Database interface { GetAllProjects() ([]types.Project, error) GetProject(projectId int) (types.Project, error) GetUserRole(username string, projectname string) (string, error) + GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error) } // This struct is a wrapper type that holds the database connection @@ -256,6 +257,31 @@ func (d *Db) GetAllUsersApplication() ([]string, error) { return usernames, nil } +func (d *Db) GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error) { + var report types.WeeklyReport + query := ` + SELECT + report_id, + user_id, + project_id, + week, + development_time, + meeting_time, + admin_time, + own_work_time, + study_time, + testing_time + FROM + weekly_reports + WHERE + user_id = (SELECT id FROM users WHERE username = ?) + AND project_id = (SELECT id FROM projects WHERE name = ?) + AND week = ? + ` + err := d.Get(&report, query, username, projectName, week) + return report, err +} + // Reads a directory of migration files and applies them to the database. // This will eventually be used on an embedded directory func (d *Db) Migrate() error { diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index 9124c45..f791066 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -371,3 +371,42 @@ func TestAddProject(t *testing.T) { t.Error("Added project not found") } } + +func TestGetWeeklyReport(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + err = db.AddUser("testuser", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + err = db.AddProject("testproject", "description", "testuser") + if err != nil { + t.Error("AddProject failed:", err) + } + + err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1) + if err != nil { + t.Error("AddWeeklyReport failed:", err) + } + + report, err := db.GetWeeklyReport("testuser", "testproject", 1) + if err != nil { + t.Error("GetWeeklyReport failed:", err) + } + + // Check if the retrieved report matches the expected values + if report.UserId != 1 { + t.Errorf("Expected UserId to be 1, got %d", report.UserId) + } + if report.ProjectId != 1 { + t.Errorf("Expected ProjectId to be 1, got %d", report.ProjectId) + } + if report.Week != 1 { + t.Errorf("Expected Week to be 1, got %d", report.Week) + } + // Check other fields similarly +} diff --git a/backend/internal/types/WeeklyReport.go b/backend/internal/types/WeeklyReport.go index a9a6264..b704cc8 100644 --- a/backend/internal/types/WeeklyReport.go +++ b/backend/internal/types/WeeklyReport.go @@ -26,7 +26,7 @@ type WeeklyReport struct { // The user id of the user who submitted the report UserId int `json:"userId" db:"user_id"` // The name of the project, as it appears in the database - ProjectId string `json:"projectId" db:"project_id"` + ProjectId int `json:"projectId" db:"project_id"` // The week number Week int `json:"week" db:"week"` // Total time spent on development From 3e00a532cf54b47602dd887eb3d971b7de2ce8c6 Mon Sep 17 00:00:00 2001 From: dDogge <> Date: Sun, 17 Mar 2024 18:05:54 +0100 Subject: [PATCH 07/11] Added handler for GetWeeklyReport --- backend/internal/handlers/global_state.go | 1 + .../handlers/handlers_report_related.go | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index 01c8999..57a1969 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -15,6 +15,7 @@ type GlobalState interface { CreateProject(c *fiber.Ctx) error // To create a new project GetUserProjects(c *fiber.Ctx) error // To get all projects SubmitWeeklyReport(c *fiber.Ctx) error + GetWeeklyReport(c *fiber.Ctx) error // GetProject(c *fiber.Ctx) error // To get a specific project // UpdateProject(c *fiber.Ctx) error // To update a project // DeleteProject(c *fiber.Ctx) error // To delete a project diff --git a/backend/internal/handlers/handlers_report_related.go b/backend/internal/handlers/handlers_report_related.go index 2486091..8754afd 100644 --- a/backend/internal/handlers/handlers_report_related.go +++ b/backend/internal/handlers/handlers_report_related.go @@ -1,6 +1,7 @@ package handlers import ( + "strconv" "ttime/internal/types" "github.com/gofiber/fiber/v2" @@ -32,3 +33,30 @@ func (gs *GState) SubmitWeeklyReport(c *fiber.Ctx) error { return c.Status(200).SendString("Time report added") } + +// Handler for retrieving weekly report +func (gs *GState) GetWeeklyReport(c *fiber.Ctx) error { + // Extract the necessary parameters from the request + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Extract project name and week from query parameters + projectName := c.Query("projectName") + week := c.Query("week") + + // Convert week to integer + weekInt, err := strconv.Atoi(week) + if err != nil { + return c.Status(400).SendString("Invalid week number") + } + + // Call the database function to get the weekly report + report, err := gs.Db.GetWeeklyReport(username, projectName, weekInt) + if err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return the retrieved weekly report + return c.JSON(report) +} From 14668ea675942a0bdf81acd290885a2f70a3bded Mon Sep 17 00:00:00 2001 From: al8763be Date: Sun, 17 Mar 2024 19:05:39 +0100 Subject: [PATCH 08/11] Test file for API --- frontend/src/API/API.test.ts | 88 ++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 frontend/src/API/API.test.ts diff --git a/frontend/src/API/API.test.ts b/frontend/src/API/API.test.ts new file mode 100644 index 0000000..e0a93f6 --- /dev/null +++ b/frontend/src/API/API.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, test } from "@jest/globals"; +import { api } from "../API/API"; +import { NewUser, NewWeeklyReport } from "../Types/goTypes"; + +describe("API", () => { + test("registerUser", async () => { + const user: NewUser = { + username: "lol", // Add the username property + password: "lol", + }; + + const response = await api.registerUser(user); + + expect(response.success).toBe(true); + expect(response.data).toHaveProperty("userId"); + }); + + test("createProject", async () => { + const project = { + name: "Project X", + description: "This is a test project", + }; + const token = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6ZmFsc2UsImV4cCI6MTcxMDk0MDIwMywibmFtZSI6InJyZ3VtZHpwbWMifQ.V9NHoYMYV61t"; + + const response = await api.createProject(project, token); + + expect(response.success).toBe(true); + expect(response.data).toHaveProperty("projectId"); + }); + + test("renewToken", async () => { + const refreshToken = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6ZmFsc2UsImV4cCI6MTcxMDk0MDIwMywibmFtZSI6InJyZ3VtZHpwbWMifQ.V9NHoYMYV61t"; + + const response = await api.renewToken(refreshToken); + + expect(response.success).toBe(true); + expect(response.data).toHaveProperty("accessToken"); + expect(response.data).toHaveProperty("refreshToken"); + }); + + test("getUserProjects", async () => { + const token = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6ZmFsc2UsImV4cCI6MTcxMDk0MDIwMywibmFtZSI6InJyZ3VtZHpwbWMifQ.V9NHoYMYV61t"; + const username = "rrgumdzpmc"; + const response = await api.getUserProjects(username, token); + + expect(response.success).toBe(true); + expect(response.data).toHaveProperty("projects"); + }); + + test("submitWeeklyReport", async () => { + const report: NewWeeklyReport = { + projectName: "vtmosxssst", + week: 2, + developmentTime: 40, + meetingTime: 5, + adminTime: 2, + ownWorkTime: 10, + studyTime: 12, + testingTime: 41, + }; + const token = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6ZmFsc2UsImV4cCI6MTcxMDk0MDIwMywibmFtZSI6InJyZ3VtZHpwbWMifQ.V9NHoYMYV61t"; + + const response = await api.submitWeeklyReport(report, token); + + expect(response.success).toBe(true); + expect(response.data).toHaveProperty( + "message", + "Report submitted successfully", + ); + }); + + test("login", async () => { + const user: NewUser = { + username: "rrgumdzpmc", // Add an empty string value for the username property + password: "always_same", + }; + + const response = await api.login(user); + + expect(response.success).toBe(true); + expect(response.data).toHaveProperty("accessToken"); + expect(response.data).toHaveProperty("refreshToken"); + }); +}); From 30cf0b606589c8eac370984d964783a1a2ad604f Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Sun, 17 Mar 2024 19:07:49 +0100 Subject: [PATCH 09/11] Making linter bro happy --- frontend/src/Components/LoginCheck.tsx | 14 ++--- frontend/src/Components/ProjectListUser.tsx | 2 +- frontend/src/Components/Register.tsx | 4 +- frontend/src/Components/TimeReport.tsx | 52 ++++++++++--------- frontend/src/Components/UserListAdmin.tsx | 8 +-- .../Pages/UserPages/UserNewTimeReportPage.tsx | 2 +- 6 files changed, 43 insertions(+), 39 deletions(-) diff --git a/frontend/src/Components/LoginCheck.tsx b/frontend/src/Components/LoginCheck.tsx index ccf761d..af45d0f 100644 --- a/frontend/src/Components/LoginCheck.tsx +++ b/frontend/src/Components/LoginCheck.tsx @@ -1,30 +1,30 @@ -import { NewUser } from "../Types/Users"; +import { NewUser } from "../Types/goTypes"; function LoginCheck(props: { username: string; password: string }): number { //Example users for testing without backend, remove when using backend const admin: NewUser = { - userName: "admin", + username: "admin", password: "123", }; const pmanager: NewUser = { - userName: "pmanager", + username: "pmanager", password: "123", }; const user: NewUser = { - userName: "user", + username: "user", password: "123", }; //TODO: Compare with db instead when finished - if (props.username === admin.userName && props.password === admin.password) { + if (props.username === admin.username && props.password === admin.password) { return 1; } else if ( - props.username === pmanager.userName && + props.username === pmanager.username && props.password === pmanager.password ) { return 2; } else if ( - props.username === user.userName && + props.username === user.username && props.password === user.password ) { return 3; diff --git a/frontend/src/Components/ProjectListUser.tsx b/frontend/src/Components/ProjectListUser.tsx index 0502159..96eeaff 100644 --- a/frontend/src/Components/ProjectListUser.tsx +++ b/frontend/src/Components/ProjectListUser.tsx @@ -1,5 +1,5 @@ import { Link } from "react-router-dom"; -import { Project } from "../Types/Project"; +import { Project } from "../Types/goTypes"; /** * The props for the ProjectsProps component diff --git a/frontend/src/Components/Register.tsx b/frontend/src/Components/Register.tsx index 0c0fcd0..f3d773e 100644 --- a/frontend/src/Components/Register.tsx +++ b/frontend/src/Components/Register.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { NewUser } from "../Types/Users"; +import { NewUser } from "../Types/goTypes"; import { api } from "../API/API"; import Logo from "../assets/Logo.svg"; import Button from "./Button"; @@ -35,7 +35,7 @@ export default function Register(): JSX.Element { const [password, setPassword] = useState(""); const handleRegister = async (): Promise => { - const newUser: NewUser = { userName: username, password }; + const newUser: NewUser = { username: username, password }; await api.registerUser(newUser); // TODO: Handle errors }; diff --git a/frontend/src/Components/TimeReport.tsx b/frontend/src/Components/TimeReport.tsx index cb33ad9..e7eb5b7 100644 --- a/frontend/src/Components/TimeReport.tsx +++ b/frontend/src/Components/TimeReport.tsx @@ -1,40 +1,44 @@ import { useState } from "react"; -import { TimeReport } from "../Types/TimeReport"; import { api } from "../API/API"; import { useNavigate } from "react-router-dom"; import Button from "./Button"; +import { NewWeeklyReport } from "../Types/goTypes"; export default function NewTimeReport(): JSX.Element { - const [week, setWeek] = useState(""); - const [development, setDevelopment] = useState("0"); - const [meeting, setMeeting] = useState("0"); - const [administration, setAdministration] = useState("0"); - const [ownwork, setOwnWork] = useState("0"); - const [studies, setStudies] = useState("0"); - const [testing, setTesting] = useState("0"); + const [projectName, setProjectName] = useState("projectName"); // TODO: Get from backend + const [week, setWeek] = useState(NaN); + const [development, setDevelopment] = useState(NaN); + const [meeting, setMeeting] = useState(NaN); + const [administration, setAdministration] = useState(NaN); + const [ownwork, setOwnWork] = useState(NaN); + const [studies, setStudies] = useState(NaN); + const [testing, setTesting] = useState(NaN); const handleNewTimeReport = async (): Promise => { - const newTimeReport: TimeReport = { + const newTimeReport: NewWeeklyReport = { + projectName, week, - development, - meeting, - administration, - ownwork, - studies, - testing, + developmentTime: development, + meetingTime: meeting, + adminTime: administration, + ownWorkTime: ownwork, + studyTime: studies, + testingTime: testing, }; await Promise.resolve(); - // await api.registerTimeReport(newTimeReport); This needs to be implemented! + await api.submitWeeklyReport(newTimeReport, "token"); }; const navigate = useNavigate(); + setProjectName("Something Reasonable"); // This should obviously not be used here + return ( <>
{ - if (week === "") { + if (!week) { alert("Please enter a week number"); e.preventDefault(); return; @@ -50,7 +54,7 @@ export default function NewTimeReport(): JSX.Element { type="week" placeholder="Week" onChange={(e) => { - const weekNumber = e.target.value.split("-W")[1]; + const weekNumber = parseInt(e.target.value.split("-W")[1]); setWeek(weekNumber); }} onKeyDown={(event) => { @@ -81,7 +85,7 @@ export default function NewTimeReport(): JSX.Element { className="border-2 border-black rounded-md text-center w-1/2" value={development} onChange={(e) => { - setDevelopment(e.target.value); + setDevelopment(parseInt(e.target.value)); }} onKeyDown={(event) => { const keyValue = event.key; @@ -100,7 +104,7 @@ export default function NewTimeReport(): JSX.Element { className="border-2 border-black rounded-md text-center w-1/2" value={meeting} onChange={(e) => { - setMeeting(e.target.value); + setMeeting(parseInt(e.target.value)); }} onKeyDown={(event) => { const keyValue = event.key; @@ -119,7 +123,7 @@ export default function NewTimeReport(): JSX.Element { className="border-2 border-black rounded-md text-center w-1/2" value={administration} onChange={(e) => { - setAdministration(e.target.value); + setAdministration(parseInt(e.target.value)); }} onKeyDown={(event) => { const keyValue = event.key; @@ -138,7 +142,7 @@ export default function NewTimeReport(): JSX.Element { className="border-2 border-black rounded-md text-center w-1/2" value={ownwork} onChange={(e) => { - setOwnWork(e.target.value); + setOwnWork(parseInt(e.target.value)); }} onKeyDown={(event) => { const keyValue = event.key; @@ -157,7 +161,7 @@ export default function NewTimeReport(): JSX.Element { className="border-2 border-black rounded-md text-center w-1/2" value={studies} onChange={(e) => { - setStudies(e.target.value); + setStudies(parseInt(e.target.value)); }} onKeyDown={(event) => { const keyValue = event.key; @@ -176,7 +180,7 @@ export default function NewTimeReport(): JSX.Element { className="border-2 border-black rounded-md text-center w-1/2" value={testing} onChange={(e) => { - setTesting(e.target.value); + setTesting(parseInt(e.target.value)); }} onKeyDown={(event) => { const keyValue = event.key; diff --git a/frontend/src/Components/UserListAdmin.tsx b/frontend/src/Components/UserListAdmin.tsx index 42fb094..e25ad5f 100644 --- a/frontend/src/Components/UserListAdmin.tsx +++ b/frontend/src/Components/UserListAdmin.tsx @@ -1,5 +1,5 @@ import { Link } from "react-router-dom"; -import { User } from "../Types/Users"; +import { User } from "../Types/goTypes"; /** * The props for the UserProps component @@ -23,9 +23,9 @@ export function UserListAdmin(props: UserProps): JSX.Element {
    {props.users.map((user) => ( - -
  • - {user.userName} + +
  • + {user.username}
  • ))} diff --git a/frontend/src/Pages/UserPages/UserNewTimeReportPage.tsx b/frontend/src/Pages/UserPages/UserNewTimeReportPage.tsx index 85c80df..ca84770 100644 --- a/frontend/src/Pages/UserPages/UserNewTimeReportPage.tsx +++ b/frontend/src/Pages/UserPages/UserNewTimeReportPage.tsx @@ -1,7 +1,7 @@ import BasicWindow from "../../Components/BasicWindow"; import Button from "../../Components/Button"; import NewTimeReport from "../../Components/TimeReport"; -import BackButton from "../../Components/BackButton"; +import { Link } from "react-router-dom"; function UserNewTimeReportPage(): JSX.Element { const content = ( From 9240d5e0521203031339766fda169662b2761520 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Sun, 17 Mar 2024 19:24:13 +0100 Subject: [PATCH 10/11] Verbose debug printing in login endpoint --- backend/internal/handlers/handlers_user_related.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/internal/handlers/handlers_user_related.go b/backend/internal/handlers/handlers_user_related.go index e90abd0..c1c8abe 100644 --- a/backend/internal/handlers/handlers_user_related.go +++ b/backend/internal/handlers/handlers_user_related.go @@ -57,9 +57,11 @@ func (gs *GState) Login(c *fiber.Ctx) error { // The body type is identical to a NewUser u := new(types.NewUser) if err := c.BodyParser(u); err != nil { + println("Error parsing body") return c.Status(400).SendString(err.Error()) } + println("Username:", u.Username) if !gs.Db.CheckUser(u.Username, u.Password) { println("User not found") return c.SendStatus(fiber.StatusUnauthorized) @@ -74,13 +76,16 @@ func (gs *GState) Login(c *fiber.Ctx) error { // Create token token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + println("Token created for user:", u.Username) // Generate encoded token and send it as response. t, err := token.SignedString([]byte("secret")) if err != nil { + println("Error signing token") return c.SendStatus(fiber.StatusInternalServerError) } + println("Successfully signed token for user:", u.Username) return c.JSON(fiber.Map{"token": t}) } From 402b0ac08b189faad3b94a41abdd29b85d6e5749 Mon Sep 17 00:00:00 2001 From: al8763be Date: Sun, 17 Mar 2024 19:28:03 +0100 Subject: [PATCH 11/11] Update API.test.ts --- frontend/src/API/API.test.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/frontend/src/API/API.test.ts b/frontend/src/API/API.test.ts index e0a93f6..dbae706 100644 --- a/frontend/src/API/API.test.ts +++ b/frontend/src/API/API.test.ts @@ -8,9 +8,8 @@ describe("API", () => { username: "lol", // Add the username property password: "lol", }; - const response = await api.registerUser(user); - + console.log(response.message); expect(response.success).toBe(true); expect(response.data).toHaveProperty("userId"); }); @@ -24,7 +23,7 @@ describe("API", () => { "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6ZmFsc2UsImV4cCI6MTcxMDk0MDIwMywibmFtZSI6InJyZ3VtZHpwbWMifQ.V9NHoYMYV61t"; const response = await api.createProject(project, token); - + console.log(response.message); expect(response.success).toBe(true); expect(response.data).toHaveProperty("projectId"); }); @@ -34,7 +33,7 @@ describe("API", () => { "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6ZmFsc2UsImV4cCI6MTcxMDk0MDIwMywibmFtZSI6InJyZ3VtZHpwbWMifQ.V9NHoYMYV61t"; const response = await api.renewToken(refreshToken); - + console.log(response.message); expect(response.success).toBe(true); expect(response.data).toHaveProperty("accessToken"); expect(response.data).toHaveProperty("refreshToken"); @@ -45,7 +44,7 @@ describe("API", () => { "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6ZmFsc2UsImV4cCI6MTcxMDk0MDIwMywibmFtZSI6InJyZ3VtZHpwbWMifQ.V9NHoYMYV61t"; const username = "rrgumdzpmc"; const response = await api.getUserProjects(username, token); - + console.log(response.message); expect(response.success).toBe(true); expect(response.data).toHaveProperty("projects"); }); @@ -65,7 +64,7 @@ describe("API", () => { "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6ZmFsc2UsImV4cCI6MTcxMDk0MDIwMywibmFtZSI6InJyZ3VtZHpwbWMifQ.V9NHoYMYV61t"; const response = await api.submitWeeklyReport(report, token); - + console.log(response.message); expect(response.success).toBe(true); expect(response.data).toHaveProperty( "message", @@ -80,7 +79,7 @@ describe("API", () => { }; const response = await api.login(user); - + console.log(response.message); expect(response.success).toBe(true); expect(response.data).toHaveProperty("accessToken"); expect(response.data).toHaveProperty("refreshToken");