diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 7f4a89c..5cbb13f 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -2,6 +2,7 @@ package database import ( "embed" + "errors" "path/filepath" "ttime/internal/types" @@ -30,6 +31,7 @@ type Database interface { GetProject(projectId int) (types.Project, error) GetUserRole(username string, projectname string) (string, error) GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error) + SignWeeklyReport(reportId int, projectManagerId int) error } // This struct is a wrapper type that holds the database connection @@ -270,7 +272,8 @@ func (d *Db) GetWeeklyReport(username string, projectName string, week int) (typ admin_time, own_work_time, study_time, - testing_time + testing_time, + signed_by FROM weekly_reports WHERE @@ -282,6 +285,34 @@ func (d *Db) GetWeeklyReport(username string, projectName string, week int) (typ return report, err } +// SignWeeklyReport signs a weekly report by updating the signed_by field +// with the provided project manager's ID, but only if the project manager +// is in the same project as the report +func (d *Db) SignWeeklyReport(reportId int, projectManagerId int) error { + // Retrieve the project ID associated with the report + var reportProjectID int + err := d.Get(&reportProjectID, "SELECT project_id FROM weekly_reports WHERE report_id = ?", reportId) + if err != nil { + return err + } + + // Retrieve the project ID associated with the project manager + var managerProjectID int + err = d.Get(&managerProjectID, "SELECT project_id FROM user_roles WHERE user_id = ? AND p_role = 'project_manager'", projectManagerId) + if err != nil { + return err + } + + // Check if the project manager is in the same project as the report + if reportProjectID != managerProjectID { + return errors.New("project manager doesn't have permission to sign the report") + } + + // Update the signed_by field of the specified report + _, err = d.Exec("UPDATE weekly_reports SET signed_by = ? WHERE report_id = ?", projectManagerId, reportId) + return 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 f791066..09de45b 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -1,6 +1,7 @@ package database import ( + "fmt" "testing" ) @@ -410,3 +411,128 @@ func TestGetWeeklyReport(t *testing.T) { } // Check other fields similarly } + +func TestSignWeeklyReport(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + // Add project manager + err = db.AddUser("projectManager", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + // Add a regular user + err = db.AddUser("testuser", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + // Add project + err = db.AddProject("testproject", "description", "projectManager") + if err != nil { + t.Error("AddProject failed:", err) + } + + // Add both regular users as members to the project + err = db.AddUserToProject("testuser", "testproject", "member") + if err != nil { + t.Error("AddUserToProject failed:", err) + } + + err = db.AddUserToProject("projectManager", "testproject", "project_manager") + if err != nil { + t.Error("AddUserToProject failed:", err) + } + + // Add a weekly report for one of the regular users + err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1) + if err != nil { + t.Error("AddWeeklyReport failed:", err) + } + + // Retrieve the added report + report, err := db.GetWeeklyReport("testuser", "testproject", 1) + if err != nil { + t.Error("GetWeeklyReport failed:", err) + } + + // Print project manager's ID + projectManagerID, err := db.GetUserId("projectManager") + if err != nil { + t.Error("GetUserId failed:", err) + } + fmt.Println("Project Manager's ID:", projectManagerID) + + // Sign the report with the project manager + err = db.SignWeeklyReport(report.ReportId, projectManagerID) + if err != nil { + t.Error("SignWeeklyReport failed:", err) + } + + // Retrieve the report again to check if it's signed + signedReport, err := db.GetWeeklyReport("testuser", "testproject", 1) + if err != nil { + t.Error("GetWeeklyReport failed:", err) + } + + // Ensure the report is signed by the project manager + if *signedReport.SignedBy != projectManagerID { + t.Errorf("Expected SignedBy to be %d, got %d", projectManagerID, *signedReport.SignedBy) + } +} + +func TestSignWeeklyReportByAnotherProjectManager(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + // Add project manager + err = db.AddUser("projectManager", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + // Add a regular user + err = db.AddUser("testuser", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + // Add project + err = db.AddProject("testproject", "description", "projectManager") + if err != nil { + t.Error("AddProject failed:", err) + } + + // Add the regular user as a member to the project + err = db.AddUserToProject("testuser", "testproject", "member") + if err != nil { + t.Error("AddUserToProject failed:", err) + } + + // Add a weekly report for the regular user + err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1) + if err != nil { + t.Error("AddWeeklyReport failed:", err) + } + + // Retrieve the added report + report, err := db.GetWeeklyReport("testuser", "testproject", 1) + if err != nil { + t.Error("GetWeeklyReport failed:", err) + } + + anotherManagerID, err := db.GetUserId("projectManager") + if err != nil { + t.Error("GetUserId failed:", err) + } + + err = db.SignWeeklyReport(report.ReportId, anotherManagerID) + if err == nil { + t.Error("Expected SignWeeklyReport to fail with a project manager who is not in the project, but it didn't") + } +} diff --git a/backend/internal/database/migrations/0035_weekly_report.sql b/backend/internal/database/migrations/0035_weekly_report.sql index 366d932..8f76b80 100644 --- a/backend/internal/database/migrations/0035_weekly_report.sql +++ b/backend/internal/database/migrations/0035_weekly_report.sql @@ -1,4 +1,4 @@ -CREATE TABLE weekly_reports ( +CREATE TABLE IF NOT EXISTS weekly_reports ( report_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, project_id INTEGER NOT NULL, diff --git a/backend/internal/handlers/handlers_user_related.go b/backend/internal/handlers/handlers_user_related.go index e90abd0..0619ea5 100644 --- a/backend/internal/handlers/handlers_user_related.go +++ b/backend/internal/handlers/handlers_user_related.go @@ -22,13 +22,16 @@ import ( func (gs *GState) Register(c *fiber.Ctx) error { u := new(types.NewUser) if err := c.BodyParser(u); err != nil { + println("Error parsing body") return c.Status(400).SendString(err.Error()) } + println("Adding user:", u.Username) if err := gs.Db.AddUser(u.Username, u.Password); err != nil { return c.Status(500).SendString(err.Error()) } + println("User added:", u.Username) return c.Status(200).SendString("User added") } @@ -57,9 +60,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 +79,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}) } diff --git a/backend/internal/types/WeeklyReport.go b/backend/internal/types/WeeklyReport.go index b704cc8..299395a 100644 --- a/backend/internal/types/WeeklyReport.go +++ b/backend/internal/types/WeeklyReport.go @@ -41,4 +41,6 @@ type WeeklyReport struct { StudyTime int `json:"studyTime" db:"study_time"` // Total time spent on testing TestingTime int `json:"testingTime" db:"testing_time"` + // The project manager who signed it + SignedBy *int `json:"signedBy" db:"signed_by"` } 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"); diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index d87594c..cfd5b61 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -7,7 +7,7 @@ import { } from "../Types/goTypes"; // This type of pattern should be hard to misuse -interface APIResponse { +export interface APIResponse { success: boolean; message?: string; data?: T; diff --git a/frontend/src/Components/LoginCheck.tsx b/frontend/src/Components/LoginCheck.tsx index af45d0f..3658cbf 100644 --- a/frontend/src/Components/LoginCheck.tsx +++ b/frontend/src/Components/LoginCheck.tsx @@ -1,34 +1,54 @@ import { NewUser } from "../Types/goTypes"; +import { api, APIResponse } from "../API/API"; +import { Dispatch, SetStateAction } from "react"; -function LoginCheck(props: { username: string; password: string }): number { - //Example users for testing without backend, remove when using backend - const admin: NewUser = { - username: "admin", - password: "123", - }; - const pmanager: NewUser = { - username: "pmanager", - password: "123", - }; +/* + * Checks if user is in database with api.login and then sets proper authority level + * TODO: change so that it checks for user type (admin, user, pm) somehow instead + **/ +function LoginCheck(props: { + username: string; + password: string; + setAuthority: Dispatch>; +}): number { const user: NewUser = { - username: "user", - password: "123", + username: props.username, + password: props.password, }; + api + .login(user) + .then((response: APIResponse) => { + if (response.success) { + if (response.data !== undefined) { + const token = response.data; + //TODO: change so that it checks for user type (admin, user, pm) instead + if (token !== "" && props.username === "admin") { + props.setAuthority((prevAuth) => { + prevAuth = 1; + return prevAuth; + }); + } else if (token !== "" && props.username === "pm") { + props.setAuthority((prevAuth) => { + prevAuth = 2; + return prevAuth; + }); + } else if (token !== "" && props.username === "user") { + props.setAuthority((prevAuth) => { + prevAuth = 3; + return prevAuth; + }); + } + } else { + console.error("Token was undefined"); + } + } else { + console.error("Token could not be fetched"); + } + }) + .catch((error) => { + console.error("An error occurred during login:", error); + }); - //TODO: Compare with db instead when finished - if (props.username === admin.username && props.password === admin.password) { - return 1; - } else if ( - props.username === pmanager.username && - props.password === pmanager.password - ) { - return 2; - } else if ( - props.username === user.username && - props.password === user.password - ) { - return 3; - } return 0; } diff --git a/frontend/src/Pages/LoginPage.tsx b/frontend/src/Pages/LoginPage.tsx index f58f3e0..1ff8c9c 100644 --- a/frontend/src/Pages/LoginPage.tsx +++ b/frontend/src/Pages/LoginPage.tsx @@ -15,9 +15,10 @@ function LoginPage(props: { and if so, redirect to correct page */ function handleSubmit(event: FormEvent): void { event.preventDefault(); - props.setAuthority((prevAuth) => { - prevAuth = LoginCheck({ username: username, password: password }); - return prevAuth; + LoginCheck({ + username: username, + password: password, + setAuthority: props.setAuthority, }); } diff --git a/frontend/src/Pages/ProjectManagerPages/PMProjectMembers.tsx b/frontend/src/Pages/ProjectManagerPages/PMProjectMembers.tsx index 2af1145..bbafd6a 100644 --- a/frontend/src/Pages/ProjectManagerPages/PMProjectMembers.tsx +++ b/frontend/src/Pages/ProjectManagerPages/PMProjectMembers.tsx @@ -14,6 +14,7 @@ function PMProjectMembers(): JSX.Element { onClick={(): void => { return; }} + type={"button"} /> @@ -22,6 +23,7 @@ function PMProjectMembers(): JSX.Element { onClick={(): void => { return; }} + type={"button"} />