Merge branch 'frontend' into gruppDM

This commit is contained in:
Davenludd 2024-04-04 20:10:57 +02:00
commit 544383809b
14 changed files with 346 additions and 138 deletions

View file

@ -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
}

View file

@ -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)
}

View file

@ -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})
}

View file

@ -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)

View file

@ -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<APIResponse<void>>;
addUserToProject(
user: NewProjMember,
addMemberInfo: AddMemberInfo,
token: string,
): Promise<APIResponse<void>>;
removeUserFromProject(
user: string,
project: string,
token: string,
): Promise<APIResponse<void>>;
@ -227,6 +233,12 @@ interface API {
projectName: string,
token: string,
): Promise<APIResponse<string>>;
/**
* 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<APIResponse<string>>;
}
/** An instance of the API */
@ -336,18 +348,20 @@ export const api: API = {
},
async addUserToProject(
user: NewProjMember,
addMemberInfo: AddMemberInfo,
token: string,
): Promise<APIResponse<void>> {
try {
const response = await fetch("/api/addUserToProject", {
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<APIResponse<void>> {
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<APIResponse<string>> {
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<APIResponse<string>> {
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" };
}
},
};

View file

@ -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<void>}
*/
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<void> {
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<void>) => {
);
if (response.success) {
alert("Member added");
added = true;
alert(`[${props.memberToAdd.userName}] added`);
} else {
alert("Member not added");
alert(`[${props.memberToAdd.userName}] not added`);
console.error(response.message);
}
})
.catch((error) => {
} catch (error) {
alert(`[${props.memberToAdd.userName}] not added`);
console.error("An error occurred during member add:", error);
});
return added;
}
}
export default AddMember;

View file

