diff --git a/backend/internal/database/sample_data/0010_sample_data.sql b/backend/internal/database/sample_data/0010_sample_data.sql index 092fbb0..ab74f1a 100644 --- a/backend/internal/database/sample_data/0010_sample_data.sql +++ b/backend/internal/database/sample_data/0010_sample_data.sql @@ -7,6 +7,8 @@ VALUES ("user", "123"); INSERT OR IGNORE INTO users(username, password) VALUES ("user2", "123"); +INSERT OR IGNORE INTO site_admin VALUES (1); + INSERT OR IGNORE INTO projects(name,description,owner_user_id) VALUES ("projecttest","test project", 1); diff --git a/backend/internal/handlers/handlers_project_related.go b/backend/internal/handlers/handlers_project_related.go index 603f4cd..e9ef966 100644 --- a/backend/internal/handlers/handlers_project_related.go +++ b/backend/internal/handlers/handlers_project_related.go @@ -44,10 +44,11 @@ func (gs *GState) DeleteProject(c *fiber.Ctx) error { // GetUserProjects returns all projects that the user is a member of func (gs *GState) GetUserProjects(c *fiber.Ctx) error { - // First we get the username from the token - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - username := claims["name"].(string) + username := c.Params("username") + if username == "" { + log.Info("No username provided") + return c.Status(400).SendString("No username provided") + } // Then dip into the database to get the projects projects, err := gs.Db.GetProjectsForUser(username) diff --git a/backend/internal/handlers/handlers_user_related.go b/backend/internal/handlers/handlers_user_related.go index 39788ae..bc4ae2d 100644 --- a/backend/internal/handlers/handlers_user_related.go +++ b/backend/internal/handlers/handlers_user_related.go @@ -59,9 +59,9 @@ func (gs *GState) UserDelete(c *fiber.Ctx) error { // Read username from Locals auth_username := c.Locals("user").(*jwt.Token).Claims.(jwt.MapClaims)["name"].(string) - if username != auth_username { - log.Info("User tried to delete another user") - return c.Status(403).SendString("You can only delete yourself") + if username == auth_username { + log.Info("User tried to delete itself") + return c.Status(403).SendString("You can't delete yourself") } if err := gs.Db.RemoveUser(username); err != nil { diff --git a/backend/main.go b/backend/main.go index ff6b94e..7d98918 100644 --- a/backend/main.go +++ b/backend/main.go @@ -84,7 +84,7 @@ func main() { // Protected routes (require a valid JWT bearer token authentication header) server.Post("/api/submitWeeklyReport", gs.SubmitWeeklyReport) - server.Get("/api/getUserProjects", gs.GetUserProjects) + server.Get("/api/getUserProjects/:username", gs.GetUserProjects) server.Post("/api/loginrenew", gs.LoginRenew) server.Delete("/api/userdelete/:username", gs.UserDelete) // Perhaps just use POST to avoid headaches server.Delete("api/project/:projectID", gs.DeleteProject) // WIP diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index 5c49a8d..39b5d0a 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -6,6 +6,8 @@ import { NewProject, UserProjectMember, WeeklyReport, + StrNameChange, + NewProjMember, } from "../Types/goTypes"; /** @@ -112,10 +114,14 @@ interface API { ): Promise>; /** Gets all the projects of a user + * @param {string} username - The authentication token. * @param {string} token - The authentication token. * @returns {Promise>} A promise containing the API response with the user's projects. */ - getUserProjects(token: string): Promise>; + getUserProjects( + username: string, + token: string, + ): Promise>; /** Gets a project by its id. * @param {number} id The id of the project to retrieve. @@ -133,6 +139,20 @@ interface API { projectName: string, token: string, ): Promise>; + /** + * Changes the username of a user in the database. + * @param {StrNameChange} data The object containing the previous and new username. + * @param {string} token The authentication token. + * @returns {Promise>} A promise resolving to an API response. + */ + changeUserName( + data: StrNameChange, + token: string, + ): Promise>; + addUserToProject( + user: NewProjMember, + token: string, + ): Promise>; } /** An instance of the API */ @@ -170,19 +190,17 @@ export const api: API = { ): Promise> { try { const response = await fetch(`/api/userdelete/${username}`, { - method: "POST", + method: "DELETE", headers: { "Content-Type": "application/json", Authorization: "Bearer " + token, }, body: JSON.stringify(username), }); - if (!response.ok) { - return { success: false, message: "Failed to remove user" }; + return { success: false, message: "Could not remove user" }; } else { - const data = (await response.json()) as User; - return { success: true, data }; + return { success: true }; } } catch (e) { return { success: false, message: "Failed to remove user" }; @@ -243,6 +261,30 @@ export const api: API = { } }, + async addUserToProject( + user: NewProjMember, + token: string, + ): Promise> { + try { + const response = await fetch("/api/addUserToProject", { + 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" }; + } else { + return { success: true, message: "Added member" }; + } + } catch (e) { + return { success: false, message: "Failed to add member" }; + } + }, + async renewToken(token: string): Promise> { try { const response = await fetch("/api/loginrenew", { @@ -264,9 +306,12 @@ export const api: API = { } }, - async getUserProjects(token: string): Promise> { + async getUserProjects( + username: string, + token: string, + ): Promise> { try { - const response = await fetch("/api/getUserProjects", { + const response = await fetch(`/api/getUserProjects/${username}`, { method: "GET", headers: { "Content-Type": "application/json", @@ -484,4 +529,28 @@ export const api: API = { }); } }, + + async changeUserName( + data: StrNameChange, + token: string, + ): Promise> { + try { + const response = await fetch("/api/changeUserName", { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + return { success: false, message: "Failed to change username" }; + } else { + return { success: true }; + } + } catch (e) { + return { success: false, message: "Failed to change username" }; + } + }, }; diff --git a/frontend/src/Components/AddMember.tsx b/frontend/src/Components/AddMember.tsx new file mode 100644 index 0000000..d29be68 --- /dev/null +++ b/frontend/src/Components/AddMember.tsx @@ -0,0 +1,39 @@ +import { APIResponse, api } from "../API/API"; +import { NewProjMember } from "../Types/goTypes"; + +/** + * Tries to add a member to a project + * @param {Object} props - A NewProjMember + * @returns {boolean} True if added, false if not + */ +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; + } + 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; +} + +export default AddMember; diff --git a/frontend/src/Components/AddUserToProject.tsx b/frontend/src/Components/AddUserToProject.tsx new file mode 100644 index 0000000..9f4439b --- /dev/null +++ b/frontend/src/Components/AddUserToProject.tsx @@ -0,0 +1,92 @@ +import { useState } from "react"; +import { NewProjMember } from "../Types/goTypes"; +import Button from "./Button"; +import GetAllUsers from "./GetAllUsers"; +import AddMember from "./AddMember"; +import BackButton from "./BackButton"; + +/** + * Provides UI for adding a member to a project. + * @returns {JSX.Element} - Returns the component UI for adding a member + */ +function AddUserToProject(): JSX.Element { + const [name, setName] = useState(""); + const [users, setUsers] = useState([]); + const [role, setRole] = useState(""); + GetAllUsers({ setUsersProp: setUsers }); + + const handleClick = (): boolean => { + const newMember: NewProjMember = { + username: name, + projectname: localStorage.getItem("projectName") ?? "", + role: role, + }; + return AddMember({ memberToAdd: newMember }); + }; + + return ( +
+

+ User chosen: [{name}] +

+

+ Role chosen: [{role}] +

+

+ Project chosen: [{localStorage.getItem("projectName") ?? ""}] +

+

Choose role:

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

Choose user:

+
+
    +
    + {users.map((user) => ( +
  • { + setName(user); + }} + > + {user} +
  • + ))} +
