From 47d4bda99bdc7d171d9b65ba187888a6d63fc1bf Mon Sep 17 00:00:00 2001 From: al8763be Date: Wed, 17 Apr 2024 21:31:47 +0200 Subject: [PATCH 1/4] ChangeProjectName Handler + API --- backend/internal/database/db.go | 7 ++++ backend/internal/database/db_test.go | 28 +++++++++++++ .../handlers/projects/ChangeProjectName.go | 42 +++++++++++++++++++ backend/main.go | 1 + frontend/src/API/API.ts | 39 +++++++++++++++++ testing/helpers.py | 13 +++++- testing/testing.py | 38 +++++++++++++++++ 7 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 backend/internal/handlers/projects/ChangeProjectName.go diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index d444b85..607f56d 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -47,6 +47,7 @@ type Database interface { GetUserName(id int) (string, error) UnsignWeeklyReport(reportId int, projectManagerId int) error DeleteReport(reportID int) error + ChangeProjectName(projectName string, newProjectName string) error } // This struct is a wrapper type that holds the database connection @@ -670,3 +671,9 @@ func (d *Db) DeleteReport(reportID int) error { _, err := d.Exec("DELETE FROM weekly_reports WHERE report_id = ?", reportID) return err } + +// ChangeProjectName is a handler that changes the name of a project +func (d *Db) ChangeProjectName(projectName string, newProjectName string) error { + _, err := d.Exec("UPDATE projects SET name = ? WHERE name = ?", newProjectName, projectName) + return err +} diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index f24175a..cc76133 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -1092,3 +1092,31 @@ func TestDeleteReport(t *testing.T) { } } + +func TestChangeProjectName(t *testing.T) { + db, err := setupAdvancedState() + if err != nil { + t.Error("setupState failed:", err) + } + + // Promote user to Admin + err = db.PromoteToAdmin("demouser") + if err != nil { + t.Error("PromoteToAdmin failed:", err) + } + + // Change project name + err = db.ChangeProjectName("projecttest", "newprojectname") + if err != nil { + t.Error("ChangeProjectName failed:", err) + } + + // Check if the project name was changed + projects, err := db.GetAllProjects() + if err != nil { + t.Error("GetAllProjects failed:", err) + } + if projects[0].Name != "newprojectname" { + t.Error("ChangeProjectName failed: expected newprojectname, got", projects[0].Name) + } +} diff --git a/backend/internal/handlers/projects/ChangeProjectName.go b/backend/internal/handlers/projects/ChangeProjectName.go new file mode 100644 index 0000000..82b3322 --- /dev/null +++ b/backend/internal/handlers/projects/ChangeProjectName.go @@ -0,0 +1,42 @@ +package projects + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// ChangeProjectName is a handler that changes the name of a project +func ChangeProjectName(c *fiber.Ctx) error { + + //check token and get username of current user + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Extract the necessary parameters from the request + projectName := c.Params("projectName") + newProjectName := c.Query("newProjectName") + + // Check if user is project manager + ismanager, err := db.GetDb(c).IsProjectManager(username, projectName) + if err != nil { + log.Warn("Error checking if projectmanager:", err) + return c.Status(500).SendString(err.Error()) + } else if !ismanager { + log.Warn("User is not projectmanager") + return c.Status(401).SendString("User is not projectmanager") + } + + // Perform the project name change + err = db.GetDb(c).ChangeProjectName(projectName, newProjectName) + if err != nil { + log.Warn("Error changing project name:", err) + return c.Status(500).SendString(err.Error()) + } + + // Return a success message + return c.Status(200).SendString("Project name changed successfully") +} diff --git a/backend/main.go b/backend/main.go index 8a3466a..2d496c4 100644 --- a/backend/main.go +++ b/backend/main.go @@ -125,6 +125,7 @@ func main() { api.Delete("/removeUserFromProject/:projectName", projects.RemoveUserFromProject) api.Delete("/removeProject/:projectName", projects.RemoveProject) api.Delete("/project/:projectID", projects.DeleteProject) + api.Put("/ChangeProjectName/:projectName", projects.ChangeProjectName) // All report related routes // reportGroup := api.Group("/report") // Not currently in use diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index fea7d95..6183d71 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -271,6 +271,18 @@ interface API { token: string, userName?: string, ): Promise>; + + /** + * Changes the name of a project + * @param {string} projectName The name of the project + * @param {string} newProjectName The new name of the project + * @param {string} token The authentication token + */ + changeProjectName( + projectName: string, + newProjectName: string, + token: string, + ): Promise>; } /** An instance of the API */ @@ -1002,4 +1014,31 @@ export const api: API = { return { success: false, message: "Failed to get statistics" }; } }, + + async changeProjectName( + projectName: string, + newProjectName: string, + token: string, + ): Promise> { + try { + const response = await fetch( + `/api/changeProjectName/${projectName}?newProjectName=${newProjectName}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }, + ); + + if (!response.ok) { + return { success: false, message: "Failed to change project name" }; + } else { + return { success: true, message: "Project name changed" }; + } + } catch (e) { + return { success: false, message: "Failed to change project name" }; + } + }, }; diff --git a/testing/helpers.py b/testing/helpers.py index 8d6b148..57e5710 100644 --- a/testing/helpers.py +++ b/testing/helpers.py @@ -37,8 +37,9 @@ promoteToPmPath = base_url + "/api/promoteToPm" unsignReportPath = base_url + "/api/unsignReport" deleteReportPath = base_url + "/api/deleteReport" getStatisticsPath = base_url + "/api/getStatistics" +changeProjectNamePath = base_url + "/api/changeProjectName" -debug_output = False +debug_output = True def gprint(*args, **kwargs): @@ -170,4 +171,12 @@ def getStatistics(token: string, projectName: string): headers = {"Authorization": "Bearer " + token}, params={"projectName": projectName} ) - return response.json() \ No newline at end of file + return response.json() + +def changeProjectName(token: string, projectName: string, newProjectName: string): + response = requests.put( + changeProjectNamePath + "/" + projectName, + headers = {"Authorization": "Bearer " + token}, + params={"newProjectName": newProjectName} + ) + return response \ No newline at end of file diff --git a/testing/testing.py b/testing/testing.py index daad215..08a4a73 100644 --- a/testing/testing.py +++ b/testing/testing.py @@ -666,8 +666,46 @@ def test_get_statistics(): assert stats["totalDevelopmentTime"] == 20, "Total development time is not correct" gprint("test_get_statistics successful") +def test_project_name_change(): + # Create admin + admin_username = randomString() + admin_password = randomString() + + project_name = "project" + randomString() + + token = register_and_login(admin_username, admin_password) + + response = create_project(token, project_name) + assert response.status_code == 200, "Create project failed" + + response = requests.get( + getUserProjectsPath + "/" + admin_username, + headers={"Authorization": "Bearer " + token}, + ) + + dprint(response.json()) + + new_project_name = "new project name " + randomString() + dprint("Changing project name from ", project_name, " to ", new_project_name) + response = changeProjectName(token, project_name, new_project_name) + + response = requests.get( + getUserProjectsPath + "/" + admin_username, + headers={"Authorization": "Bearer " + token}, + ) + + dprint(response.json()) + + if (response.json()[0]["name"] != new_project_name): + assert False, "Project name change failed" + + + assert response.status_code == 200, "Project name change failed" + gprint("test_projectNameChange successful") + if __name__ == "__main__": + test_project_name_change(); test_delete_report() test_unsign_report() test_promote_to_manager() From 0176f780671552b23337ced13c5a39e239b2d258 Mon Sep 17 00:00:00 2001 From: al8763be Date: Wed, 17 Apr 2024 22:04:23 +0200 Subject: [PATCH 2/4] ChangeUserPassword Handler + API --- backend/internal/database/db.go | 6 +++ backend/internal/database/db_test.go | 22 ++++++++++ .../handlers/projects/ChangeProjectName.go | 13 +++--- .../handlers/users/ChangeUserPassword.go | 42 +++++++++++++++++++ backend/main.go | 1 + frontend/src/API/API.ts | 39 +++++++++++++++++ testing/helpers.py | 11 ++++- testing/testing.py | 36 ++++++++++++++++ 8 files changed, 163 insertions(+), 7 deletions(-) create mode 100644 backend/internal/handlers/users/ChangeUserPassword.go diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 607f56d..12b0ee1 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -48,6 +48,7 @@ type Database interface { UnsignWeeklyReport(reportId int, projectManagerId int) error DeleteReport(reportID int) error ChangeProjectName(projectName string, newProjectName string) error + ChangeUserPassword(username string, password string) error } // This struct is a wrapper type that holds the database connection @@ -677,3 +678,8 @@ func (d *Db) ChangeProjectName(projectName string, newProjectName string) error _, err := d.Exec("UPDATE projects SET name = ? WHERE name = ?", newProjectName, projectName) return err } + +func (d *Db) ChangeUserPassword(username string, password string) error { + _, err := d.Exec("UPDATE users SET password = ? WHERE username = ?", password, username) + return err +} diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index cc76133..7b599f2 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -1120,3 +1120,25 @@ func TestChangeProjectName(t *testing.T) { t.Error("ChangeProjectName failed: expected newprojectname, got", projects[0].Name) } } + +func TestChangeUserPassword(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + // Add a user + _ = db.AddUser("testuser", "password") + + // Change user password + err = db.ChangeUserPassword("testuser", "newpassword") + if err != nil { + t.Error("ChangeUserPassword failed:", err) + } + + // Check if the password was changed + if !db.CheckUser("testuser", "newpassword") { + t.Error("ChangeUserPassword failed: password not changed") + } + +} diff --git a/backend/internal/handlers/projects/ChangeProjectName.go b/backend/internal/handlers/projects/ChangeProjectName.go index 82b3322..f6831db 100644 --- a/backend/internal/handlers/projects/ChangeProjectName.go +++ b/backend/internal/handlers/projects/ChangeProjectName.go @@ -20,16 +20,17 @@ func ChangeProjectName(c *fiber.Ctx) error { projectName := c.Params("projectName") newProjectName := c.Query("newProjectName") - // Check if user is project manager - ismanager, err := db.GetDb(c).IsProjectManager(username, projectName) + // Check if user is site admin + issiteadmin, err := db.GetDb(c).IsSiteAdmin(username) if err != nil { - log.Warn("Error checking if projectmanager:", err) + log.Warn("Error checking if siteadmin:", err) return c.Status(500).SendString(err.Error()) - } else if !ismanager { - log.Warn("User is not projectmanager") - return c.Status(401).SendString("User is not projectmanager") + } else if !issiteadmin { + log.Warn("User is not siteadmin") + return c.Status(401).SendString("User is not siteadmin") } + // Perform the project name change err = db.GetDb(c).ChangeProjectName(projectName, newProjectName) if err != nil { diff --git a/backend/internal/handlers/users/ChangeUserPassword.go b/backend/internal/handlers/users/ChangeUserPassword.go new file mode 100644 index 0000000..1596247 --- /dev/null +++ b/backend/internal/handlers/users/ChangeUserPassword.go @@ -0,0 +1,42 @@ +package users + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// ChangeUserPassword is a handler that changes the password of a user +func ChangeUserPassword(c *fiber.Ctx) error { + + //Check token and get username of current user + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + admin := claims["name"].(string) + + // Extract the necessary parameters from the request + username := c.Params("username") + newPassword := c.Query("newPassword") + + // Check if user is site admin + issiteadmin, err := db.GetDb(c).IsSiteAdmin(admin) + if err != nil { + log.Warn("Error checking if siteadmin:", err) + return c.Status(500).SendString(err.Error()) + } else if !issiteadmin { + log.Warn("User is not siteadmin") + return c.Status(401).SendString("User is not siteadmin") + } + + // Perform the password change + err = db.GetDb(c).ChangeUserPassword(username, newPassword) + if err != nil { + log.Warn("Error changing password:", err) + return c.Status(500).SendString(err.Error()) + } + + // Return a success message + return c.Status(200).SendString("Password changed successfully") +} diff --git a/backend/main.go b/backend/main.go index 2d496c4..282f2c2 100644 --- a/backend/main.go +++ b/backend/main.go @@ -110,6 +110,7 @@ func main() { api.Post("/promoteToAdmin", users.PromoteToAdmin) api.Put("/changeUserName", users.ChangeUserName) api.Delete("/userdelete/:username", users.UserDelete) // Perhaps just use POST to avoid headaches + api.Put("/changeUserPassword/:username", users.ChangeUserPassword) // All project related routes // projectGroup := api.Group("/project") // Not currently in use diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index 6183d71..29789c4 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -283,6 +283,18 @@ interface API { newProjectName: string, token: string, ): Promise>; + + /** + * Changes the password of a user + * @param {string} username The username of the user + * @param {string} newPassword The new password + * @param {string} token The authentication token + */ + changeUserPassword( + username: string, + newPassword: string, + token: string, + ): Promise>; } /** An instance of the API */ @@ -1041,4 +1053,31 @@ export const api: API = { return { success: false, message: "Failed to change project name" }; } }, + + async changeUserPassword( + username: string, + newPassword: string, + token: string, + ): Promise> { + try { + const response = await fetch( + `/api/changePassword/${username}?newPassword=${newPassword}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }, + ); + + if (!response.ok) { + return { success: false, message: "Failed to change password" }; + } else { + return { success: true, message: "Password changed" }; + } + } catch (e) { + return { success: false, message: "Failed to change password" }; + } + }, }; diff --git a/testing/helpers.py b/testing/helpers.py index 57e5710..634933a 100644 --- a/testing/helpers.py +++ b/testing/helpers.py @@ -38,8 +38,9 @@ unsignReportPath = base_url + "/api/unsignReport" deleteReportPath = base_url + "/api/deleteReport" getStatisticsPath = base_url + "/api/getStatistics" changeProjectNamePath = base_url + "/api/changeProjectName" +changeUserPasswordPath = base_url + "/api/changeUserPassword" -debug_output = True +debug_output = False def gprint(*args, **kwargs): @@ -179,4 +180,12 @@ def changeProjectName(token: string, projectName: string, newProjectName: string headers = {"Authorization": "Bearer " + token}, params={"newProjectName": newProjectName} ) + return response + +def changeUserPassword(token: string, username: string, newPassword: string): + response = requests.put( + changeUserPasswordPath + "/" + username, + headers = {"Authorization": "Bearer " + token}, + params={"newPassword": newPassword} + ) return response \ No newline at end of file diff --git a/testing/testing.py b/testing/testing.py index 08a4a73..6baf8f0 100644 --- a/testing/testing.py +++ b/testing/testing.py @@ -675,6 +675,14 @@ def test_project_name_change(): token = register_and_login(admin_username, admin_password) + # Promote to admin + response = requests.post( + promoteToAdminPath, + json={"username": admin_username}, + headers={"Authorization": "Bearer " + token}, + ) + + response = create_project(token, project_name) assert response.status_code == 200, "Create project failed" @@ -703,8 +711,36 @@ def test_project_name_change(): assert response.status_code == 200, "Project name change failed" gprint("test_projectNameChange successful") +def test_change_user_password(): + # Create admin + admin_username = randomString() + admin_password = randomString() + + user = randomString() + password = randomString() + + token = register_and_login(admin_username, admin_password) + + # Promote to admin + response = requests.post( + promoteToAdminPath, + json={"username": admin_username}, + headers={"Authorization": "Bearer " + token}, + ) + + _ = register_and_login(user, password) + + response = changeUserPassword(token, user, "new_password") + assert response.status_code == 200, "Change user password failed" + + response = login(user, "new_password") + assert response.status_code == 200, "Login failed with new password" + + gprint("test_change_user_password successful") + if __name__ == "__main__": + test_change_user_password() test_project_name_change(); test_delete_report() test_unsign_report() From 8948067514dc0cf4c39a23ba89a67a3ef850a83e Mon Sep 17 00:00:00 2001 From: Peter KW Date: Wed, 17 Apr 2024 23:03:32 +0200 Subject: [PATCH 3/4] ChangeProjectName.tsx created and implemented + minor fix in main.go api paths --- backend/main.go | 2 +- frontend/src/Components/ChangeProjectName.tsx | 36 +++++++++++++++++++ frontend/src/Components/ProjectInfoModal.tsx | 8 +++-- 3 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 frontend/src/Components/ChangeProjectName.tsx diff --git a/backend/main.go b/backend/main.go index 282f2c2..0c66369 100644 --- a/backend/main.go +++ b/backend/main.go @@ -126,7 +126,7 @@ func main() { api.Delete("/removeUserFromProject/:projectName", projects.RemoveUserFromProject) api.Delete("/removeProject/:projectName", projects.RemoveProject) api.Delete("/project/:projectID", projects.DeleteProject) - api.Put("/ChangeProjectName/:projectName", projects.ChangeProjectName) + api.Put("/changeProjectName/:projectName", projects.ChangeProjectName) // All report related routes // reportGroup := api.Group("/report") // Not currently in use diff --git a/frontend/src/Components/ChangeProjectName.tsx b/frontend/src/Components/ChangeProjectName.tsx new file mode 100644 index 0000000..8348217 --- /dev/null +++ b/frontend/src/Components/ChangeProjectName.tsx @@ -0,0 +1,36 @@ +import { APIResponse, api } from "../API/API"; + +/** + * Changes the name of a project + * @param {string} props.projectName - Current project name + * @param {string} props.newProjectName - New project name + * @returns {void} - Nothing + */ +export default function ChangeProjectName(props: { + projectName: string; + newProjectName: string; +}): void { + if (props.projectName === "" || props.projectName === props.newProjectName) { + alert("You have to give a new name\n\nName not changed"); + return; + } + api + .changeProjectName( + props.projectName, + props.newProjectName, + localStorage.getItem("accessToken") ?? "", + ) + .then((response: APIResponse) => { + if (response.success) { + alert("Name changed successfully"); + location.reload(); + } else { + alert("Name not changed, name could be taken"); + console.error(response.message); + } + }) + .catch((error) => { + alert("Name not changed"); + console.error("An error occurred during change:", error); + }); +} diff --git a/frontend/src/Components/ProjectInfoModal.tsx b/frontend/src/Components/ProjectInfoModal.tsx index 71a72fb..4be3397 100644 --- a/frontend/src/Components/ProjectInfoModal.tsx +++ b/frontend/src/Components/ProjectInfoModal.tsx @@ -8,6 +8,7 @@ import InputField from "./InputField"; import ProjectNameInput from "./Inputs/ProjectNameInput"; import { alphanumeric } from "../Data/regex"; import { projNameHighLimit, projNameLowLimit } from "../Data/constants"; +import ChangeProjectName from "./ChangeProjectName"; function ProjectInfoModal(props: { projectname: string; @@ -50,9 +51,10 @@ function ProjectInfoModal(props: { `Are you sure you want to change name of ${props.projectname} to ${newProjName}?`, ) ) { - //TODO: change and insert change name functionality - alert("Not implemented yet"); - setNewProjName(""); + ChangeProjectName({ + projectName: props.projectname, + newProjectName: newProjName, + }); } else { alert("Name was not changed!"); } From 3be2319bce9cb6eb57ce29a7e10b619365e5c6e9 Mon Sep 17 00:00:00 2001 From: Peter KW Date: Wed, 17 Apr 2024 23:04:27 +0200 Subject: [PATCH 4/4] ChangeUserPassword.tsx created and implemented + minor fix in API.ts --- frontend/src/API/API.ts | 2 +- .../src/Components/ChangeUserPassword.tsx | 36 +++++++++++++++++++ frontend/src/Components/UserInfoModal.tsx | 8 +++-- 3 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 frontend/src/Components/ChangeUserPassword.tsx diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index 4ebd859..d330f6d 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -1065,7 +1065,7 @@ export const api: API = { ): Promise> { try { const response = await fetch( - `/api/changePassword/${username}?newPassword=${newPassword}`, + `/api/changeUserPassword/${username}?newPassword=${newPassword}`, { method: "PUT", headers: { diff --git a/frontend/src/Components/ChangeUserPassword.tsx b/frontend/src/Components/ChangeUserPassword.tsx new file mode 100644 index 0000000..0bc5222 --- /dev/null +++ b/frontend/src/Components/ChangeUserPassword.tsx @@ -0,0 +1,36 @@ +import { APIResponse, api } from "../API/API"; + +/** + * Changes the password of a user + * @param {string} props.username - The username of the user + * @param {string} props.newPassword - The new password + * @returns {void} - Nothing + */ +export default function ChangeUserPassword(props: { + username: string; + newPassword: string; +}): void { + if (props.username === localStorage.getItem("username")) { + alert("You cannot change admin password"); + return; + } + api + .changeUserPassword( + props.username, + props.newPassword, + localStorage.getItem("accessToken") ?? "", + ) + .then((response: APIResponse) => { + if (response.success) { + alert("Password changed successfully"); + location.reload(); + } else { + alert("Password not changed"); + console.error(response.message); + } + }) + .catch((error) => { + alert("Password not changed"); + console.error("An error occurred during change:", error); + }); +} diff --git a/frontend/src/Components/UserInfoModal.tsx b/frontend/src/Components/UserInfoModal.tsx index 764d176..460ef85 100644 --- a/frontend/src/Components/UserInfoModal.tsx +++ b/frontend/src/Components/UserInfoModal.tsx @@ -12,6 +12,7 @@ import { usernameLowLimit, usernameUpLimit, } from "../Data/constants"; +import ChangeUserPassword from "./ChangeUserPassword"; function UserInfoModal(props: { isVisible: boolean; @@ -94,9 +95,10 @@ function UserInfoModal(props: { if ( confirm(`Are you sure you want to change password of ${props.username}?`) ) { - //TODO: insert change password functionality - alert("Not implemented yet"); - setNewPassword(""); + ChangeUserPassword({ + username: props.username, + newPassword: newPassword, + }); } else { alert("Password was not changed!"); }