@ -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<void>) => {
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<void> => {
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 (
<div className="flex flex-col h-fit w-screen items-center justify-center">
<div className="border-4 border-black bg-white flex flex-col items-center justify-center h-fit w-fit rounded-3xl content-center pl-20 pr-20">
@ -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();
}}
>
<img

View file

@ -1,71 +1,86 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import Button from "./Button";
import GetAllUsers from "./GetAllUsers";
import AddMember, { NewProjMember } from "./AddMember";
import AddMember, { AddMemberInfo } from "./AddMember";
import BackButton from "./BackButton";
import GetUsersInProject, { ProjectMember } from "./GetUsersInProject";
import GetAllUsers from "./GetAllUsers";
/**
* Provides UI for adding a member to a project.
* @returns {JSX.Element} - Returns the component UI for adding a member
*/
function AddUserToProject(props: { projectName: string }): JSX.Element {
const [name, setName] = useState("");
const [names, setNames] = useState<string[]>([]);
const [users, setUsers] = useState<string[]>([]);
const [role, setRole] = useState("");
GetAllUsers({ setUsersProp: setUsers });
const [usersProj, setUsersProj] = useState<ProjectMember[]>([]);
const handleClick = (): boolean => {
const newMember: NewProjMember = {
username: name,
projectname: props.projectName,
role: role,
// 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<void> => {
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,
};
return AddMember({ memberToAdd: newMember });
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 (
<div className="border-4 border-black bg-white flex flex-col items-center justify-center rounded-3xl content-center pl-20 pr-20 h-[75vh] w-[50vh]">
<p className="pb-4 mb-2 text-center font-bold text-[18px]">
User chosen: [{name}]
<div className="border-4 border-black bg-white flex flex-col items-center pt-10 rounded-3xl content-center pl-20 pr-20 h-[63vh] w-[50] overflow-auto">
<h1 className="text-center font-bold text-[36px] pb-10">
{props.projectName}
</h1>
<p className="p-1 text-center font-bold text-[26px]">
Choose users to add:
</p>
<p className="pb-4 mb-2 text-center font-bold text-[18px]">
Role chosen: [{role}]
</p>
<p className="pb-4 mb-2 text-center font-bold text-[18px]">
Project chosen: [{props.projectName}]
</p>
<p className="p-1">Choose role:</p>
<div className="border-2 border-black p-2 rounded-xl text-center h-[10h] w-[16] overflow-auto">
<ul className="text-center items-center font-medium space-y-2">
<li
className="h-[10] w-[14] items-start px-2 py-1 border-2 border-black rounded-full bg-orange-200 hover:bg-orange-600 hover:text-slate-100 hover:cursor-pointer"
onClick={() => {
setRole("member");
}}
>
{"Member"}
</li>
<li
className="h-[10] w-[14] items-start px-2 py-1 border-2 border-black rounded-full bg-orange-200 hover:bg-orange-600 hover:text-slate-100 hover:cursor-pointer"
onClick={() => {
setRole("project_manager");
}}
>
{"Project manager"}
</li>
</ul>
</div>
<p className="p-1">Choose user:</p>
<div className="border-2 border-black p-2 rounded-xl text-center overflow-scroll h-[26vh] w-[26vh]">
<div className="border-2 border-black pl-2 pr-2 pb-2 rounded-xl text-center overflow-auto h-[26vh] w-[26vh]">
<ul className="text-center font-medium space-y-2">
<div></div>
{users.map((user) => (
<li
className="items-start p-1 border-2 border-black rounded-full bg-orange-200 hover:bg-orange-600 hover:text-slate-100 hover:cursor-pointer"
className={
names.includes(user)
? "items-start p-1 border-2 border-transparent rounded-full bg-orange-500 hover:bg-orange-600 text-white hover:cursor-pointer ring-2 ring-black"
: "items-start p-1 border-2 border-black rounded-full bg-orange-200 hover:bg-orange-400 hover:text-slate-100 hover:cursor-pointer"
}
key={user}
value={user}
onClick={() => {
setName(user);
handleUserClick(user);
}}
>
<span>{user}</span>
@ -73,13 +88,16 @@ function AddUserToProject(props: { projectName: string }): JSX.Element {
))}
</ul>
</div>
<div className="flex space-x-5 items-center justify-between">
<p className="pt-10 pb-5 underline text-center font-bold text-[18px]">
Number of users to be added: {names.length}
</p>
<div className="space-x-10 items-center">
<Button
text="Add"
onClick={(): void => {
handleClick();
void handleAddClick();
}}
type="submit"
type="button"
/>
<BackButton />
</div>

View file

@ -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 {

View file

@ -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);
}
})

View file

@ -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 (
<div
className="fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm
className="fixed inset-10 bg-opacity-30 backdrop-blur-sm
flex justify-center items-center"
>
<div className="border-4 border-black bg-white rounded-lg text-center flex flex-col">
@ -42,13 +42,16 @@ function MemberInfoModal(props: {
<UserProjectListAdmin username={props.username} />
<div className="items-center space-x-6">
<Button
text={"Delete"}
text={"Remove"}
onClick={function (): void {
if (
window.confirm("Are you sure you want to delete this user?")
window.confirm(
"Are you sure you want to remove this user from the project?",
)
) {
DeleteUser({
usernameToDelete: props.username,
RemoveUserFromProj({
userToRemove: props.username,
projectName: props.projectName,
});
}
}}

View file

@ -15,17 +15,21 @@ export default function Register(): JSX.Element {
const [errMessage, setErrMessage] = useState<string>();
const handleRegister = async (): Promise<void> => {
if (username === "" || password === "") {
alert("Must provide username and password");
return;
}
const newUser: NewUser = {
username: username ?? "",
username: username?.replace(/ /g, "") ?? "",
password: password ?? "",
};
const response = await api.registerUser(newUser);
if (response.success) {
alert("User added!");
alert(`${newUser.username} added!`);
setPassword("");
setUsername("");
} else {
alert("User not added");
alert("User not added, name could be taken");
setErrMessage(response.message ?? "Unknown error");
console.error(errMessage);
}

View file

@ -0,0 +1,41 @@
import { api, APIResponse } from "../API/API";
/**
* Removes a user from a project
* @param {string} props.usernameToDelete - The username of user to remove
* @param {string} props.projectName - Project to remove user from
* @returns {void}
* @example
* const exampleUsername = "user";
* const exampleProjectName "project";
* RemoveUserFromProj({ userToRemove: exampleUsername, projectName: exampleProjectName });
*/
export default function RemoveUserFromProj(props: {
userToRemove: string;
projectName: string;
}): void {
if (props.userToRemove === localStorage.getItem("username")) {
alert("Cannot remove yourself");
return;
}
api
.removeUserFromProject(
props.userToRemove,
props.projectName,
localStorage.getItem("accessToken") ?? "",
)
.then((response: APIResponse<void>) => {
if (response.success) {
alert(`${props.userToRemove} has been removed!`);
location.reload();
} else {
alert(`${props.userToRemove} has not been removed due to an error`);
console.error(response.message);
}
})
.catch((error) => {
alert(`${props.userToRemove} has not been removed due to an error`);
console.error("An error occurred during deletion:", error);
});
}

View file

@ -28,7 +28,7 @@ function UserInfoModal(props: {
const handleClickChangeName = (): void => {
const nameChange: StrNameChange = {
prevName: props.username,
newName: newUsername,
newName: newUsername.replace(/ /g, ""),
};
ChangeUsername({ nameChange: nameChange });
};