diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 6bf6fba..b8e7205 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -17,6 +17,7 @@ type Database interface { AddUser(username string, password string) error CheckUser(username string, password string) bool RemoveUser(username string) error + RemoveUserFromProject(username string, projectname string) error PromoteToAdmin(username string) error GetUserId(username string) (int, error) AddProject(name string, description string, username string) error @@ -43,6 +44,7 @@ type Database interface { GetProjectTimes(projectName string) (map[string]int, error) UpdateWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error RemoveProject(projectname string) error + GetUserName(id int) (string, error) } // This struct is a wrapper type that holds the database connection @@ -86,6 +88,10 @@ const isProjectManagerQuery = `SELECT COUNT(*) > 0 FROM user_roles JOIN projects ON user_roles.project_id = projects.id WHERE users.username = ? AND projects.name = ? AND user_roles.p_role = 'project_manager'` +const removeUserFromProjectQuery = `DELETE FROM user_roles + WHERE user_id = (SELECT id FROM users WHERE username = ?) + AND project_id = (SELECT id FROM projects WHERE name = ?)` + // DbConnect connects to the database func DbConnect(dbpath string) Database { // Open the database @@ -147,6 +153,11 @@ func (d *Db) AddUserToProject(username string, projectname string, role string) return err } +func (d *Db) RemoveUserFromProject(username string, projectname string) error { + _, err := d.Exec(removeUserFromProjectQuery, username, projectname) + return err +} + // ChangeUserRole changes the role of a user within a project. func (d *Db) ChangeUserRole(username string, projectname string, role string) error { // Execute the SQL query to change the user's role @@ -601,3 +612,9 @@ func (d *Db) RemoveProject(projectname string) error { _, err := d.Exec("DELETE FROM projects WHERE name = ?", projectname) return err } + +func (d *Db) GetUserName(id int) (string, error) { + var username string + err := d.Get(&username, "SELECT username FROM users WHERE id = ?", id) + return username, err +} diff --git a/backend/internal/handlers/projects/RemoveUserFromProject.go b/backend/internal/handlers/projects/RemoveUserFromProject.go new file mode 100644 index 0000000..7aefcf8 --- /dev/null +++ b/backend/internal/handlers/projects/RemoveUserFromProject.go @@ -0,0 +1,40 @@ +package projects + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +func RemoveUserFromProject(c *fiber.Ctx) error { + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + pm_name := claims["name"].(string) + + project := c.Params("projectName") + username := c.Query("userName") + + // Check if the user is a project manager + isPM, err := db.GetDb(c).IsProjectManager(pm_name, project) + if err != nil { + log.Info("Error checking if user is project manager:", err) + return c.Status(500).SendString(err.Error()) + } + + if !isPM { + log.Info("User: ", pm_name, " is not a project manager in project: ", project) + return c.Status(403).SendString("User is not a project manager") + } + + // Remove the user from the project + if err = db.GetDb(c).RemoveUserFromProject(username, project); err != nil { + log.Info("Error removing user from project:", err) + return c.Status(500).SendString(err.Error()) + } + + // Return success message + log.Info("User : ", username, " removed from project: ", project) + return c.SendStatus(fiber.StatusOK) +} diff --git a/backend/internal/handlers/users/GetUserName.go b/backend/internal/handlers/users/GetUserName.go new file mode 100644 index 0000000..82b6cc8 --- /dev/null +++ b/backend/internal/handlers/users/GetUserName.go @@ -0,0 +1,32 @@ +package users + +import ( + "strconv" + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" +) + +// Return the username of a user given their user id +func GetUserName(c *fiber.Ctx) error { + // Check the query params for userId + user_id_string := c.Query("userId") + if user_id_string == "" { + return c.Status(400).SendString("Missing user id") + } + + // Convert to int + user_id, err := strconv.Atoi(user_id_string) + if err != nil { + return c.Status(400).SendString("Invalid user id") + } + + // Get the username from the database + username, err := db.GetDb(c).GetUserName(user_id) + if err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Send the nuclear launch codes to north korea + return c.JSON(fiber.Map{"username": username}) +} diff --git a/backend/main.go b/backend/main.go index b5ecacf..7b19dd9 100644 --- a/backend/main.go +++ b/backend/main.go @@ -103,6 +103,7 @@ func main() { // userGroup := api.Group("/user") // Not currently in use api.Get("/users/all", users.ListAllUsers) api.Get("/project/getAllUsers", users.GetAllUsersProject) + api.Get("/username", users.GetUserName) api.Post("/login", users.Login) api.Post("/register", users.Register) api.Post("/loginrenew", users.LoginRenew) @@ -121,6 +122,7 @@ func main() { api.Post("/ProjectRoleChange", projects.ProjectRoleChange) api.Put("/promoteToPm/:projectName", projects.PromoteToPm) api.Put("/addUserToProject/:projectName", projects.AddUserToProjectHandler) + api.Delete("/removeUserFromProject/:projectName", projects.RemoveUserFromProject) api.Delete("/removeProject/:projectName", projects.RemoveProject) api.Delete("/project/:projectID", projects.DeleteProject) diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index b3c6eae..86ad6dc 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -1,4 +1,4 @@ -import { NewProjMember } from "../Components/AddMember"; +import { AddMemberInfo } from "../Components/AddMember"; import { ProjectRoleChange } from "../Components/ChangeRole"; import { projectTimes } from "../Components/GetProjectTimes"; import { ProjectMember } from "../Components/GetUsersInProject"; @@ -197,7 +197,13 @@ interface API { ): Promise>; addUserToProject( - user: NewProjMember, + addMemberInfo: AddMemberInfo, + token: string, + ): Promise>; + + removeUserFromProject( + user: string, + project: string, token: string, ): Promise>; @@ -227,6 +233,12 @@ interface API { projectName: string, token: string, ): Promise>; + /** + * Get the username from the id + * @param {number} id The id of the user + * @param {string} token Your token + */ + getUsername(id: number, token: string): Promise>; } /** An instance of the API */ @@ -336,18 +348,20 @@ export const api: API = { }, async addUserToProject( - user: NewProjMember, + addMemberInfo: AddMemberInfo, token: string, ): Promise> { try { - const response = await fetch("/api/addUserToProject", { - method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, + const response = await fetch( + `/api/addUserToProject/${addMemberInfo.projectName}/?userName=${addMemberInfo.userName}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, }, - body: JSON.stringify(user), - }); + ); if (!response.ok) { return { success: false, message: "Failed to add member" }; @@ -359,6 +373,31 @@ export const api: API = { } }, + async removeUserFromProject( + user: string, + project: string, + token: string, + ): Promise> { + try { + const response = await fetch( + `/api/removeUserFromProject/${project}?userName=${user}`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }, + ); + if (!response.ok) { + return { success: false, message: "Failed to remove member" }; + } + } catch (e) { + return { success: false, message: "Failed to remove member" }; + } + return { success: true, message: "Removed member" }; + }, + async renewToken(token: string): Promise> { try { const response = await fetch("/api/loginrenew", { @@ -837,4 +876,25 @@ export const api: API = { } return { success: true, message: "User promoted to project manager" }; }, + + async getUsername(id: number, token: string): Promise> { + try { + const response = await fetch(`/api/username?userId=${id}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }); + + if (!response.ok) { + return { success: false, message: "Failed to get username" }; + } else { + const data = (await response.json()) as string; + return { success: true, data }; + } + } catch (e) { + return { success: false, message: "Failed to get username" }; + } + }, }; diff --git a/frontend/src/Components/AddMember.tsx b/frontend/src/Components/AddMember.tsx index 194afe8..d8036b7 100644 --- a/frontend/src/Components/AddMember.tsx +++ b/frontend/src/Components/AddMember.tsx @@ -1,44 +1,35 @@ -import { APIResponse, api } from "../API/API"; +import { api } from "../API/API"; -export interface NewProjMember { - username: string; - role: string; - projectname: string; +export interface AddMemberInfo { + userName: string; + projectName: string; } /** * Tries to add a member to a project - * @param {Object} props - A NewProjMember - * @returns {boolean} True if added, false if not + * @param {AddMemberInfo} props.membertoAdd - Contains user's name and project's name + * @returns {Promise} */ -function AddMember(props: { memberToAdd: NewProjMember }): boolean { - let added = false; - if ( - props.memberToAdd.username === "" || - props.memberToAdd.role === "" || - props.memberToAdd.projectname === "" - ) { - alert("All fields must be filled before adding"); - return added; +async function AddMember(props: { memberToAdd: AddMemberInfo }): Promise { + if (props.memberToAdd.userName === "") { + alert("You must choose at least one user to add"); + return; } - api - .addUserToProject( + try { + const response = await api.addUserToProject( props.memberToAdd, localStorage.getItem("accessToken") ?? "", - ) - .then((response: APIResponse) => { - if (response.success) { - alert("Member added"); - added = true; - } else { - alert("Member not added"); - console.error(response.message); - } - }) - .catch((error) => { - console.error("An error occurred during member add:", error); - }); - return added; + ); + if (response.success) { + alert(`[${props.memberToAdd.userName}] added`); + } else { + alert(`[${props.memberToAdd.userName}] not added`); + console.error(response.message); + } + } catch (error) { + alert(`[${props.memberToAdd.userName}] not added`); + console.error("An error occurred during member add:", error); + } } export default AddMember; diff --git a/frontend/src/Components/AddProject.tsx b/frontend/src/Components/AddProject.tsx index e2ad8b9..c8a1c66 100644 --- a/frontend/src/Components/AddProject.tsx +++ b/frontend/src/Components/AddProject.tsx @@ -1,37 +1,10 @@ import { useState } from "react"; -import { APIResponse, api } from "../API/API"; +import { api } from "../API/API"; import { NewProject } from "../Types/goTypes"; import InputField from "./InputField"; import Logo from "../assets/Logo.svg"; import Button from "./Button"; -/** - * Tries to add a project to the system - * @param {Object} props - Project name and description - * @returns {boolean} True if created, false if not - */ -function CreateProject(props: { name: string; description: string }): void { - const project: NewProject = { - name: props.name, - description: props.description, - }; - - api - .createProject(project, localStorage.getItem("accessToken") ?? "") - .then((response: APIResponse) => { - if (response.success) { - alert("Project added!"); - } else { - alert("Project NOT added!"); - console.error(response.message); - } - }) - .catch((error) => { - alert("Project NOT added!"); - console.error("An error occurred during creation:", error); - }); -} - /** * Provides UI for adding a project to the system. * @returns {JSX.Element} - Returns the component UI for adding a project @@ -40,6 +13,33 @@ function AddProject(): JSX.Element { const [name, setName] = useState(""); const [description, setDescription] = useState(""); + /** + * Tries to add a project to the system + */ + const handleCreateProject = async (): Promise => { + const project: NewProject = { + name: name.replace(/ /g, ""), + description: description.trim(), + }; + try { + const response = await api.createProject( + project, + localStorage.getItem("accessToken") ?? "", + ); + if (response.success) { + alert(`${project.name} added!`); + setDescription(""); + setName(""); + } else { + alert("Project not added, name could be taken"); + console.error(response.message); + } + } catch (error) { + alert("Project not added"); + console.error(error); + } + }; + return (
@@ -47,10 +47,7 @@ function AddProject(): JSX.Element { className="bg-white rounded px-8 pt-6 pb-8 mb-4 items-center justify-center flex flex-col w-fit h-fit" onSubmit={(e) => { e.preventDefault(); - CreateProject({ - name: name, - description: description, - }); + void handleCreateProject(); }} > ([]); const [users, setUsers] = useState([]); - const [role, setRole] = useState(""); - GetAllUsers({ setUsersProp: setUsers }); + const [usersProj, setUsersProj] = useState([]); - const handleClick = (): boolean => { - const newMember: NewProjMember = { - username: name, - projectname: props.projectName, - role: role, - }; - return AddMember({ memberToAdd: newMember }); + // Gets all users and project members for filtering + GetAllUsers({ setUsersProp: setUsers }); + GetUsersInProject({ + setUsersProp: setUsersProj, + projectName: props.projectName, + }); + /* + * Filters the members from users so that users who are already + * members are not shown + */ + useEffect(() => { + setUsers((prevUsers) => { + const filteredUsers = prevUsers.filter( + (user) => + !usersProj.some((projectUser) => projectUser.Username === user), + ); + return filteredUsers; + }); + }, [usersProj]); + + // Attempts to add all of the selected users to the project + const handleAddClick = async (): Promise => { + if (names.length === 0) + alert("You have to choose at least one user to add"); + for (const name of names) { + const newMember: AddMemberInfo = { + userName: name, + projectName: props.projectName, + }; + await AddMember({ memberToAdd: newMember }); + } + setNames([]); + location.reload(); + }; + + // Updates the names that have been selected + const handleUserClick = (user: string): void => { + setNames((prevNames): string[] => { + if (!prevNames.includes(user)) { + return [...prevNames, user]; + } + return prevNames.filter((name) => name !== user); + }); }; return ( -
-

- User chosen: [{name}] +

+

+ {props.projectName} +

+

+ Choose users to add:

-

- Role chosen: [{role}] -

-

- Project chosen: [{props.projectName}] -

-

Choose role:

-
-
    -
  • { - setRole("member"); - }} - > - {"Member"} -
  • -
  • { - setRole("project_manager"); - }} - > - {"Project manager"} -
  • -
-
-

Choose user:

-
+
    {users.map((user) => (
  • { - setName(user); + handleUserClick(user); }} > {user} @@ -73,13 +88,16 @@ function AddUserToProject(props: { projectName: string }): JSX.Element { ))}
-
+

+ Number of users to be added: {names.length} +

+
diff --git a/frontend/src/Components/ChangeRoleView.tsx b/frontend/src/Components/ChangeRoleView.tsx index 30dce3c..782ad8d 100644 --- a/frontend/src/Components/ChangeRoleView.tsx +++ b/frontend/src/Components/ChangeRoleView.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import Button from "./Button"; import ChangeRole, { ProjectRoleChange } from "./ChangeRole"; -export default function ChangeRoles(props: { +export default function ChangeRoleView(props: { projectName: string; username: string; }): JSX.Element { diff --git a/frontend/src/Components/ChangeUsername.tsx b/frontend/src/Components/ChangeUsername.tsx index 78d7da9..2f73bb6 100644 --- a/frontend/src/Components/ChangeUsername.tsx +++ b/frontend/src/Components/ChangeUsername.tsx @@ -2,8 +2,11 @@ import { APIResponse, api } from "../API/API"; import { StrNameChange } from "../Types/goTypes"; function ChangeUsername(props: { nameChange: StrNameChange }): void { - if (props.nameChange.newName === "") { - alert("You have to select a new name"); + if ( + props.nameChange.newName === "" || + props.nameChange.newName === props.nameChange.prevName + ) { + alert("You have to give a new name\n\nName not changed"); return; } api @@ -13,7 +16,7 @@ function ChangeUsername(props: { nameChange: StrNameChange }): void { alert("Name changed successfully"); location.reload(); } else { - alert("Name not changed"); + alert("Name not changed, name could be taken"); console.error(response.message); } }) diff --git a/frontend/src/Components/MemberInfoModal.tsx b/frontend/src/Components/MemberInfoModal.tsx index 8b32367..dac11f8 100644 --- a/frontend/src/Components/MemberInfoModal.tsx +++ b/frontend/src/Components/MemberInfoModal.tsx @@ -1,8 +1,8 @@ import Button from "./Button"; -import DeleteUser from "./DeleteUser"; import UserProjectListAdmin from "./UserProjectListAdmin"; import { useState } from "react"; import ChangeRoleView from "./ChangeRoleView"; +import RemoveUserFromProj from "./RemoveUserFromProj"; function MemberInfoModal(props: { projectName: string; @@ -20,7 +20,7 @@ function MemberInfoModal(props: { }; return (
@@ -42,13 +42,16 @@ function MemberInfoModal(props: {