From 2468fe8fab7a8aa112269e772fa1879f5589654c Mon Sep 17 00:00:00 2001 From: dDogge <> Date: Fri, 15 Mar 2024 15:14:45 +0100 Subject: [PATCH 01/19] Added GetProject in db.go and corresponding test --- backend/internal/database/db.go | 7 ++++++ backend/internal/database/db_test.go | 35 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index b5e1981..a0cf5b9 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -28,6 +28,7 @@ type Database interface { GetAllUsersApplication() ([]string, error) GetProjectsForUser(username string) ([]types.Project, error) GetAllProjects() ([]types.Project, error) + GetProject(projectId int) (types.Project, error) GetUserRole(username string, projectname string) (string, error) } @@ -100,6 +101,12 @@ func (d *Db) GetAllProjects() ([]types.Project, error) { return projects, err } +func (d *Db) GetProject(projectId int) (types.Project, error) { + var project types.Project + err := d.Select(&project, "SELECT * FROM projects WHERE id = ?") + return project, err +} + func (d *Db) AddTimeReport(projectName string, userName string, start time.Time, end time.Time) error { // WIP _, err := d.Exec(addTimeReport, userName, projectName, start, end) return err diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index 7650739..117c08a 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -343,3 +343,38 @@ func TestGetProjectsForUser(t *testing.T) { t.Error("GetProjectsForUser failed: expected 1, got", len(projects)) } } + +func TestAddProject(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) + } + + // Retrieve the added project to verify its existence + projects, err := db.GetAllProjects() + if err != nil { + t.Error("GetAllProjects failed:", err) + } + + // Check if the project was added successfully + found := false + for _, project := range projects { + if project.Name == "testproject" { + found = true + break + } + } + if !found { + t.Error("Added project not found") + } +} From 78f5415d9abe407019c4615b94cab35634c3f303 Mon Sep 17 00:00:00 2001 From: dDogge <> Date: Fri, 15 Mar 2024 15:28:45 +0100 Subject: [PATCH 02/19] Handler for GetProject from db.go added in global_stage.go --- backend/internal/handlers/global_state.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index fea0dfd..91d46a9 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -1,6 +1,7 @@ package handlers import ( + "strconv" "time" "ttime/internal/database" "ttime/internal/types" @@ -225,3 +226,24 @@ func (gs *GState) ProjectRoleChange(c *fiber.Ctx) 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) +} From 581209742a6d2a45adcd6efdb5444b8fa94b8968 Mon Sep 17 00:00:00 2001 From: dDogge <> Date: Fri, 15 Mar 2024 15:14:45 +0100 Subject: [PATCH 03/19] Added GetProject in db.go and corresponding test --- backend/internal/database/db.go | 7 ++++++ backend/internal/database/db_test.go | 35 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index b5e1981..a0cf5b9 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -28,6 +28,7 @@ type Database interface { GetAllUsersApplication() ([]string, error) GetProjectsForUser(username string) ([]types.Project, error) GetAllProjects() ([]types.Project, error) + GetProject(projectId int) (types.Project, error) GetUserRole(username string, projectname string) (string, error) } @@ -100,6 +101,12 @@ func (d *Db) GetAllProjects() ([]types.Project, error) { return projects, err } +func (d *Db) GetProject(projectId int) (types.Project, error) { + var project types.Project + err := d.Select(&project, "SELECT * FROM projects WHERE id = ?") + return project, err +} + func (d *Db) AddTimeReport(projectName string, userName string, start time.Time, end time.Time) error { // WIP _, err := d.Exec(addTimeReport, userName, projectName, start, end) return err diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index 7650739..117c08a 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -343,3 +343,38 @@ func TestGetProjectsForUser(t *testing.T) { t.Error("GetProjectsForUser failed: expected 1, got", len(projects)) } } + +func TestAddProject(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) + } + + // Retrieve the added project to verify its existence + projects, err := db.GetAllProjects() + if err != nil { + t.Error("GetAllProjects failed:", err) + } + + // Check if the project was added successfully + found := false + for _, project := range projects { + if project.Name == "testproject" { + found = true + break + } + } + if !found { + t.Error("Added project not found") + } +} From 018dc24516d5a398d3725bc68be988eb40ec9436 Mon Sep 17 00:00:00 2001 From: dDogge <> Date: Fri, 15 Mar 2024 15:28:45 +0100 Subject: [PATCH 04/19] Handler for GetProject from db.go added in global_stage.go --- backend/internal/handlers/global_state.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index fea0dfd..91d46a9 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -1,6 +1,7 @@ package handlers import ( + "strconv" "time" "ttime/internal/database" "ttime/internal/types" @@ -225,3 +226,24 @@ func (gs *GState) ProjectRoleChange(c *fiber.Ctx) 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) +} From 04e17a1721982a2fe619ad8ad56c7a8febe523b0 Mon Sep 17 00:00:00 2001 From: dDogge <> Date: Fri, 15 Mar 2024 16:57:42 +0100 Subject: [PATCH 05/19] Added comments to various functions --- backend/internal/database/db.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index a0cf5b9..6ed5b17 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -89,29 +89,34 @@ func DbConnect(dbpath string) Database { return &Db{db} } +// GetProjectsForUser retrieves all projects associated with a specific user. func (d *Db) GetProjectsForUser(username string) ([]types.Project, error) { var projects []types.Project err := d.Select(&projects, getProjectsForUser, username) return projects, err } +// GetAllProjects retrieves all projects from the database. func (d *Db) GetAllProjects() ([]types.Project, error) { var projects []types.Project err := d.Select(&projects, "SELECT * FROM projects") return projects, err } +// GetProject retrieves a specific project by its ID. func (d *Db) GetProject(projectId int) (types.Project, error) { var project types.Project err := d.Select(&project, "SELECT * FROM projects WHERE id = ?") return project, err } +// AddTimeReport adds a time report for a specific project and user. func (d *Db) AddTimeReport(projectName string, userName string, start time.Time, end time.Time) error { // WIP _, err := d.Exec(addTimeReport, userName, projectName, start, end) return err } +// AddUserToProject adds a user to a project with a specified role. func (d *Db) AddUserToProject(username string, projectname string, role string) error { // WIP var userid int userid, err := d.GetUserId(username) @@ -129,23 +134,28 @@ func (d *Db) AddUserToProject(username string, projectname string, role string) return err3 } +// ChangeUserRole changes the role of a user within a project. func (d *Db) ChangeUserRole(username string, projectname string, role string) error { + // Get the user ID var userid int userid, err := d.GetUserId(username) if err != nil { panic(err) } + // Get the project ID var projectid int projectid, err2 := d.GetProjectId(projectname) if err2 != nil { panic(err2) } + // Execute the SQL query to change the user's role _, err3 := d.Exec(changeUserRole, role, userid, projectid) return err3 } +// GetUserRole retrieves the role of a user within a project. func (d *Db) GetUserRole(username string, projectname string) (string, error) { var role string err := d.Get(&role, "SELECT p_role FROM user_roles WHERE user_id = (SELECT id FROM users WHERE username = ?) AND project_id = (SELECT id FROM projects WHERE name = ?)", username, projectname) From 47d7d9fe3cbd916d08917befcea5708a29663543 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Sat, 16 Mar 2024 17:22:55 +0100 Subject: [PATCH 06/19] Activity type database changes with interface changes in go --- backend/internal/database/db.go | 10 +++++----- backend/internal/database/db_test.go | 4 ++-- .../internal/database/migrations/0030_time_reports.sql | 2 ++ .../database/migrations/0080_activity_types.sql | 10 ++++++++++ 4 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 backend/internal/database/migrations/0080_activity_types.sql diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index b5e1981..4efba3f 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -21,7 +21,7 @@ type Database interface { AddProject(name string, description string, username string) error Migrate(dirname string) error GetProjectId(projectname string) (int, error) - AddTimeReport(projectName string, userName string, start time.Time, end time.Time) error + AddTimeReport(projectName string, userName string, activityType string, start time.Time, end time.Time) error AddUserToProject(username string, projectname string, role string) error ChangeUserRole(username string, projectname string, role string) error GetAllUsersProject(projectname string) ([]UserProjectMember, error) @@ -51,8 +51,8 @@ const projectInsert = "INSERT INTO projects (name, description, owner_user_id) S const promoteToAdmin = "INSERT INTO site_admin (admin_id) SELECT id FROM users WHERE username = ?" const addTimeReport = `WITH UserLookup AS (SELECT id FROM users WHERE username = ?), ProjectLookup AS (SELECT id FROM projects WHERE name = ?) - INSERT INTO time_reports (project_id, user_id, start, end) - VALUES ((SELECT id FROM ProjectLookup), (SELECT id FROM UserLookup), ?, ?);` + INSERT INTO time_reports (project_id, user_id, activity_type, start, end) + VALUES ((SELECT id FROM ProjectLookup), (SELECT id FROM UserLookup),?, ?, ?);` const addUserToProject = "INSERT INTO user_roles (user_id, project_id, p_role) VALUES (?, ?, ?)" // WIP const changeUserRole = "UPDATE user_roles SET p_role = ? WHERE user_id = ? AND project_id = ?" @@ -100,8 +100,8 @@ func (d *Db) GetAllProjects() ([]types.Project, error) { return projects, err } -func (d *Db) AddTimeReport(projectName string, userName string, start time.Time, end time.Time) error { // WIP - _, err := d.Exec(addTimeReport, userName, projectName, start, end) +func (d *Db) AddTimeReport(projectName string, userName string, activityType string, start time.Time, end time.Time) error { // WIP + _, err := d.Exec(addTimeReport, userName, projectName, activityType, start, end) return err } diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index 7650739..3b9f339 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -112,7 +112,7 @@ func TestAddTimeReport(t *testing.T) { var now = time.Now() var then = now.Add(time.Hour) - err = db.AddTimeReport("testproject", "testuser", now, then) + err = db.AddTimeReport("testproject", "testuser", "activity", now, then) if err != nil { t.Error("AddTimeReport failed:", err) } @@ -137,7 +137,7 @@ func TestAddUserToProject(t *testing.T) { var now = time.Now() var then = now.Add(time.Hour) - err = db.AddTimeReport("testproject", "testuser", now, then) + err = db.AddTimeReport("testproject", "testuser", "activity", now, then) if err != nil { t.Error("AddTimeReport failed:", err) } diff --git a/backend/internal/database/migrations/0030_time_reports.sql b/backend/internal/database/migrations/0030_time_reports.sql index 76812a1..7c169c2 100644 --- a/backend/internal/database/migrations/0030_time_reports.sql +++ b/backend/internal/database/migrations/0030_time_reports.sql @@ -2,10 +2,12 @@ CREATE TABLE IF NOT EXISTS time_reports ( id INTEGER PRIMARY KEY, project_id INTEGER NOT NULL, user_id INTEGER NOT NULL, + activity_type TEXT NOT NULL, start DATETIME NOT NULL, end DATETIME NOT NULL, FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + FOREIGN KEY (activity_type) REFERENCES activity_types (name) ON DELETE CASCADE ); CREATE TRIGGER IF NOT EXISTS time_reports_start_before_end diff --git a/backend/internal/database/migrations/0080_activity_types.sql b/backend/internal/database/migrations/0080_activity_types.sql new file mode 100644 index 0000000..d984d58 --- /dev/null +++ b/backend/internal/database/migrations/0080_activity_types.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS activity_types ( + name TEXT PRIMARY KEY +); + +INSERT OR IGNORE INTO activity_types (name) VALUES ('Development'); +INSERT OR IGNORE INTO activity_types (name) VALUES ('Meeting'); +INSERT OR IGNORE INTO activity_types (name) VALUES ('Administration'); +INSERT OR IGNORE INTO activity_types (name) VALUES ('Own Work'); +INSERT OR IGNORE INTO activity_types (name) VALUES ('Studies'); +INSErt OR IGNORE INTO activity_types (name) VALUES ('Testing'); \ No newline at end of file From 17c8a17ebf3a52895eda60f5b2a80a2e6ab46e9d Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Sat, 16 Mar 2024 17:32:51 +0100 Subject: [PATCH 07/19] IF EXISTS condition in salts table --- backend/internal/database/migrations/0070_salts.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/internal/database/migrations/0070_salts.sql b/backend/internal/database/migrations/0070_salts.sql index b84dfac..de9757d 100644 --- a/backend/internal/database/migrations/0070_salts.sql +++ b/backend/internal/database/migrations/0070_salts.sql @@ -1,7 +1,7 @@ -- It is unclear weather this table will be used -- Create the table to store hash salts -CREATE TABLE salts ( +CREATE TABLE IF NOT EXISTS salts ( id INTEGER PRIMARY KEY, salt TEXT NOT NULL ); From 3c87fd4d8cafe79961ad7cb46512c0a19189c62e Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Sat, 16 Mar 2024 17:42:28 +0100 Subject: [PATCH 08/19] New api interface --- frontend/src/API/API.ts | 135 +++++++++++++++++++++++++++++----------- 1 file changed, 99 insertions(+), 36 deletions(-) diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index f33c87c..248ad37 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -1,57 +1,120 @@ import { NewProject, Project } from "../Types/Project"; import { NewUser, User } from "../Types/Users"; +// This type of pattern should be hard to misuse +interface APIResponse { + success: boolean; + message?: string; + data?: T; +} + +// Note that all protected routes also require a token // Defines all the methods that an instance of the API must implement interface API { /** Register a new user */ - registerUser(user: NewUser): Promise; + registerUser(user: NewUser): Promise>; /** Remove a user */ - removeUser(username: string): Promise; + removeUser(username: string, token: string): Promise>; /** Create a project */ - createProject(project: NewProject): Promise; + createProject( + project: NewProject, + token: string, + ): Promise>; /** Renew the token */ - renewToken(token: string): Promise; + renewToken(token: string): Promise>; } // Export an instance of the API export const api: API = { - async registerUser(user: NewUser): Promise { - return fetch("/api/register", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(user), - }).then((res) => res.json() as Promise); + async registerUser(user: NewUser): Promise> { + try { + const response = await fetch("/api/register", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(user), + }); + + if (!response.ok) { + return { success: false, message: "Failed to register user" }; + } else { + const data = (await response.json()) as User; + return { success: true, data }; + } + } catch (e) { + return { success: false, message: "Failed to register user" }; + } }, - async removeUser(username: string): Promise { - return fetch("/api/userdelete", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(username), - }).then((res) => res.json() as Promise); + async removeUser( + username: string, + token: string, + ): Promise> { + try { + const response = await fetch("/api/userdelete", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + body: JSON.stringify(username), + }); + + if (!response.ok) { + return { success: false, message: "Failed to remove user" }; + } else { + const data = (await response.json()) as User; + return { success: true, data }; + } + } catch (e) { + return { success: false, message: "Failed to remove user" }; + } }, - async createProject(project: NewProject): Promise { - return fetch("/api/project", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(project), - }).then((res) => res.json() as Promise); + async createProject( + project: NewProject, + token: string, + ): Promise> { + try { + const response = await fetch("/api/project", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + body: JSON.stringify(project), + }); + + if (!response.ok) { + return { success: false, message: "Failed to create project" }; + } else { + const data = (await response.json()) as Project; + return { success: true, data }; + } + } catch (e) { + return { success: false, message: "Failed to create project" }; + } }, - async renewToken(token: string): Promise { - return fetch("/api/loginrenew", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, - }, - }).then((res) => res.json() as Promise); + async renewToken(token: string): Promise> { + try { + const response = await fetch("/api/loginrenew", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }); + + if (!response.ok) { + return { success: false, message: "Failed to renew token" }; + } else { + const data = (await response.json()) as string; + return { success: true, data }; + } + } catch (e) { + return { success: false, message: "Failed to renew token" }; + } }, }; From 151d6de39b15346f57a4294e5922a5fc08a5a73c Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Sat, 16 Mar 2024 17:43:38 +0100 Subject: [PATCH 09/19] Tygo for go->typescript type generation --- backend/Makefile | 4 ++++ backend/tygo.yaml | 9 +++++++++ frontend/.eslintrc.cjs | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 backend/tygo.yaml diff --git a/backend/Makefile b/backend/Makefile index 9cfa335..da0e254 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -118,3 +118,7 @@ uml: plantuml.jar install-just: @echo "Installing just" @curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin + +.PHONY: types +types: + tygo generate \ No newline at end of file diff --git a/backend/tygo.yaml b/backend/tygo.yaml new file mode 100644 index 0000000..54c1e8f --- /dev/null +++ b/backend/tygo.yaml @@ -0,0 +1,9 @@ +packages: + - path: "ttime/internal/types" + output_path: "../frontend/src/Types/goTypes.ts" + type_mappings: + time.Time: "string /* RFC3339 */" + null.String: "null | string" + null.Bool: "null | boolean" + uuid.UUID: "string /* uuid */" + uuid.NullUUID: "null | string /* uuid */" diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs index 447a464..1051c03 100644 --- a/frontend/.eslintrc.cjs +++ b/frontend/.eslintrc.cjs @@ -9,7 +9,7 @@ module.exports = { 'plugin:react-hooks/recommended', 'plugin:prettier/recommended', ], - ignorePatterns: ['dist', '.eslintrc.cjs', 'tailwind.config.js', 'postcss.config.js', 'jest.config.cjs'], + ignorePatterns: ['dist', '.eslintrc.cjs', 'tailwind.config.js', 'postcss.config.js', 'jest.config.cjs', 'goTypes.ts'], parser: '@typescript-eslint/parser', plugins: ['react-refresh', 'prettier'], rules: { From d99de54c5d3e6a10d6ef49f6dc5430109c1301a0 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Sat, 16 Mar 2024 22:47:19 +0100 Subject: [PATCH 10/19] Major changes related to reports --- backend/internal/database/db.go | 31 ++++++------------ backend/internal/database/db_test.go | 17 +++------- .../database/migrations/0030_time_reports.sql | 22 ------------- .../migrations/0035_weekly_report.sql | 14 ++++++++ .../0040_time_report_collections.sql | 9 ------ .../database/migrations/0070_salts.sql | 16 ---------- .../migrations/0080_activity_types.sql | 10 ------ backend/internal/handlers/global_state.go | 32 ++++++++++++++++--- backend/internal/types/WeeklyReport.go | 21 ++++++++++++ backend/internal/types/project.go | 3 +- backend/internal/types/users.go | 1 + backend/main.go | 3 +- 12 files changed, 81 insertions(+), 98 deletions(-) delete mode 100644 backend/internal/database/migrations/0030_time_reports.sql create mode 100644 backend/internal/database/migrations/0035_weekly_report.sql delete mode 100644 backend/internal/database/migrations/0040_time_report_collections.sql delete mode 100644 backend/internal/database/migrations/0070_salts.sql delete mode 100644 backend/internal/database/migrations/0080_activity_types.sql create mode 100644 backend/internal/types/WeeklyReport.go diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index c13308b..6b8a990 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -4,7 +4,6 @@ import ( "embed" "os" "path/filepath" - "time" "ttime/internal/types" "github.com/jmoiron/sqlx" @@ -21,7 +20,7 @@ type Database interface { AddProject(name string, description string, username string) error Migrate(dirname string) error GetProjectId(projectname string) (int, error) - AddTimeReport(projectName string, userName string, activityType string, start time.Time, end time.Time) error + AddWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error AddUserToProject(username string, projectname string, role string) error ChangeUserRole(username string, projectname string, role string) error GetAllUsersProject(projectname string) ([]UserProjectMember, error) @@ -50,27 +49,16 @@ var scripts embed.FS const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)" const projectInsert = "INSERT INTO projects (name, description, owner_user_id) SELECT ?, ?, id FROM users WHERE username = ?" const promoteToAdmin = "INSERT INTO site_admin (admin_id) SELECT id FROM users WHERE username = ?" -const addTimeReport = `WITH UserLookup AS (SELECT id FROM users WHERE username = ?), +const addWeeklyReport = `WITH UserLookup AS (SELECT id FROM users WHERE username = ?), ProjectLookup AS (SELECT id FROM projects WHERE name = ?) - INSERT INTO time_reports (project_id, user_id, activity_type, start, end) - VALUES ((SELECT id FROM ProjectLookup), (SELECT id FROM UserLookup),?, ?, ?);` + INSERT INTO weekly_reports (project_id, user_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time) + VALUES ((SELECT id FROM ProjectLookup), (SELECT id FROM UserLookup),?, ?, ?, ?, ?, ?, ?);` const addUserToProject = "INSERT INTO user_roles (user_id, project_id, p_role) VALUES (?, ?, ?)" // WIP const changeUserRole = "UPDATE user_roles SET p_role = ? WHERE user_id = ? AND project_id = ?" -const getProjectsForUser = ` -SELECT - projects.id, - projects.name, - projects.description, - projects.owner_user_id -FROM - projects -JOIN - user_roles ON projects.id = user_roles.project_id -JOIN - users ON user_roles.user_id = users.id -WHERE - users.username = ?;` +const getProjectsForUser = `SELECT projects.id, projects.name, projects.description, projects.owner_user_id + FROM projects JOIN user_roles ON projects.id = user_roles.project_id + JOIN users ON user_roles.user_id = users.id WHERE users.username = ?;` // DbConnect connects to the database func DbConnect(dbpath string) Database { @@ -110,9 +98,8 @@ func (d *Db) GetProject(projectId int) (types.Project, error) { return project, err } -// AddTimeReport adds a time report for a specific project and user. -func (d *Db) AddTimeReport(projectName string, userName string, activityType string, start time.Time, end time.Time) error { // WIP - _, err := d.Exec(addTimeReport, userName, projectName, activityType, start, end) +func (d *Db) AddWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error { + _, err := d.Exec(addWeeklyReport, userName, projectName, week, developmentTime, meetingTime, adminTime, ownWorkTime, studyTime, testingTime) return err } diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index 9118e2f..5438d66 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -2,7 +2,6 @@ package database import ( "testing" - "time" ) // Tests are not guaranteed to be sequential @@ -93,7 +92,7 @@ func TestPromoteToAdmin(t *testing.T) { } } -func TestAddTimeReport(t *testing.T) { +func TestAddWeeklyReport(t *testing.T) { db, err := setupState() if err != nil { t.Error("setupState failed:", err) @@ -109,12 +108,9 @@ func TestAddTimeReport(t *testing.T) { t.Error("AddProject failed:", err) } - var now = time.Now() - var then = now.Add(time.Hour) - - err = db.AddTimeReport("testproject", "testuser", "activity", now, then) + err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1) if err != nil { - t.Error("AddTimeReport failed:", err) + t.Error("AddWeeklyReport failed:", err) } } @@ -134,12 +130,9 @@ func TestAddUserToProject(t *testing.T) { t.Error("AddProject failed:", err) } - var now = time.Now() - var then = now.Add(time.Hour) - - err = db.AddTimeReport("testproject", "testuser", "activity", now, then) + err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1) if err != nil { - t.Error("AddTimeReport failed:", err) + t.Error("AddWeeklyReport failed:", err) } err = db.AddUserToProject("testuser", "testproject", "user") diff --git a/backend/internal/database/migrations/0030_time_reports.sql b/backend/internal/database/migrations/0030_time_reports.sql deleted file mode 100644 index 7c169c2..0000000 --- a/backend/internal/database/migrations/0030_time_reports.sql +++ /dev/null @@ -1,22 +0,0 @@ -CREATE TABLE IF NOT EXISTS time_reports ( - id INTEGER PRIMARY KEY, - project_id INTEGER NOT NULL, - user_id INTEGER NOT NULL, - activity_type TEXT NOT NULL, - start DATETIME NOT NULL, - end DATETIME NOT NULL, - FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE - FOREIGN KEY (activity_type) REFERENCES activity_types (name) ON DELETE CASCADE -); - -CREATE TRIGGER IF NOT EXISTS time_reports_start_before_end - BEFORE INSERT ON time_reports - FOR EACH ROW - BEGIN - SELECT - CASE - WHEN NEW.start >= NEW.end THEN - RAISE (ABORT, 'start must be before end') - END; - END; \ No newline at end of file diff --git a/backend/internal/database/migrations/0035_weekly_report.sql b/backend/internal/database/migrations/0035_weekly_report.sql new file mode 100644 index 0000000..0e29b97 --- /dev/null +++ b/backend/internal/database/migrations/0035_weekly_report.sql @@ -0,0 +1,14 @@ +CREATE TABLE weekly_reports ( + user_id INTEGER, + project_id INTEGER, + week INTEGER, + development_time INTEGER, + meeting_time INTEGER, + admin_time INTEGER, + own_work_time INTEGER, + study_time INTEGER, + testing_time INTEGER, + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (project_id) REFERENCES projects(id) + PRIMARY KEY (user_id, project_id, week) +) \ No newline at end of file diff --git a/backend/internal/database/migrations/0040_time_report_collections.sql b/backend/internal/database/migrations/0040_time_report_collections.sql deleted file mode 100644 index be406ff..0000000 --- a/backend/internal/database/migrations/0040_time_report_collections.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE TABLE IF NOT EXISTS report_collection ( - id INTEGER PRIMARY KEY, - owner_id INTEGER NOT NULL, - project_id INTEGER NOT NULL, - date DATE NOT NULL, - signed_by INTEGER, -- NULL if not signed - FOREIGN KEY (owner_id) REFERENCES users (id) - FOREIGN KEY (signed_by) REFERENCES users (id) -); \ No newline at end of file diff --git a/backend/internal/database/migrations/0070_salts.sql b/backend/internal/database/migrations/0070_salts.sql deleted file mode 100644 index de9757d..0000000 --- a/backend/internal/database/migrations/0070_salts.sql +++ /dev/null @@ -1,16 +0,0 @@ --- It is unclear weather this table will be used - --- Create the table to store hash salts -CREATE TABLE IF NOT EXISTS salts ( - id INTEGER PRIMARY KEY, - salt TEXT NOT NULL -); - --- Commented out for now, no time for good practices, which is atrocious --- Create a trigger to automatically generate a salt when inserting a new user record --- CREATE TRIGGER generate_salt_trigger --- AFTER INSERT ON users --- BEGIN --- INSERT INTO salts (salt) VALUES (randomblob(16)); --- UPDATE users SET salt_id = (SELECT last_insert_rowid()) WHERE id = new.id; --- END; diff --git a/backend/internal/database/migrations/0080_activity_types.sql b/backend/internal/database/migrations/0080_activity_types.sql deleted file mode 100644 index d984d58..0000000 --- a/backend/internal/database/migrations/0080_activity_types.sql +++ /dev/null @@ -1,10 +0,0 @@ -CREATE TABLE IF NOT EXISTS activity_types ( - name TEXT PRIMARY KEY -); - -INSERT OR IGNORE INTO activity_types (name) VALUES ('Development'); -INSERT OR IGNORE INTO activity_types (name) VALUES ('Meeting'); -INSERT OR IGNORE INTO activity_types (name) VALUES ('Administration'); -INSERT OR IGNORE INTO activity_types (name) VALUES ('Own Work'); -INSERT OR IGNORE INTO activity_types (name) VALUES ('Studies'); -INSErt OR IGNORE INTO activity_types (name) VALUES ('Testing'); \ No newline at end of file diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index 91d46a9..f8c7ce1 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -18,6 +18,7 @@ type GlobalState interface { LoginRenew(c *fiber.Ctx) error // To renew the token CreateProject(c *fiber.Ctx) error // To create a new project GetUserProjects(c *fiber.Ctx) error // To get all projects + SubmitWeeklyReport(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 @@ -77,12 +78,17 @@ func (gs *GState) Register(c *fiber.Ctx) error { // This path should obviously be protected in the future // UserDelete deletes a user from the database func (gs *GState) UserDelete(c *fiber.Ctx) error { - u := new(types.User) - if err := c.BodyParser(u); err != nil { - return c.Status(400).SendString(err.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(u.Username); err != nil { + if err := gs.Db.RemoveUser(username); err != nil { return c.Status(500).SendString(err.Error()) } @@ -247,3 +253,21 @@ func (gs *GState) GetProject(c *fiber.Ctx) 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()) + } + + 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/types/WeeklyReport.go b/backend/internal/types/WeeklyReport.go new file mode 100644 index 0000000..23624db --- /dev/null +++ b/backend/internal/types/WeeklyReport.go @@ -0,0 +1,21 @@ +package types + +// This is what should be submitted to the server, the username will be derived from the JWT token +type NewWeeklyReport struct { + // The name of the project, as it appears in the database + ProjectName string + // The week number + Week int + // Total time spent on development + DevelopmentTime int + // Total time spent in meetings + MeetingTime int + // Total time spent on administrative tasks + AdminTime int + // Total time spent on personal projects + OwnWorkTime int + // Total time spent on studying + StudyTime int + // Total time spent on testing + TestingTime int +} diff --git a/backend/internal/types/project.go b/backend/internal/types/project.go index 8fcfaf5..7e1747f 100644 --- a/backend/internal/types/project.go +++ b/backend/internal/types/project.go @@ -8,9 +8,8 @@ type Project struct { Owner string `json:"owner" db:"owner_user_id"` } -// As it arrives from the client +// As it arrives from the client, Owner is derived from the JWT token type NewProject struct { Name string `json:"name"` Description string `json:"description"` - Owner string `json:"owner"` } diff --git a/backend/internal/types/users.go b/backend/internal/types/users.go index 233ec71..e9dff67 100644 --- a/backend/internal/types/users.go +++ b/backend/internal/types/users.go @@ -16,6 +16,7 @@ func (u *User) ToPublicUser() (*PublicUser, error) { }, nil } +// Should be used when registering, for example type NewUser struct { Username string `json:"username"` Password string `json:"password"` diff --git a/backend/main.go b/backend/main.go index 4e0935c..9ba2556 100644 --- a/backend/main.go +++ b/backend/main.go @@ -68,9 +68,10 @@ func main() { SigningKey: jwtware.SigningKey{Key: []byte("secret")}, })) + server.Post("/api/submitReport", gs.SubmitWeeklyReport) server.Get("/api/getUserProjects", gs.GetUserProjects) server.Post("/api/loginrenew", gs.LoginRenew) - server.Delete("/api/userdelete", gs.UserDelete) // Perhaps just use POST to avoid headaches + server.Delete("/api/userdelete/:username", gs.UserDelete) // Perhaps just use POST to avoid headaches server.Post("/api/project", gs.CreateProject) // Announce the port we are listening on and start the server From c6d93079790e0ca3e9945883d2223b20491686ea Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Sat, 16 Mar 2024 22:57:48 +0100 Subject: [PATCH 11/19] Fix for CreateProject handler --- backend/internal/handlers/global_state.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index f8c7ce1..f7172f5 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -165,9 +165,9 @@ func (gs *GState) CreateProject(c *fiber.Ctx) 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) - p.Owner = claims["name"].(string) + owner := claims["name"].(string) - if err := gs.Db.AddProject(p.Name, p.Description, p.Owner); err != nil { + if err := gs.Db.AddProject(p.Name, p.Description, owner); err != nil { return c.Status(500).SendString(err.Error()) } From c13378d3b964c85b77f7f1c467fd3c70a35a3ba3 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Sun, 17 Mar 2024 01:32:10 +0100 Subject: [PATCH 12/19] Proper login endpoint functionality --- backend/internal/database/db.go | 10 ++++++++++ backend/internal/handlers/global_state.go | 3 +-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 6b8a990..ef365cd 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -14,6 +14,7 @@ import ( type Database interface { // Insert a new user into the database, password should be hashed before calling AddUser(username string, password string) error + CheckUser(username string, password string) bool RemoveUser(username string) error PromoteToAdmin(username string) error GetUserId(username string) (int, error) @@ -77,6 +78,15 @@ func DbConnect(dbpath string) Database { return &Db{db} } +func (d *Db) CheckUser(username string, password string) bool { + var dbPassword string + err := d.Get(&dbPassword, "SELECT password FROM users WHERE username = ?", username) + if err != nil { + return false + } + return dbPassword == password +} + // GetProjectsForUser retrieves all projects associated with a specific user. func (d *Db) GetProjectsForUser(username string) ([]types.Project, error) { var projects []types.Project diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index f7172f5..648b4ed 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -110,8 +110,7 @@ func (gs *GState) Login(c *fiber.Ctx) error { user := c.FormValue("user") pass := c.FormValue("pass") - // Throws Unauthorized error - if user != "user" || pass != "pass" { + if !gs.Db.CheckUser(user, pass) { return c.SendStatus(fiber.StatusUnauthorized) } From 04d7a2cdeccd041bf0d07170a7f15287247bbb79 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Sun, 17 Mar 2024 01:55:24 +0100 Subject: [PATCH 13/19] Test script --- Makefile | 4 ++++ testing.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 testing.py diff --git a/Makefile b/Makefile index 668ccf1..97db62e 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,10 @@ clean: remove-podman-containers cd backend && make clean @echo "Cleaned up!" +.PHONY: itest +itest: + python testing.py + # Cleans up everything related to podman, not just the project. Make sure you understand what this means. podman-clean: podman system reset --force diff --git a/testing.py b/testing.py new file mode 100644 index 0000000..6394b94 --- /dev/null +++ b/testing.py @@ -0,0 +1,47 @@ +import requests +import string +import random + +def randomString(len=10): + """Generate a random string of fixed length """ + letters = string.ascii_lowercase + return ''.join(random.choice(letters) for i in range(len)) + +# Defined once per test run +username = randomString() + +# Define the base URL of the API +base_url = "http://localhost:8080" + +# Define the endpoint to test +registerPath = base_url + "/api/register" +loginPath = base_url + "/api/login" + +# Define a function to prform POST request with data and return response +def create_user(data): + response = requests.post(registerPath, json=data) + return response + +def login(username, password): + response = requests.post(loginPath, json={"username": username, "password": password}) + return response + +def test_login(): + response = login(username, "always_same") + assert response.status_code == 200, "Login failed" + print("Login successful") + print(response.json()["token"]) + +# Define a function to test the POST request +def test_create_user(): + data = {"username": username, "password": "always_same"} + response = create_user(data) + assert response.status_code == 200, "Registration failed" + print("Registration successful") + +# Run the tests +if __name__ == "__main__": + # test_get_users() + test_create_user() + test_login() + From 2e44d1437023411d7458b7ee2ad40dbe76db665d Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Sun, 17 Mar 2024 03:39:31 +0100 Subject: [PATCH 14/19] Extending test script --- backend/internal/handlers/global_state.go | 13 ++++-- testing.py | 56 +++++++++++++++++------ 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index 648b4ed..415b215 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -106,17 +106,20 @@ func (gs *GState) IncrementButtonCount(c *fiber.Ctx) error { // Login is a simple login handler that returns a JWT token func (gs *GState) Login(c *fiber.Ctx) error { - // To test: curl --data "user=user&pass=pass" http://localhost:8080/api/login - user := c.FormValue("user") - pass := c.FormValue("pass") + // 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(user, pass) { + if !gs.Db.CheckUser(u.Username, u.Password) { + println("User not found") return c.SendStatus(fiber.StatusUnauthorized) } // Create the Claims claims := jwt.MapClaims{ - "name": user, + "name": u.Username, "admin": false, "exp": time.Now().Add(time.Hour * 72).Unix(), } diff --git a/testing.py b/testing.py index 6394b94..fa97567 100644 --- a/testing.py +++ b/testing.py @@ -2,46 +2,74 @@ import requests import string import random + def randomString(len=10): - """Generate a random string of fixed length """ + """Generate a random string of fixed length""" letters = string.ascii_lowercase - return ''.join(random.choice(letters) for i in range(len)) + return "".join(random.choice(letters) for i in range(len)) + # Defined once per test run username = randomString() +token = None -# Define the base URL of the API +# The base URL of the API base_url = "http://localhost:8080" -# Define the endpoint to test +# Endpoint to test registerPath = base_url + "/api/register" loginPath = base_url + "/api/login" +addProjectPath = base_url + "/api/project" + # Define a function to prform POST request with data and return response -def create_user(data): - response = requests.post(registerPath, json=data) +def register(username: string, password: string): + print("Registering with username: ", username, " and password: ", password) + response = requests.post( + registerPath, json={"username": username, "password": password} + ) + print(response.text) return response -def login(username, password): - response = requests.post(loginPath, json={"username": username, "password": password}) + +def login(username: string, password: string): + print("Logging in with username: ", username, " and password: ", password) + response = requests.post( + loginPath, json={"username": username, "password": password} + ) + print(response.text) return response + def test_login(): response = login(username, "always_same") assert response.status_code == 200, "Login failed" print("Login successful") - print(response.json()["token"]) + return response.json()["token"] + # Define a function to test the POST request def test_create_user(): - data = {"username": username, "password": "always_same"} - response = create_user(data) + response = register(username, "always_same") assert response.status_code == 200, "Registration failed" print("Registration successful") -# Run the tests + +def test_add_project(): + loginResponse = login(username, "always_same") + token = loginResponse.json()["token"] + projectName = randomString() + response = requests.post( + addProjectPath, + json={"name": projectName, "description": "This is a project"}, + headers={"Authorization": "Bearer " + token}, + ) + print(response.text) + assert response.status_code == 200, "Add project failed" + print("Add project successful") + + if __name__ == "__main__": - # test_get_users() test_create_user() test_login() - + test_add_project() From b69f8d82ff43f440320eb74797b9f511f9f03613 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Sun, 17 Mar 2024 03:46:16 +0100 Subject: [PATCH 15/19] Better testing comments --- testing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing.py b/testing.py index fa97567..ff9534f 100644 --- a/testing.py +++ b/testing.py @@ -22,7 +22,7 @@ loginPath = base_url + "/api/login" addProjectPath = base_url + "/api/project" -# Define a function to prform POST request with data and return response +# Posts the username and password to the register endpoint def register(username: string, password: string): print("Registering with username: ", username, " and password: ", password) response = requests.post( @@ -32,6 +32,7 @@ def register(username: string, password: string): return response +# Posts the username and password to the login endpoint def login(username: string, password: string): print("Logging in with username: ", username, " and password: ", password) response = requests.post( @@ -48,7 +49,6 @@ def test_login(): return response.json()["token"] -# Define a function to test the POST request def test_create_user(): response = register(username, "always_same") assert response.status_code == 200, "Registration failed" From 23dd22eab59c152cf13b3b11db58e27b39fe502e Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Sun, 17 Mar 2024 04:14:40 +0100 Subject: [PATCH 16/19] Json field alias for WeeklyReport --- backend/internal/types/WeeklyReport.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/internal/types/WeeklyReport.go b/backend/internal/types/WeeklyReport.go index 23624db..e0ea1ef 100644 --- a/backend/internal/types/WeeklyReport.go +++ b/backend/internal/types/WeeklyReport.go @@ -3,19 +3,19 @@ package types // This is what should be submitted to the server, the username will be derived from the JWT token type NewWeeklyReport struct { // The name of the project, as it appears in the database - ProjectName string + ProjectName string `json:"projectName"` // The week number - Week int + Week int `json:"week"` // Total time spent on development - DevelopmentTime int + DevelopmentTime int `json:"developmentTime"` // Total time spent in meetings - MeetingTime int + MeetingTime int `json:"meetingTime"` // Total time spent on administrative tasks - AdminTime int + AdminTime int `json:"adminTime"` // Total time spent on personal projects - OwnWorkTime int + OwnWorkTime int `json:"ownWorkTime"` // Total time spent on studying - StudyTime int + StudyTime int `json:"studyTime"` // Total time spent on testing - TestingTime int + TestingTime int `json:"testingTime"` } From 0d053add5e93669b71d13d97a08108ce9a5a9ef4 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Sun, 17 Mar 2024 04:16:26 +0100 Subject: [PATCH 17/19] Add some sanity checking in SubmitWeeklyReport route --- backend/internal/handlers/global_state.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index 415b215..2378f7b 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -267,6 +267,14 @@ func (gs *GState) SubmitWeeklyReport(c *fiber.Ctx) error { 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()) } From dbb2ff84e56ae60f0d1bba64e1487e0e7d605e02 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Sun, 17 Mar 2024 04:16:54 +0100 Subject: [PATCH 18/19] WeeklyReport testing --- testing.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/testing.py b/testing.py index ff9534f..d2c64fe 100644 --- a/testing.py +++ b/testing.py @@ -11,7 +11,7 @@ def randomString(len=10): # Defined once per test run username = randomString() -token = None +projectName = randomString() # The base URL of the API base_url = "http://localhost:8080" @@ -20,6 +20,7 @@ base_url = "http://localhost:8080" registerPath = base_url + "/api/register" loginPath = base_url + "/api/login" addProjectPath = base_url + "/api/project" +submitReportPath = base_url + "/api/submitReport" # Posts the username and password to the register endpoint @@ -58,7 +59,6 @@ def test_create_user(): def test_add_project(): loginResponse = login(username, "always_same") token = loginResponse.json()["token"] - projectName = randomString() response = requests.post( addProjectPath, json={"name": projectName, "description": "This is a project"}, @@ -69,7 +69,29 @@ def test_add_project(): print("Add project successful") +def test_submit_report(): + token = login(username, "always_same").json()["token"] + response = requests.post( + submitReportPath, + json={ + "projectName": "report1", + "week": 1, + "developmentTime": 10, + "meetingTime": 5, + "adminTime": 5, + "ownWorkTime": 10, + "studyTime": 10, + "testingTime": 10, + }, + headers={"Authorization": "Bearer " + token}, + ) + print(response.text) + assert response.status_code == 200, "Submit report failed" + print("Submit report successful") + + if __name__ == "__main__": test_create_user() test_login() test_add_project() + test_submit_report() From 887f31dde001a477c1b0048064df618779762299 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Sun, 17 Mar 2024 04:23:26 +0100 Subject: [PATCH 19/19] goTypes generated from go code with tygo --- frontend/src/Types/goTypes.ts | 88 +++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 frontend/src/Types/goTypes.ts diff --git a/frontend/src/Types/goTypes.ts b/frontend/src/Types/goTypes.ts new file mode 100644 index 0000000..89084b7 --- /dev/null +++ b/frontend/src/Types/goTypes.ts @@ -0,0 +1,88 @@ +// Code generated by tygo. DO NOT EDIT. + +////////// +// source: WeeklyReport.go + +/** + * This is what should be submitted to the server, the username will be derived from the JWT token + */ +export interface NewWeeklyReport { + /** + * The name of the project, as it appears in the database + */ + projectName: string; + /** + * The week number + */ + week: number /* int */; + /** + * Total time spent on development + */ + developmentTime: number /* int */; + /** + * Total time spent in meetings + */ + meetingTime: number /* int */; + /** + * Total time spent on administrative tasks + */ + adminTime: number /* int */; + /** + * Total time spent on personal projects + */ + ownWorkTime: number /* int */; + /** + * Total time spent on studying + */ + studyTime: number /* int */; + /** + * Total time spent on testing + */ + testingTime: number /* int */; +} + +////////// +// source: project.go + +/** + * Project is a struct that holds the information about a project + */ +export interface Project { + id: number /* int */; + name: string; + description: string; + owner: string; +} +/** + * As it arrives from the client, Owner is derived from the JWT token + */ +export interface NewProject { + name: string; + description: string; +} + +////////// +// source: users.go + +/** + * User struct represents a user in the system + */ +export interface User { + userId: string; + username: string; + password: string; +} +/** + * Should be used when registering, for example + */ +export interface NewUser { + username: string; + password: string; +} +/** + * PublicUser represents a user that is safe to send over the API (no password) + */ +export interface PublicUser { + userId: string; + username: string; +}