+
+
+
+

+
+ ); +} + +export default AddUserToProject; diff --git a/frontend/src/Components/ChangeUsername.tsx b/frontend/src/Components/ChangeUsername.tsx index 3c35e94..e297a04 100644 --- a/frontend/src/Components/ChangeUsername.tsx +++ b/frontend/src/Components/ChangeUsername.tsx @@ -1,23 +1,48 @@ import React, { useState } from "react"; import InputField from "./InputField"; +import { api } from "../API/API"; function ChangeUsername(): JSX.Element { const [newUsername, setNewUsername] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); const handleChange = (e: React.ChangeEvent): void => { setNewUsername(e.target.value); }; - // const handleSubmit = async (): Promise => { - // try { - // // Call the API function to update the username - // await api.updateUsername(newUsername); - // // Optionally, add a success message or redirect the user - // } catch (error) { - // console.error("Error updating username:", error); - // // Optionally, handle the error - // } - // }; + const handleSubmit = async (): Promise => { + try { + // Call the API function to change the username + const token = localStorage.getItem("accessToken"); + if (!token) { + throw new Error("Access token not found"); + } + + const response = await api.changeUserName( + { prevName: "currentName", newName: newUsername }, + token, + ); + + if (response.success) { + // Optionally, add a success message or redirect the user + console.log("Username changed successfully"); + } else { + // Handle the error message + console.error("Failed to change username:", response.message); + setErrorMessage(response.message ?? "Failed to change username"); + } + } catch (error) { + console.error("Error changing username:", error); + // Optionally, handle the error + setErrorMessage("Failed to change username"); + } + }; + + const handleButtonClick = (): void => { + handleSubmit().catch((error) => { + console.error("Error in handleSubmit:", error); + }); + }; return (
@@ -27,6 +52,8 @@ function ChangeUsername(): JSX.Element { value={newUsername} onChange={handleChange} /> + {errorMessage &&
{errorMessage}
} +
); } diff --git a/frontend/src/Components/DeleteUser.tsx b/frontend/src/Components/DeleteUser.tsx index db49724..d1dbc7f 100644 --- a/frontend/src/Components/DeleteUser.tsx +++ b/frontend/src/Components/DeleteUser.tsx @@ -11,7 +11,6 @@ import { api, APIResponse } from "../API/API"; */ function DeleteUser(props: { usernameToDelete: string }): boolean { - //console.log(props.usernameToDelete); FOR DEBUG let removed = false; api .removeUser( @@ -20,12 +19,16 @@ function DeleteUser(props: { usernameToDelete: string }): boolean { ) .then((response: APIResponse) => { if (response.success) { + alert("User has been deleted!"); + location.reload(); removed = true; } else { + alert("User has not been deleted"); console.error(response.message); } }) .catch((error) => { + alert("User has not been deleted"); console.error("An error occurred during creation:", error); }); return removed; diff --git a/frontend/src/Components/DisplayUserProjects.tsx b/frontend/src/Components/DisplayUserProjects.tsx index f4fd782..0cd5a8e 100644 --- a/frontend/src/Components/DisplayUserProjects.tsx +++ b/frontend/src/Components/DisplayUserProjects.tsx @@ -1,7 +1,7 @@ -import { useState, useEffect } from "react"; +import { useState } from "react"; import { Project } from "../Types/goTypes"; import { Link } from "react-router-dom"; -import { api } from "../API/API"; +import GetProjects from "./GetProjects"; /** * Renders a component that displays the projects a user is a part of and links to the projects start-page. @@ -10,21 +10,10 @@ import { api } from "../API/API"; function DisplayUserProject(): JSX.Element { const [projects, setProjects] = useState([]); - const getProjects = async (): Promise => { - const token = localStorage.getItem("accessToken") ?? ""; - const response = await api.getUserProjects(token); - console.log(response); - if (response.success) { - setProjects(response.data ?? []); - } else { - console.error(response.message); - } - }; - - // Call getProjects when the component mounts - useEffect(() => { - void getProjects(); - }, []); + GetProjects({ + setProjectsProp: setProjects, + username: localStorage.getItem("username") ?? "", + }); return ( <> diff --git a/frontend/src/Components/GetProjects.tsx b/frontend/src/Components/GetProjects.tsx index d6ab1f7..764b082 100644 --- a/frontend/src/Components/GetProjects.tsx +++ b/frontend/src/Components/GetProjects.tsx @@ -12,6 +12,7 @@ import { api } from "../API/API"; */ function GetProjects(props: { setProjectsProp: Dispatch>; + username: string; }): void { const setProjects: Dispatch> = props.setProjectsProp; @@ -19,7 +20,7 @@ function GetProjects(props: { const fetchUsers = async (): Promise => { try { const token = localStorage.getItem("accessToken") ?? ""; - const response = await api.getUserProjects(token); + const response = await api.getUserProjects(props.username, token); if (response.success) { setProjects(response.data ?? []); } else { @@ -31,7 +32,7 @@ function GetProjects(props: { }; void fetchUsers(); - }, [setProjects]); + }, [props.username, setProjects]); } export default GetProjects; diff --git a/frontend/src/Components/ProjectInfoModal.tsx b/frontend/src/Components/ProjectInfoModal.tsx index b153e9c..3075b19 100644 --- a/frontend/src/Components/ProjectInfoModal.tsx +++ b/frontend/src/Components/ProjectInfoModal.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import Button from "./Button"; import { UserProjectMember } from "../Types/goTypes"; import GetUsersInProject from "./GetUsersInProject"; +import { Link } from "react-router-dom"; function ProjectInfoModal(props: { isVisible: boolean; @@ -18,9 +19,12 @@ function ProjectInfoModal(props: { className="fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm flex justify-center items-center" > -
+
-

Project members:

+

+ {localStorage.getItem("projectName") ?? ""} +

+

Project members:

    @@ -50,6 +54,15 @@ function ProjectInfoModal(props: { }} type="button" /> + +